@avantf/dsh-mem 0.8.1 → 0.9.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 (39) hide show
  1. package/lib/client.js +1088 -303
  2. package/lib/engine/graph-recall/index.d.ts +2 -2
  3. package/lib/engine/graph-recall/traverse.d.ts +2 -2
  4. package/lib/engine/graph-recall/types.d.ts +1 -1
  5. package/lib/engine/mem/config/config_files.d.ts +1 -1
  6. package/lib/engine/mem/defaults.d.ts +4 -1
  7. package/lib/engine/mem/entities/extract.d.ts +34 -14
  8. package/lib/engine/mem/index.d.ts +1 -0
  9. package/lib/engine/mem/modelBootstrap.d.ts +29 -0
  10. package/lib/engine/mem/store/graph_reader.d.ts +2 -2
  11. package/lib/engine/mem/store/memory.d.ts +53 -6
  12. package/lib/engine/mem/store/recall_policy.d.ts +10 -0
  13. package/lib/engine/mem-contract/config.d.ts +135 -0
  14. package/lib/engine/mem-contract/types.d.ts +11 -0
  15. package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +10 -0
  16. package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +28 -0
  17. package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +48 -6
  18. package/lib/engine/mem-retrieval/interfaces.d.ts +22 -2
  19. package/lib/engine/mem-retrieval/text_budget.d.ts +31 -0
  20. package/lib/index.js +1617 -114
  21. package/lib/interface-version.json +2 -2
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/client/paging.d.ts +58 -8
  24. package/lib/types/client/paging.d.ts.map +1 -1
  25. package/lib/types/client/time_filter.d.ts +54 -0
  26. package/lib/types/client/time_filter.d.ts.map +1 -0
  27. package/lib/types/deps_audit.d.ts +88 -0
  28. package/lib/types/deps_audit.d.ts.map +1 -0
  29. package/lib/types/envinit-bootstrap.d.ts +1 -1
  30. package/lib/types/envinit.d.ts +224 -4
  31. package/lib/types/envinit.d.ts.map +1 -1
  32. package/lib/types/hints.d.ts +58 -0
  33. package/lib/types/hints.d.ts.map +1 -1
  34. package/lib/types/index.d.ts.map +1 -1
  35. package/lib/types/render.d.ts.map +1 -1
  36. package/lib/types/runtime_deps_resource.d.ts +29 -0
  37. package/lib/types/runtime_deps_resource.d.ts.map +1 -0
  38. package/package.json +5 -4
  39. package/res/runtime-deps.yaml +99 -0
package/lib/index.js CHANGED
@@ -12,9 +12,9 @@ import { createHash, randomBytes } from "node:crypto";
12
12
  import { Readable } from "node:stream";
13
13
  import { pipeline } from "node:stream/promises";
14
14
  import { brotliCompressSync, brotliDecompressSync, constants, gunzipSync, gzipSync } from "node:zlib";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
15
16
  import { AsyncLocalStorage } from "node:async_hooks";
16
17
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
17
- import { fileURLToPath, pathToFileURL } from "node:url";
18
18
  //#region ../contract/lib/types.js
19
19
  /** Shared domain types for avantf-mem. Single source of truth in @avantf/mem-contract. */
20
20
  /**
@@ -174,6 +174,186 @@ const DEGRADED_LEG_NOTE = "本次仅词法腿,语义腿未就绪(语义后
174
174
  * with no error, so the adapter bounds the text first and warns once (DESIGN §20).
175
175
  */
176
176
  const maxInputTokens = z.number().int().nonnegative().default(0);
177
+ /**
178
+ * One registry/cache endpoint a self-heal section's attempts may name.
179
+ *
180
+ * SHARED by `semantic.self_heal` (transformers.js) and `semantic.native_self_heal` (the native
181
+ * packages): the base `runtime-deps` provider takes ONE source vocabulary, and two spellings of it
182
+ * would be two ways to get the same install wrong.
183
+ *
184
+ * Mirrors the provider's `DependencySource` (interface v5) without importing it: this package is
185
+ * also used by the CLI/MCP processes, which never load the base. An empty `cache` on a source with
186
+ * no registry means "the family root's npm cache" — the plugin fills in `<home>/cache/npm` when it
187
+ * builds the item, because this schema does not know the family root.
188
+ */
189
+ const selfHealSourceSchema = z.object({
190
+ id: z.string().min(1),
191
+ registry: z.string().default(""),
192
+ cache: z.string().default("")
193
+ });
194
+ /**
195
+ * One install attempt, in the base provider's vocabulary. SHARED by both self-heal sections.
196
+ *
197
+ * `env` exists mainly for `ONNXRUNTIME_NODE_INSTALL=skip`: `onnxruntime-node`'s postinstall download
198
+ * returns 302 on some mirrors, and skipping that check is enough for transformers.js to load its
199
+ * WASM/CPU fallback.
200
+ *
201
+ * `allowScripts` is explicit in this plugin's defaults: the base provider defaults it to `false`
202
+ * (lifecycle scripts from a registry package are a caller decision), and `onnxruntime-node` needs its
203
+ * postinstall unless the attempt sets `ONNXRUNTIME_NODE_INSTALL=skip`. Leaving it unset here would
204
+ * turn every default attempt into an `--ignore-scripts` install and break the native binary. The same
205
+ * holds — more strongly — for the native section: `nodejieba` and `hnswlib-node` are node-gyp builds
206
+ * whose ONLY artefact is produced by the install script.
207
+ */
208
+ const selfHealAttemptSchema = z.object({
209
+ source: z.string().min(1),
210
+ manager: z.enum(["npm", "pnpm"]).optional(),
211
+ env: z.record(z.string(), z.string()).default({}),
212
+ allowScripts: z.boolean().optional()
213
+ });
214
+ /**
215
+ * One entry of the STARTUP DEPENDENCY AUDIT (`audit:` in `res/runtime-deps.yaml`): what to observe,
216
+ * how loud to say it, and under which condition — a session notice, plus the probe that decides
217
+ * whether it is due.
218
+ *
219
+ * WHY IT IS DATA, NOT CODE. The interesting question at the end of a mount is "which of the optional
220
+ * things this build is made of can this process actually load?" — and the set differs per RELEASE (a
221
+ * native binding added or dropped, a mirror changed, a sentence reworded). Writing those names and
222
+ * sentences in code would make the implementation carry a deployment's vocabulary. So the plugin
223
+ * implements two GENERIC probe kinds, and the shipped declaration says which pieces exist and what a
224
+ * session is told about them. No package name, body, severity or condition is written in code.
225
+ *
226
+ * `kind` selects the probe:
227
+ *
228
+ * - `module` — resolve AND LOAD `specifier` (a `require`/`import`, so a native addon's binding is
229
+ * really loaded, not just found), from a ready self-heal root first and from this tree otherwise.
230
+ * States: `available` (loaded), `failed` (resolved, but loading it threw — the classic node-gyp
231
+ * build that exists and does not work), `missing` (nothing resolved).
232
+ * - `runtime-deps` — the STATE of the named `item` (`mem:semantic-deps`, `mem:native-deps`, or any
233
+ * other runtime-deps item): `available` (ready), `failed`, `missing`, `skipped`, `pending`.
234
+ *
235
+ * `when` is matched against that state, and the word means the same thing for both kinds:
236
+ * `failed` → `failed`; `missing` → `missing` or `skipped`; `degraded` → any of the three. An entry
237
+ * that is `pending` matches NOTHING: a line latched while a background install is still running would
238
+ * outlive the success it is waiting for. (`pending` only occurs for `runtime-deps`, whose item may be
239
+ * mid-install; a `module` probe is a single synchronous load and cannot be pending.)
240
+ *
241
+ * `min_vectors` (optional, `0`/omitted = every corpus) holds the line back until the live vector
242
+ * index reaches that size — the corpus-size knob an ANN entry needs (below the switch-over point the
243
+ * brute-force store is the intended backend, so a missing ANN binding is not a defect). It is a
244
+ * literal in the declaration, deliberately not derived from `vectorStore.auto_thresholds.hnswlib`:
245
+ * code that computed it would be branching on a package, which is what this section exists to avoid.
246
+ *
247
+ * `id` must be namespaced (`mem:…`) — it is the unit of the once-per-session latch, so an id shared
248
+ * with another plugin would silently suppress one of the two lines. The base registry warns and
249
+ * ignores a malformed one instead of throwing, and so this schema rejects it up front.
250
+ */
251
+ const dependencyAuditFields = {
252
+ id: z.string().min(1),
253
+ severity: z.enum([
254
+ "info",
255
+ "warning",
256
+ "degraded"
257
+ ]),
258
+ when: z.enum([
259
+ "failed",
260
+ "missing",
261
+ "degraded"
262
+ ]),
263
+ min_vectors: z.number().int().nonnegative().optional(),
264
+ body: z.string().min(1)
265
+ };
266
+ /** One audit entry. `kind` decides which probe runs, and which of `specifier`/`item` is required. */
267
+ const DependencyAuditEntrySchema = z.discriminatedUnion("kind", [z.object({
268
+ kind: z.literal("module"),
269
+ /** The npm specifier to load (`nodejieba`, `hnswlib-node`, `@huggingface/transformers`, …). */
270
+ specifier: z.string().min(1),
271
+ ...dependencyAuditFields
272
+ }), z.object({
273
+ kind: z.literal("runtime-deps"),
274
+ /** The runtime-deps item whose state is observed (`mem:semantic-deps`, `mem:native-deps`, …). */
275
+ item: z.string().min(1),
276
+ ...dependencyAuditFields
277
+ })]);
278
+ /**
279
+ * One `runtime-deps` declaration as SHIPPED WITH THE PLUGIN (`packages/plugin/res/runtime-deps.yaml`).
280
+ *
281
+ * WHY IT IS A RESOURCE AND NOT USER CONFIG. The package set, the sources, the attempts, the verify
282
+ * list and the audit sentences are facts about THIS BUILD: which module the local backend imports,
283
+ * which native bindings this release looks for, what a session should be told when one of them is
284
+ * absent. Leaving them to `configs/common.yaml` made every deployment responsible for keeping that
285
+ * list correct — a stale copy would install a module no code loads, or silently drop the audit — and
286
+ * the shipped defaults were only ever the plugin's own declaration anyway. The operator keeps the one
287
+ * thing that really is a per-deployment decision (the two `enabled` switches); the DATA travels in the
288
+ * package, next to the code that reads it.
289
+ *
290
+ * Validation is strict on purpose: the file is read from the INSTALL directory at mount, and a
291
+ * malformed one is a packaging bug. The plugin logs it and degrades (no self-heal item, no audit)
292
+ * rather than throwing out of the mount — see `packages/plugin/src/runtime_deps_resource.ts`.
293
+ *
294
+ * The schema is the SINGLE definition of the file's shape: the loader validates with it, and the
295
+ * plugin maps it to the base provider's objects field by field (the base never reads YAML).
296
+ */
297
+ const RuntimeDepsResourceSchema = z.object({
298
+ /** Format version, so a future shape can be recognized instead of misread. */
299
+ version: z.literal(1),
300
+ /** `@huggingface/transformers` — the semantic leg's optional module. */
301
+ semantic: z.object({
302
+ packages: z.record(z.string(), z.string()),
303
+ sources: z.array(selfHealSourceSchema),
304
+ attempts: z.array(selfHealAttemptSchema),
305
+ verify: z.array(z.string())
306
+ }),
307
+ /** The native optional dependencies (`nodejieba`, `hnswlib-node`). */
308
+ native: z.object({
309
+ packages: z.record(z.string(), z.string()),
310
+ sources: z.array(selfHealSourceSchema),
311
+ attempts: z.array(selfHealAttemptSchema),
312
+ verify: z.array(z.string())
313
+ }),
314
+ /**
315
+ * What the startup dependency audit observes, and what a session is told. Each entry is one probe
316
+ * (`kind`) plus its delivery decision — see {@link DependencyAuditEntrySchema}.
317
+ */
318
+ audit: z.array(DependencyAuditEntrySchema)
319
+ });
320
+ /**
321
+ * Semantic dependency self-heal (`@huggingface/transformers` + transitive `onnxruntime-node`).
322
+ *
323
+ * **OFF by default** on purpose: enabling it means the plugin may run a package-manager install at
324
+ * startup, i.e. network access before anyone asked for it. A profile that wants the repair opts in
325
+ * (`semantic.self_heal.enabled: true`); without it the runtime keeps its existing optional-dependency
326
+ * resolution and degrades to FTS+entity when the package is absent.
327
+ *
328
+ * The SPEC itself (packages / sources / attempts / verify) is NOT here: it ships in
329
+ * `res/runtime-deps.yaml` (`RuntimeDepsResourceSchema`). This section is the operator's policy — the
330
+ * only per-deployment decision left, because it costs a startup install.
331
+ */
332
+ const semanticSelfHealSchema = z.object({ enabled: z.boolean().default(false) });
333
+ /**
334
+ * NATIVE dependency self-heal (`nodejieba`, `hnswlib-node`) — the optional npm dependencies whose
335
+ * absence degrades a FEATURE rather than the plugin:
336
+ *
337
+ * - `nodejieba` → POS tagging; without it entity/relation extraction falls back to a regex pass
338
+ * (weaker entity + jaccard legs, still functional),
339
+ * - `hnswlib-node` → the ANN vector index; without it queries stay on the brute-force store (exact,
340
+ * slower once the corpus grows).
341
+ *
342
+ * Both are node-gyp packages, so their shipped `attempts` allow install scripts (see
343
+ * `res/runtime-deps.yaml`): an `--ignore-scripts` install of either produces a `require()` that fails
344
+ * at run time, which is the one outcome a "self-heal" must not produce.
345
+ *
346
+ * One item, two packages, ONE store root per spec: the base's `runtime-deps` item is
347
+ * content-addressed, so this spec (a different package set from the semantic one) lands in a
348
+ * DIFFERENT immutable root under `<home>/deps/runtime/<key>/` — never the transformers root, whose
349
+ * `node_modules` does not carry either native build.
350
+ *
351
+ * OFF by default, for the same reason as `self_heal`: enabling it means a package-manager install
352
+ * (and a native compile) at startup. What a session is told about a missing package is an `audit`
353
+ * entry in the same resource file, judged on the package itself and therefore working with or without
354
+ * a repair.
355
+ */
356
+ const nativeSelfHealSchema = z.object({ enabled: z.boolean().default(false) });
177
357
  const semanticSchema = z.object({
178
358
  backend: z.string().default("local_bge"),
179
359
  local_model: z.string().default("Xenova/bge-base-zh-v1.5"),
@@ -193,7 +373,14 @@ const semanticSchema = z.object({
193
373
  * stay, but nothing re-encodes on its own. Set to `false` on a metered/embedded host that must not
194
374
  * spend CPU on background re-encoding.
195
375
  */
196
- auto_migrate: z.boolean().default(true)
376
+ auto_migrate: z.boolean().default(true),
377
+ /** Repair a missing `@huggingface/transformers` install through the base's shared runtime store. */
378
+ self_heal: semanticSelfHealSchema.prefault({}),
379
+ /**
380
+ * Repair a missing NATIVE optional dependency (`nodejieba` / `hnswlib-node`) through the same
381
+ * base mechanism, in its OWN store root (see {@link nativeSelfHealSchema}).
382
+ */
383
+ native_self_heal: nativeSelfHealSchema.prefault({})
197
384
  });
198
385
  const vectorStoreSchema = z.object({
199
386
  backend: z.string().default("auto"),
@@ -731,9 +918,13 @@ const NULLABLE_HIT_FIELDS = /* @__PURE__ */ new Set([
731
918
  ]);
732
919
  /**
733
920
  * The envelope a recall result carries besides its rows. Two of these are only meaningful in one
734
- * mode — `total`/`offset`/`limit`/`truncated` describe an EXACT page, `degraded` describes a fuzzy
735
- * pass — and the projection keeps whichever are present, so one function serves both shapes
736
- * (RECALL_INTERFACE §5).
921
+ * mode — `total`/`offset`/`limit`/`remaining`/`truncated` describe a paged answer (`remaining` only
922
+ * exists on a pure time window, which pages at 50), `degraded` describes a fuzzy pass — and the
923
+ * projection keeps whichever are present, so one function serves every shape (RECALL_INTERFACE §5).
924
+ *
925
+ * `remaining` is here for a specific reason: "how much of the window did I not see" is a fact the
926
+ * model must act on (narrow the window, or page on), and leaving it out left only `truncated: true`
927
+ * with the size of the gap to be guessed.
737
928
  *
738
929
  * `weights`, `floors`, `dropped_by_floor` and `used_tokens` are the operator's diagnostics: a
739
930
  * threshold the caller cannot set and a drop count it cannot act on are pure tokens in the model's
@@ -744,6 +935,7 @@ const RESULT_ENVELOPE_FIELDS = [
744
935
  "total",
745
936
  "offset",
746
937
  "limit",
938
+ "remaining",
747
939
  "truncated"
748
940
  ];
749
941
  /** Copy the envelope fields a payload actually carries, in a fixed order. */
@@ -1068,9 +1260,16 @@ const TITLE_INCLUDE_EXCERPT_DESCRIPTION = "是否返回内容摘要(可选,
1068
1260
  * The exact page's own field descriptions. Their numbers are the documented ones (page 10, cap 50)
1069
1261
  * and they apply ONLY to an exact result — the fuzzy pass is a fixed shortlist of five, so a caller
1070
1262
  * that got no exact hit cannot page it.
1263
+ *
1264
+ * `limit`'s DEFAULT is the one place the pure time window differs: a window is a LIST, so it is
1265
+ * paged at the contract's maximum (50) instead of the exact page's ten (core `timePage`,
1266
+ * `TIME_DEFAULT_LIMIT`), and its result reports `remaining` — how much of the window the page does
1267
+ * not show. The schema's `.max(50)` is unchanged: an omitted `limit` never goes through it, so the
1268
+ * default is a number the ENGINE picks, not a value the caller could send. `titles` keeps its own
1269
+ * description (unchanged).
1071
1270
  */
1072
1271
  const SEARCH_OFFSET_DESCRIPTION = "分页偏移(从 0 开始);可选,默认 0。只作用于分页类结果(search 的 exact/time/facts);模糊检索不接受分页。";
1073
- const SEARCH_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10。只作用于分页类结果(search 的 exact/time/facts);模糊检索固定返回 5 条。";
1272
+ const SEARCH_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10(纯时间召回即只给 `time` 时默认 50;该方法的结果另带 `remaining`=窗口内还有多少条未显示)。只作用于分页类结果(search 的 exact/time/facts);模糊检索固定返回 5 条。";
1074
1273
  const TITLES_LIMIT_DESCRIPTION = "返回条数上限(1-50);可选,默认 10。只作用于标题完整相等的精确结果;模糊标题检索固定返回 5 条。";
1075
1274
  const SEARCH_QUERY_DESCRIPTION = "检索文本(1–" + String(MAX_QUERY_CHARS) + " 字符)。已给 `time` 时可省略——只给 `time` 就是纯时间召回(`mode=time`:按 `time.field` 从新到旧,不再做字面/事实/关系/模糊检索)。";
1076
1275
  /**
@@ -1089,7 +1288,7 @@ const SearchRequestSchema = z.object({
1089
1288
  source: z.string().optional().describe("限定来源(`fact_sources.ref`):来源等于该值的事实才参与本次检索;可选,不参与打分(未给 = 不过滤)。"),
1090
1289
  offset: z.number().int().nonnegative().optional().describe(SEARCH_OFFSET_DESCRIPTION),
1091
1290
  limit: z.number().int().positive().max(50).optional().describe(SEARCH_LIMIT_DESCRIPTION),
1092
- max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。"),
1291
+ max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省:exact/facts/模糊用配置 retrieval.max_output_tokens;纯时间召回不套预算。"),
1093
1292
  floors: z.enum(FLOOR_PROFILES).optional().describe(FLOORS_FIELD_DESCRIPTION),
1094
1293
  include_scores: z.boolean().optional().describe(INCLUDE_SCORES_DESCRIPTION),
1095
1294
  time: TimeFilterSchema.optional().describe("时间过滤(可选):内容 ∩ 时间。硬过滤——窗口内的事实不会被窗口外的高分结果挤掉;给了它就按它,不再叠隐式时间窗。没给 `query` 时必填:时间窗就是整个问题(纯时间召回)。")
@@ -1743,7 +1942,7 @@ function warn$1(log, message) {
1743
1942
  log?.warn(message);
1744
1943
  } catch {}
1745
1944
  }
1746
- function reasonOf$1(error) {
1945
+ function reasonOf$3(error) {
1747
1946
  return error instanceof Error ? error.message : String(error);
1748
1947
  }
1749
1948
  /**
@@ -1775,7 +1974,7 @@ async function loadGuarded(options, loadOnce, log, words) {
1775
1974
  try {
1776
1975
  return await loadOnce(options);
1777
1976
  } catch (error) {
1778
- warn$1(log, `${words.prefix} WARNING — environment initialisation failed (${reasonOf$1(error)}); ${words.loadFailure}`);
1977
+ warn$1(log, `${words.prefix} WARNING — environment initialisation failed (${reasonOf$3(error)}); ${words.loadFailure}`);
1779
1978
  return;
1780
1979
  }
1781
1980
  }
@@ -1844,6 +2043,12 @@ function stillActive(ctx) {
1844
2043
  * `agent/pre-step` listener), and the harness shows such a message in the trace as coming from the
1845
2044
  * user. Without an author, a line like "记忆里有相关内容" reads as something the user said. The
1846
2045
  * `[avantf-mem]` prefix is what keeps the provenance honest.
2046
+ *
2047
+ * The step's injection is not only this hint: the plugin's DEPENDENCY NOTICES (the `audit` entries of
2048
+ * the shipped `res/runtime-deps.yaml`) ride in the same message ({@link stepInjection}). They are a
2049
+ * different producer of text with the same delivery rule — at most once per owner session, only on a
2050
+ * step that already carried a user message — so merging them keeps ONE message per step instead of
2051
+ * two competing ones.
1847
2052
  */
1848
2053
  const RELEVANCE_HINT = "[avantf-mem] 记忆里有与上条用户消息相关的内容;需要时用 `mem_recall` 检索。";
1849
2054
  /**
@@ -1879,6 +2084,92 @@ function messageText(message) {
1879
2084
  return b?.type === "text" && typeof b.text === "string" ? b.text : "";
1880
2085
  }).filter(Boolean).join("\n");
1881
2086
  }
2087
+ /**
2088
+ * The text of the ONE message this step injects: the session notices, then the memory hint.
2089
+ *
2090
+ * `''` means "inject nothing" — the loop's turn-end guard is `decision.messages.length === 0`, so an
2091
+ * empty injection has to stay empty rather than become an empty user message.
2092
+ *
2093
+ * Order: notices first. They report a capability that is not working (a repair the operator asked for
2094
+ * did not finish), which is the louder statement; the hint is an invitation. Neither repeats within a
2095
+ * session: the notice registry latches per session id, and the hint is one line by construction.
2096
+ *
2097
+ * Each body is used AS CONFIGURED, with one exception that is about YAML and not about editing: a
2098
+ * block scalar (`body: |`) ends with a newline, and keeping it would insert a blank line between the
2099
+ * notice and the hint. Trailing whitespace is therefore dropped; nothing else is rewritten, and a
2100
+ * whitespace-only body is skipped rather than rendered as an empty paragraph.
2101
+ */
2102
+ function stepInjection(hint, notices) {
2103
+ const lines = [];
2104
+ for (const line of notices) {
2105
+ if (typeof line !== "string") continue;
2106
+ const body = line.trimEnd();
2107
+ if (body.trim() === "") continue;
2108
+ lines.push(body);
2109
+ }
2110
+ if (hint) lines.push(RELEVANCE_HINT);
2111
+ return lines.join("\n");
2112
+ }
2113
+ /**
2114
+ * The session-notice lines for this step, or `[]` when there are none to deliver.
2115
+ *
2116
+ * Three rules, all of them the caller's to make and this plugin's to keep:
2117
+ *
2118
+ * 1. **`ownerOnly` is a CLAIM, not a lookup.** The base registry cannot know which session owns the
2119
+ * plugin, so it serves only a call that claims it ({@link isOwnerSession} decides), and a
2120
+ * non-claiming call latches nothing — a worker/subagent session therefore never consumes the line
2121
+ * out from under the owner session.
2122
+ * 2. **Nothing is cached here.** `consume()` re-reads the item's real state on every step, so a
2123
+ * dependency that healed stops matching and a NEW session simply sees nothing — "fixed ⇒ silent"
2124
+ * needs no invalidation of its own.
2125
+ * 3. **Never throws.** This runs on the prompt path of a live agent: a hostile/mismatched registry
2126
+ * costs one skipped line, never the step.
2127
+ */
2128
+ function noticeLines(registry, sessionId, owner) {
2129
+ if (registry === void 0 || sessionId === "") return [];
2130
+ try {
2131
+ const lines = registry.consume(sessionId, { ownerOnly: owner });
2132
+ if (!Array.isArray(lines)) return [];
2133
+ return lines.filter((line) => typeof line === "string");
2134
+ } catch {
2135
+ return [];
2136
+ }
2137
+ }
2138
+ /**
2139
+ * Whether `sessionId` is the OWNER session — a TOP-LEVEL agent, not a worker/subagent.
2140
+ *
2141
+ * Read STRUCTURALLY off the host's agent registry (`ctx.agents`), deliberately without a dependency
2142
+ * on `@deepseek-ai/dsh-agent`: the only question is "is this id one of the roots", and the answer
2143
+ * decides a claim the base registry would otherwise have to invent a role for. A subagent session is
2144
+ * created under its parent's context, so it is never a root — and a mission executor is a subagent
2145
+ * too. Those sessions keep working (memory tools, retrieval), they just do not receive the owner's
2146
+ * repair notice.
2147
+ *
2148
+ * "Cannot tell" DELIVERS (`true`): a host with no agent registry, an older registry without `roots()`,
2149
+ * a throwing read — none of those prove the session is a worker, and swallowing a real notice on a
2150
+ * guess would be the worse failure. This mirrors the family rule that only a PROVEN condition may
2151
+ * take a capability away.
2152
+ */
2153
+ function isOwnerSession(agents, sessionId) {
2154
+ if (sessionId === "") return false;
2155
+ let roots;
2156
+ try {
2157
+ roots = agents?.roots;
2158
+ } catch {
2159
+ return true;
2160
+ }
2161
+ if (typeof roots !== "function") return true;
2162
+ try {
2163
+ const list = roots.call(agents);
2164
+ if (!Array.isArray(list)) return true;
2165
+ return list.some((agent) => {
2166
+ const id = agent?.id;
2167
+ return id !== void 0 && id !== null && String(id) === sessionId;
2168
+ });
2169
+ } catch {
2170
+ return true;
2171
+ }
2172
+ }
1882
2173
  //#endregion
1883
2174
  //#region ../core/lib/config/paths.js
1884
2175
  /**
@@ -1966,6 +2257,11 @@ const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。
1966
2257
  # dim: 768
1967
2258
  # auto_download: true
1968
2259
  # auto_migrate: true # 换模型/宽度后自动分批重算旧空间的向量(默认开);关掉只剩启动告警 + vectors --fix
2260
+ # self_heal: # 语义依赖自愈(默认关):允许启动期安装缺失的 @huggingface/transformers
2261
+ # enabled: false # 打开后走底座 runtime-deps 共享根(base ≥ 0.7.0 / interface v6);失败只降级到 FTS+实体
2262
+ # # 装哪个包 / 从哪装 / 提示说什么:见插件自带的 res/runtime-deps.yaml(随包发布)
2263
+ # native_self_heal: # 原生可选依赖自愈:nodejieba / hnswlib-node(默认关)
2264
+ # enabled: false # 装哪个包、从哪装、提示说什么,都由插件自带的 res/runtime-deps.yaml 决定
1969
2265
  # ⚠️ cache_dir / mirror 由家族底座 @avantf/dsh-plugin-base 管理:写在这里会被忽略并告警。
1970
2266
  # 缓存目录用环境变量 AVANTF_MEM_MODEL_CACHE,镜像用 AVANTF_MEM_MODEL_MIRROR(或 HF_ENDPOINT)。
1971
2267
 
@@ -2688,6 +2984,49 @@ function ensureAllAsync(options) {
2688
2984
  //#endregion
2689
2985
  //#region ../core/lib/entities/extract.js
2690
2986
  /**
2987
+ * Entity extraction: POS tagging and the noun-ish names that become `entities` rows.
2988
+ *
2989
+ * Uses nodejieba (optional dependency) for POS; degrades gracefully to a character/regex fallback
2990
+ * when unavailable, exactly like the `jieba_available()` guard.
2991
+ *
2992
+ * Note: nodejieba's `tag()` returns `{ word, tag }` objects, and its POS flag set
2993
+ * differs from Python jieba (e.g. proper nouns → `nr`, 名动词 → `vn`).
2994
+ *
2995
+ * WHAT THIS MODULE IS NOT. It owns what is an entity NAME — nothing about relations. The relation
2996
+ * half (SVO candidate triples, explicit patterns, the name/statement/confidence gates and the
2997
+ * predicate vocabulary) lives in `relations/`; the two halves share exactly one thing, the tagging
2998
+ * pass ({@link tagText}), so a write pays for the dictionary once and drives both.
2999
+ *
3000
+ * WHERE THE MODULE COMES FROM. `nodejieba` is an optional npm dependency, so it is normally resolved
3001
+ * from this tree. A host that provisions it instead (the DSH plugin's `runtime-deps` self-heal) sets
3002
+ * the shared root with {@link useTokenizerModuleDir}; the load then resolves from THAT root first and
3003
+ * falls back to this tree, which is the pre-existing behaviour and also the recovery path for a root
3004
+ * that was garbage-collected or damaged. Nothing here decides whether a root should exist.
3005
+ */
3006
+ /**
3007
+ * The shared module root the tokenizer should be loaded from, when the caller provisioned one.
3008
+ *
3009
+ * `undefined` (the default, and every CLI/MCP/test caller) = this tree's own optional dependency.
3010
+ * Set by {@link useTokenizerModuleDir} from the DSH plugin once the native self-heal item is READY;
3011
+ * a failed resolution from it falls back to this tree rather than degrading the tokenizer.
3012
+ */
3013
+ let moduleRoot$1;
3014
+ /**
3015
+ * Point the optional tokenizer at a shared module root (the base `runtime-deps` handle's directory).
3016
+ *
3017
+ * The memo follows the ROOT: changing it forgets whatever the previous root produced, so the next
3018
+ * load resolves from the new one — that is the whole point of setting a root. The cost is at most one
3019
+ * extra dictionary parse (~1 s, once) when a root arrives after a lazily loaded copy from this tree;
3020
+ * Node's own module cache keeps the import itself cheap. The plugin only sets this once, when its
3021
+ * native self-heal item settles.
3022
+ */
3023
+ function useTokenizerModuleDir(dir) {
3024
+ const next = dir === "" ? void 0 : dir;
3025
+ if (next === moduleRoot$1) return;
3026
+ moduleRoot$1 = next;
3027
+ jiebaLoad = void 0;
3028
+ }
3029
+ /**
2691
3030
  * The load, memoized as a PROMISE — not as its result.
2692
3031
  *
2693
3032
  * `mod.load()` parses nodejieba's dictionary synchronously on the main thread (~1 s measured), and
@@ -2698,28 +3037,67 @@ function ensureAllAsync(options) {
2698
3037
  * time — measured 2143 ms for two concurrent callers against 1000 ms for one.
2699
3038
  *
2700
3039
  * A FAILED load is cached too (the same one-attempt policy as before): a process whose jieba cannot
2701
- * be imported uses the regex fallback for its lifetime rather than retrying on every write.
3040
+ * be imported uses the regex fallback for its lifetime rather than retrying on every write. The one
3041
+ * exception is a module root that arrives afterwards — see {@link useTokenizerModuleDir}.
2702
3042
  */
2703
3043
  let jiebaLoad;
2704
3044
  /** How many times the dictionary was actually parsed in this process (see `jiebaLoadAttempts`). */
2705
3045
  let jiebaAttempts = 0;
3046
+ /** The root the LAST attempt resolved from (`undefined` = this tree); reported by {@link jiebaStatus}. */
3047
+ let jiebaAttemptedRoot;
3048
+ /** Why the last attempt failed, one short line — what makes an "unavailable" log line diagnosable. */
3049
+ let jiebaError;
2706
3050
  function loadJieba() {
2707
3051
  jiebaLoad ??= loadJiebaOnce();
2708
3052
  return jiebaLoad;
2709
3053
  }
3054
+ /**
3055
+ * Import `nodejieba` from the shared root, else from this tree.
3056
+ *
3057
+ * The shared root's own `package.json` is the resolution anchor (the same rule the semantic adapter
3058
+ * uses for transformers.js): its `node_modules` is what the provider installed, and importing by FILE
3059
+ * URL means this root's copy is the one loaded even when this tree also carries the package. A root
3060
+ * that no longer resolves is not an error to report here — the fallback below is the behaviour, and
3061
+ * the caller's dependency audit is what tells an operator about it.
3062
+ */
3063
+ async function importTokenizer(root) {
3064
+ if (root !== void 0) try {
3065
+ return await import(pathToFileURL(createRequire(join(root, "package.json")).resolve("nodejieba")).href);
3066
+ } catch {}
3067
+ return await import("nodejieba");
3068
+ }
3069
+ /** nodejieba's export shape, normalised (CJS through `import()` may land on `default`). */
3070
+ function tokenizerOf(raw) {
3071
+ const mod = typeof raw?.tag === "function" ? raw : raw?.default ?? raw;
3072
+ return mod !== null && typeof mod?.tag === "function" ? mod : null;
3073
+ }
2710
3074
  async function loadJiebaOnce() {
2711
3075
  jiebaAttempts += 1;
3076
+ const root = moduleRoot$1;
3077
+ jiebaAttemptedRoot = root;
2712
3078
  try {
2713
- const raw = await import("nodejieba");
2714
- const mod = typeof raw.tag === "function" ? raw : raw.default ?? raw;
3079
+ const mod = tokenizerOf(await importTokenizer(root));
3080
+ if (mod === null) throw new Error("nodejieba 的 tag() 不可用");
2715
3081
  if (typeof mod.load === "function") mod.load();
3082
+ jiebaError = void 0;
2716
3083
  return mod;
2717
- } catch {
3084
+ } catch (error) {
3085
+ jiebaError = error instanceof Error ? error.message : String(error);
3086
+ if (moduleRoot$1 !== root) jiebaLoad = void 0;
2718
3087
  return null;
2719
3088
  }
2720
3089
  }
2721
- async function jiebaAvailable() {
2722
- return await loadJieba() !== null;
3090
+ /** The status of the memoized load, starting one only when none has run yet. */
3091
+ async function jiebaProbe() {
3092
+ await loadJieba();
3093
+ const load = jiebaLoad;
3094
+ return {
3095
+ available: load !== void 0 && await load !== null,
3096
+ attempts: jiebaAttempts,
3097
+ attemptedRoot: jiebaAttemptedRoot,
3098
+ root: moduleRoot$1,
3099
+ error: jiebaError
3100
+ };
2723
3101
  }
2724
3102
  const NOUN_FLAGS = /* @__PURE__ */ new Set([
2725
3103
  "n",
@@ -2899,6 +3277,32 @@ async function awaitStartupGate(options = {}) {
2899
3277
  return await whenEventLoopIdle();
2900
3278
  }
2901
3279
  /**
3280
+ * The line one tokenizer observation deserves.
3281
+ *
3282
+ * `previous` is what makes the pair readable: a `ready` after a `degraded` is a RECOVERY (the
3283
+ * earlier probe ran before a loadable copy existed — the provisioned root had not been handed over,
3284
+ * or the package was not installed yet), not a change of story. A `degraded` line never claims more
3285
+ * than it knows: it names the reason and the two ways to fix it, and says that a loadable copy
3286
+ * arriving later is picked up automatically.
3287
+ *
3288
+ * Exported for the test that pins those transitions; it has no side effects.
3289
+ */
3290
+ function tokenizerReport(status, elapsedMs, previous) {
3291
+ if (status.available) return {
3292
+ level: "info",
3293
+ message: `model bootstrap: tokenizer ready in ${elapsedMs}ms (nodejieba${status.attemptedRoot === void 0 ? "" : ", provisioned root"})${previous === "degraded" ? " — recovered: the earlier \"unavailable\" line described a probe that ran before a loadable copy existed, not this state" : ""}`,
3294
+ next: "ready"
3295
+ };
3296
+ const reason = status.error === void 0 || status.error === "" ? "nodejieba could not be loaded" : status.error;
3297
+ return {
3298
+ level: "warn",
3299
+ message: `model bootstrap: nodejieba unavailable in ${elapsedMs}ms — ${status.attemptedRoot === void 0 ? `no loadable nodejieba in this tree (${reason})` : `the provisioned root ${status.attemptedRoot} cannot load nodejieba (${reason})`}${previous === "ready" ? " (the copy that was loadable earlier is no longer resolvable — a provisioned root replaced it, or it was removed)" : ""}; entity/triple extraction runs on the regex path (weaker entity + jaccard legs) until a loadable copy exists — install nodejieba, or enable semantic.native_self_heal to have the native item provision it (a provisioned root that arrives later is picked up automatically)`,
3300
+ next: "degraded"
3301
+ };
3302
+ }
3303
+ /** The last verdict reported in THIS process; see {@link TokenizerVerdict}. */
3304
+ let tokenizerVerdict = "unknown";
3305
+ /**
2902
3306
  * Warm ONLY the tokenizer, after the host-readiness signal and (when asked) an idle event loop.
2903
3307
  *
2904
3308
  * nodejieba loads its dictionary lazily on the FIRST `tag()` (measured ~950 ms, after which a call
@@ -2914,8 +3318,10 @@ async function warmTokenizer(rt, options = {}) {
2914
3318
  const waited = await awaitStartupGate(options);
2915
3319
  if (options.deferTokenizerUntilIdle === true && waited >= 200) rt.logger.info(`model bootstrap: tokenizer warm waited ${waited}ms for an idle event loop`);
2916
3320
  const jiebaStarted = Date.now();
2917
- if (await jiebaAvailable()) rt.logger.info(`model bootstrap: tokenizer ready in ${Date.now() - jiebaStarted}ms (nodejieba)`);
2918
- else rt.logger.warn(`model bootstrap: nodejieba unavailable in ${Date.now() - jiebaStarted}ms — entity/triple extraction falls back to the regex path (weaker entity + jaccard legs)`);
3321
+ const report = tokenizerReport(await jiebaProbe(), Date.now() - jiebaStarted, tokenizerVerdict);
3322
+ tokenizerVerdict = report.next;
3323
+ if (report.level === "warn") rt.logger.warn(report.message);
3324
+ else rt.logger.info(report.message);
2919
3325
  }
2920
3326
  /**
2921
3327
  * Non-blocking embedding warm, for a caller that knows the model files are already on disk.
@@ -3525,6 +3931,69 @@ function truncateToTokens(text, maxTokens, opts = {}) {
3525
3931
  };
3526
3932
  }
3527
3933
  /**
3934
+ * Slice `text` into windows of at most `maxTokens` estimated tokens instead of truncating it.
3935
+ *
3936
+ * WHY THIS EXISTS. The window belongs to the loaded model and the call site cannot widen it, so a
3937
+ * text past it has exactly one of two fates: cut (what {@link truncateToTokens} does, and what
3938
+ * transformers.js does silently on its own) or split. A knowledge chunk of ~1000 characters against
3939
+ * a 512-token model lost half its content from the vector that way — measured in production as
3940
+ * "503 tokens dropped here". Splitting keeps every token in the representation; the adapter that
3941
+ * can only return ONE vector then pools the window vectors (`LocalBgeBackend.encode`).
3942
+ *
3943
+ * Every returned window satisfies `estimate(window) <= maxTokens` (a one-character window is the
3944
+ * floor; the estimator cannot exceed a limit of 1 token for a single character). Cuts land on a
3945
+ * sentence/clause boundary when one is in the window's final quarter, so a window does not start or
3946
+ * end mid-word.
3947
+ *
3948
+ * @param text - the input.
3949
+ * @param maxTokens - the per-window budget (already net of special tokens, as the adapter computes it).
3950
+ * @param opts - estimator override (tests use a flat one); defaults to {@link estimateTokens}.
3951
+ */
3952
+ function splitToWindows(text, maxTokens, opts = {}) {
3953
+ const estimate = opts.estimate ?? estimateTokens;
3954
+ const limit = Math.max(1, Math.floor(maxTokens));
3955
+ const tokens = estimate(text);
3956
+ const chars = Array.from(text);
3957
+ if (chars.length === 0) return {
3958
+ windows: [text],
3959
+ tokens,
3960
+ windowTokens: [tokens]
3961
+ };
3962
+ if (tokens <= limit) return {
3963
+ windows: [text],
3964
+ tokens,
3965
+ windowTokens: [tokens]
3966
+ };
3967
+ const windows = [];
3968
+ const windowTokens = [];
3969
+ let start = 0;
3970
+ while (start < chars.length) {
3971
+ let used = 0;
3972
+ let end = start;
3973
+ while (end < chars.length) {
3974
+ const cost = CJK.test(chars[end]) ? 1 : chars[end].trim() === "" ? 0 : .25;
3975
+ if (used + cost > limit && end > start) break;
3976
+ used += cost;
3977
+ end += 1;
3978
+ }
3979
+ const floor = start + Math.floor((end - start) * .75);
3980
+ for (let i = end - 1; i >= floor; i--) if (BREAK.has(chars[i])) {
3981
+ end = i + 1;
3982
+ break;
3983
+ }
3984
+ while (end - start > 1 && estimate(chars.slice(start, end).join("")) > limit) end -= 1;
3985
+ const window = chars.slice(start, end).join("");
3986
+ windows.push(window);
3987
+ windowTokens.push(estimate(window));
3988
+ start = end;
3989
+ }
3990
+ return {
3991
+ windows,
3992
+ tokens,
3993
+ windowTokens
3994
+ };
3995
+ }
3996
+ /**
3528
3997
  * Resolve the effective input window for a loaded model.
3529
3998
  *
3530
3999
  * @param configured - `0` (or negative) means "auto: use the model's declared window".
@@ -3548,6 +4017,14 @@ function declaredWindowOf(pipe) {
3548
4017
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > SENTINEL_WINDOW) return void 0;
3549
4018
  return Math.floor(value);
3550
4019
  }
4020
+ /**
4021
+ * Characters of a chunk that fit the window for `window` tokens, minus the per-chunk
4022
+ * overhead (`specialTokens` for CLS/SEP). Used to derive `knowledge.chunk_size` so the
4023
+ * default chunking is justified by the shipped model's window instead of by feel.
4024
+ */
4025
+ function windowChars(window, specialTokens = 2) {
4026
+ return Math.max(1, Math.floor((window - specialTokens) / 1));
4027
+ }
3551
4028
  //#endregion
3552
4029
  //#region ../retrieval-core/lib/budget.js
3553
4030
  /**
@@ -3760,6 +4237,10 @@ var LocalBgeBackend = class {
3760
4237
  };
3761
4238
  return this._representation;
3762
4239
  }
4240
+ /** Point the loader at a shared runtime-dependency root before the first warmup. */
4241
+ useModuleDir(dir) {
4242
+ this.env.moduleDir = dir;
4243
+ }
3763
4244
  /**
3764
4245
  * Fire-and-forget retry for call sites that only observe availability. No-op
3765
4246
  * while the model is ready or an attempt is already in flight; otherwise
@@ -3808,42 +4289,118 @@ var LocalBgeBackend = class {
3808
4289
  return this._window;
3809
4290
  }
3810
4291
  /**
3811
- * Bound `text` to the window BEFORE it reaches transformers.js.
4292
+ * The window-sized slices `text` is encoded in (see `text_budget.splitToWindows`).
3812
4293
  *
3813
- * This is the only place the guard can live: the feature-extraction pipeline's `_call`
3814
- * accepts `{pooling, normalize, quantize, precision}` and nothing else, so `max_length`
3815
- * cannot be raised or lowered from the call site — past `model_max_length` the tokenizer
3816
- * truncates and says nothing.
4294
+ * WHERE THE BOUNDARY CAN LIVE. The feature-extraction pipeline's `_call` accepts
4295
+ * `{pooling, normalize, quantize, precision}` and nothing else, so `max_length` cannot be raised
4296
+ * from the call site — past `model_max_length` transformers.js truncates and says nothing. This
4297
+ * adapter is therefore the only place that can apply the model's own window, and it SPLITS rather
4298
+ * than cuts: a text past the window is a document chunk (or a long fact) whose tail is content,
4299
+ * and cutting it dropped 503 of ~1015 tokens out of the vector in production.
3817
4300
  */
3818
- bound(text) {
3819
- const out = truncateToTokens(text, Math.max(1, this.window() - SPECIAL_TOKENS));
3820
- if (out.truncated) {
3821
- recordTruncation("embedding");
3822
- if (!this._lengthWarned) {
3823
- this._lengthWarned = true;
3824
- retrievalLogger().warn(`semantic: input exceeded the ${this.window()}-token window of ${this.model} — text is truncated before encoding (${out.omittedTokens} tokens dropped here); check knowledge.chunk_size and semantic.max_input_tokens`);
3825
- }
3826
- }
3827
- return out;
4301
+ windows(text) {
4302
+ return splitToWindows(text, Math.max(1, this.window() - SPECIAL_TOKENS));
3828
4303
  }
3829
- async encode(text) {
3830
- await this.warmUp();
3831
- if (!this._available || !this._pipe) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
3832
- const bounded = this.bound(text);
3833
- const out = await this._pipe(bounded.text, {
3834
- pooling: this.pooling,
3835
- normalize: this.normalize
3836
- });
3837
- const arr = Array.from(out.data);
3838
- if (arr.length !== this.dim && !this._dimWarned) {
4304
+ /**
4305
+ * Report an input that needed more than one window — once per process, and as an INFO line.
4306
+ *
4307
+ * Not a warning any more: nothing is lost (every window is pooled into the one vector), so the
4308
+ * line is a tuning hint, not a degradation. It carries the actionable number: `chunk_size` is
4309
+ * measured in CHARACTERS and {@link windowChars} converts the model's token window into exactly
4310
+ * that, which is the thing an operator (or a chunker's default) has to change to keep one window
4311
+ * per input. `semantic.max_input_tokens` is deliberately NOT suggested: `resolveWindow` clamps it
4312
+ * to the model's declared window, so raising it cannot buy a single extra token.
4313
+ */
4314
+ reportSplit(count, tokens) {
4315
+ recordTruncation("embedding");
4316
+ if (this._lengthWarned) return;
4317
+ this._lengthWarned = true;
4318
+ retrievalLogger().info(`semantic: input of ~${tokens} tokens exceeds the ${this.window()}-token window of ${this.model} — encoded as ${count} windows and pooled, nothing dropped (one pass per input needs knowledge.chunk_size within ${windowChars(this.window())} characters for this model)`);
4319
+ }
4320
+ /** The configured width's vector: model output padded/truncated, with the one-shot dim warning. */
4321
+ toVector(values) {
4322
+ if (values.length !== this.dim && !this._dimWarned) {
3839
4323
  this._dimWarned = true;
3840
- retrievalLogger().warn(`semantic: model ${this.model} outputs dim=${arr.length} but config semantic.dim=${this.dim} — vectors are truncated/padded; set semantic.dim to ${arr.length} and reindex`);
4324
+ retrievalLogger().warn(`semantic: model ${this.model} outputs dim=${values.length} but config semantic.dim=${this.dim} — vectors are truncated/padded; set semantic.dim to ${values.length} and reindex`);
3841
4325
  }
3842
4326
  const vec = new Float32Array(this.dim);
3843
- for (let i = 0; i < this.dim && i < arr.length; i++) vec[i] = arr[i];
4327
+ for (let i = 0; i < this.dim && i < values.length; i++) vec[i] = values[i];
3844
4328
  return vec;
3845
4329
  }
3846
4330
  /**
4331
+ * ONE vector for several windows: token-weighted mean of the window vectors, re-normalized when the
4332
+ * representation normalizes.
4333
+ *
4334
+ * Weighted rather than a plain mean because the last window is usually short, and a plain mean
4335
+ * would over-represent a fragment. Mean-of-normalized-vectors then re-normalized is the mean
4336
+ * DIRECTION of the windows — the same coordinate space the model's own mean pooling produces, so
4337
+ * `representationKey` (`pooling`/`normalize`/window) does not move and stored vectors stay
4338
+ * comparable with the ones this writes.
4339
+ */
4340
+ pool(vectors, weights) {
4341
+ const width = vectors[0]?.length ?? 0;
4342
+ if (width === 0 || vectors.length === 0) return new Float32Array(this.dim);
4343
+ const total = weights.reduce((sum, w) => sum + Math.max(0, w), 0);
4344
+ const out = new Float64Array(width);
4345
+ for (let i = 0; i < vectors.length; i++) {
4346
+ const weight = total > 0 ? Math.max(0, weights[i] ?? 0) / total : 1 / vectors.length;
4347
+ const vec = vectors[i];
4348
+ for (let d = 0; d < width; d++) out[d] += (vec[d] ?? 0) * weight;
4349
+ }
4350
+ if (this.normalize) {
4351
+ let norm = 0;
4352
+ for (let d = 0; d < width; d++) norm += out[d] * out[d];
4353
+ norm = Math.sqrt(norm);
4354
+ if (norm > 0) for (let d = 0; d < width; d++) out[d] /= norm;
4355
+ }
4356
+ return this.toVector(Array.from(out));
4357
+ }
4358
+ /** Encode the windows of one text in ONE model call, falling back to one call per window. */
4359
+ async encodeWindows(pipe, windows, weights) {
4360
+ const opts = {
4361
+ pooling: this.pooling,
4362
+ normalize: this.normalize
4363
+ };
4364
+ try {
4365
+ const out = await pipe(windows, opts);
4366
+ const rows = out.dims?.[0];
4367
+ const flat = out.data;
4368
+ if (rows === windows.length) {
4369
+ const width = Math.floor(flat.length / rows);
4370
+ if (width > 0) {
4371
+ const vecs = [];
4372
+ for (let i = 0; i < rows; i++) vecs.push(Array.from(flat.slice(i * width, (i + 1) * width), Number));
4373
+ return this.pool(vecs, weights);
4374
+ }
4375
+ }
4376
+ } catch {}
4377
+ const vecs = [];
4378
+ for (const window of windows) {
4379
+ const out = await pipe(window, opts);
4380
+ vecs.push(Array.from(out.data));
4381
+ }
4382
+ return this.pool(vecs, weights);
4383
+ }
4384
+ /** Encode one plan: a single window verbatim, several pooled. */
4385
+ async encodePlan(pipe, plan) {
4386
+ const first = plan.windows[0];
4387
+ if (plan.windows.length <= 1) {
4388
+ const out = await pipe(first ?? "", {
4389
+ pooling: this.pooling,
4390
+ normalize: this.normalize
4391
+ });
4392
+ return this.toVector(Array.from(out.data));
4393
+ }
4394
+ this.reportSplit(plan.windows.length, plan.tokens);
4395
+ return await this.encodeWindows(pipe, plan.windows, plan.windowTokens);
4396
+ }
4397
+ async encode(text) {
4398
+ await this.warmUp();
4399
+ const pipe = this._pipe;
4400
+ if (!this._available || pipe === null) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
4401
+ return await this.encodePlan(pipe, this.windows(text));
4402
+ }
4403
+ /**
3847
4404
  * Encode several texts in ONE model call.
3848
4405
  *
3849
4406
  * This used to be a serial loop over `encode()`, and production code never called it — so
@@ -3856,11 +4413,22 @@ var LocalBgeBackend = class {
3856
4413
  if (texts.length === 0) return [];
3857
4414
  if (texts.length === 1) return [await this.encode(texts[0])];
3858
4415
  await this.warmUp();
3859
- if (!this._available || !this._pipe) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
3860
- const sorted = texts.map((text, index) => ({
3861
- text,
3862
- index
3863
- })).sort((a, b) => a.text.length - b.text.length);
4416
+ const pipe = this._pipe;
4417
+ if (!this._available || pipe === null) throw new Error("local_bge:语义后端不可用(请安装 @huggingface/transformers 并等模型下载完成)");
4418
+ const plans = texts.map((text) => this.windows(text));
4419
+ const singles = [];
4420
+ const longs = [];
4421
+ plans.forEach((plan, index) => {
4422
+ if (plan.windows.length <= 1) singles.push({
4423
+ text: plan.windows[0] ?? texts[index],
4424
+ index
4425
+ });
4426
+ else longs.push({
4427
+ plan,
4428
+ index
4429
+ });
4430
+ });
4431
+ const sorted = singles.sort((a, b) => a.text.length - b.text.length);
3864
4432
  const result = new Array(texts.length);
3865
4433
  for (let i = 0; i < sorted.length; i += BATCH_WINDOW) {
3866
4434
  const group = sorted.slice(i, i + BATCH_WINDOW);
@@ -3869,15 +4437,15 @@ var LocalBgeBackend = class {
3869
4437
  result[g.index] = vecs[j];
3870
4438
  });
3871
4439
  }
4440
+ for (const { plan, index } of longs) result[index] = await this.encodePlan(pipe, plan);
3872
4441
  return result;
3873
4442
  }
3874
4443
  /** One model call for a length-homogeneous group; falls back to per-text calls on failure. */
3875
4444
  async encodeGroup(texts) {
3876
4445
  const pipe = this._pipe;
3877
4446
  if (pipe === null) throw new Error("local_bge:语义后端不可用");
3878
- const bounded = texts.map((t) => this.bound(t).text);
3879
4447
  try {
3880
- const out = await pipe(bounded, {
4448
+ const out = await pipe(texts, {
3881
4449
  pooling: this.pooling,
3882
4450
  normalize: this.normalize
3883
4451
  });
@@ -3904,12 +4472,32 @@ var LocalBgeBackend = class {
3904
4472
  }
3905
4473
  };
3906
4474
  async function createFeatureExtraction(model, env) {
3907
- const mod = await import("@huggingface/transformers");
4475
+ const mod = await loadTransformers(env.moduleDir);
3908
4476
  if (!mod.pipeline) throw new Error("transformers.js 的 pipeline 不可用");
3909
4477
  applyModelEnv(mod, env);
3910
4478
  return await mod.pipeline("feature-extraction", model);
3911
4479
  }
3912
4480
  /**
4481
+ * Load transformers.js from the shared runtime-dependency root, else from this tree.
4482
+ *
4483
+ * The shared root is the base provider's content-addressed install (`<home>/deps/runtime/<key>`), and
4484
+ * its `package.json` is the anchor `createRequire` needs: resolving `@huggingface/transformers` from
4485
+ * there picks the copy the provider verified, together with its transitive `onnxruntime-node` — no
4486
+ * second copy, and no dependence on this bundle's own node_modules. The import is by file URL because
4487
+ * the resolved entry is an ESM file inside the root, not a bare specifier this module could resolve.
4488
+ *
4489
+ * A root that no longer resolves (GC, damaged tree, a package whose entry moved) is a WARNING and the
4490
+ * pre-existing optional resolution: a broken self-heal must not be worse than no self-heal.
4491
+ */
4492
+ async function loadTransformers(moduleDir) {
4493
+ if (moduleDir !== void 0 && moduleDir.trim() !== "") try {
4494
+ return await import(pathToFileURL(createRequire(join(moduleDir, "package.json")).resolve("@huggingface/transformers")).href);
4495
+ } catch (error) {
4496
+ retrievalLogger().warn(`semantic: could not load @huggingface/transformers from the shared runtime root ${moduleDir} (${describeError$1(error)}); falling back to this tree's optional dependency`);
4497
+ }
4498
+ return await import("@huggingface/transformers");
4499
+ }
4500
+ /**
3913
4501
  * Apply this project's cache/mirror policy to transformers.js.
3914
4502
  *
3915
4503
  * `mod.env` is PROCESS-GLOBAL and last-writer-wins. Setting the keys from a single place is what
@@ -4007,6 +4595,60 @@ function dot(a, b) {
4007
4595
  //#region ../retrieval-core/lib/adapters/hnswlib.js
4008
4596
  const require = createRequire(import.meta.url);
4009
4597
  /**
4598
+ * The shared module root `hnswlib-node` should be loaded from, when the caller provisioned one.
4599
+ *
4600
+ * `undefined` (every CLI/MCP/direct caller) = this tree's own optional dependency. The DSH plugin
4601
+ * sets it from the native self-heal item's `handle.dir` once that item is ready; a root that no
4602
+ * longer resolves falls back to this tree, which is the pre-existing behaviour and must never be
4603
+ * worse than no self-heal.
4604
+ */
4605
+ let moduleRoot;
4606
+ /**
4607
+ * The loaded binding, memoized (`undefined` = not attempted, `null` = the last attempt failed).
4608
+ *
4609
+ * Native addons are expensive to `dlopen` and there is exactly one per process; memoizing also makes
4610
+ * every store in the process shares one load. The cache follows the ROOT (see
4611
+ * {@link useHnswlibModuleDir}), because a root that arrives later is the one event that can turn a
4612
+ * failed load into a working index.
4613
+ */
4614
+ let loaded;
4615
+ /**
4616
+ * Point the loader at a shared module root (the base `runtime-deps` handle's directory).
4617
+ *
4618
+ * The cache follows the ROOT: a change forgets what was loaded, because the next `require` must come
4619
+ * from the new root (that is the whole point of setting one). It costs nothing in practice — Node's
4620
+ * own `require` cache makes re-entering the same file cheap, and a live native index is untouched
4621
+ * (see `HnswlibVectorStore.useModuleDir`, which only rebuilds when it has no native index at all).
4622
+ */
4623
+ function useHnswlibModuleDir(dir) {
4624
+ const next = dir === "" ? void 0 : dir;
4625
+ if (next === moduleRoot) return;
4626
+ moduleRoot = next;
4627
+ loaded = void 0;
4628
+ }
4629
+ /** Does this module actually carry the constructor this adapter needs? */
4630
+ function usable(mod) {
4631
+ return typeof mod?.HierarchicalNSW === "function";
4632
+ }
4633
+ /** `require('hnswlib-node')` from the shared root, else from this tree; `null` when neither works. */
4634
+ function loadHnswlib() {
4635
+ if (loaded !== void 0) return loaded;
4636
+ let mod = null;
4637
+ if (moduleRoot !== void 0) try {
4638
+ const entry = createRequire(join(moduleRoot, "package.json")).resolve("hnswlib-node");
4639
+ const candidate = require(entry);
4640
+ if (usable(candidate)) mod = candidate;
4641
+ } catch {}
4642
+ if (mod === null) try {
4643
+ const candidate = require("hnswlib-node");
4644
+ mod = usable(candidate) ? candidate : null;
4645
+ } catch {
4646
+ mod = null;
4647
+ }
4648
+ loaded = mod;
4649
+ return mod;
4650
+ }
4651
+ /**
4010
4652
  * When to COMPACT tombstones away, as a fraction of the live count.
4011
4653
  *
4012
4654
  * Deleted points stay in the graph (excluded from results, but still traversed), so they cost
@@ -4065,15 +4707,41 @@ var HnswlibVectorStore = class {
4065
4707
  this.dim = dim;
4066
4708
  this.efSearch = Math.max(16, Math.floor(efSearch));
4067
4709
  this.fallback = new LocalNumpyVectorStore(dim);
4710
+ this.lib = this.openNative(Math.max(1024, dim * 2));
4711
+ }
4712
+ /**
4713
+ * Build a native index at `max` capacity, or `null` when the binding cannot be loaded.
4714
+ *
4715
+ * One place for "make an index": the constructor, `reindex()` and `rebuild()` each used to spell
4716
+ * the `require` out themselves, which is how a module root set after construction could never
4717
+ * reach them. `null` is the caller's signal to serve from {@link fallback} — a missing binding
4718
+ * degrades, it never throws.
4719
+ */
4720
+ openNative(max) {
4721
+ const mod = loadHnswlib();
4722
+ if (mod === null) return null;
4068
4723
  try {
4069
- const { HierarchicalNSW } = require("hnswlib-node");
4070
- const lib = new HierarchicalNSW("cosine", dim);
4071
- lib.initIndex(Math.max(1024, dim * 2), 16, 100);
4072
- this.lib = lib;
4724
+ const lib = new mod.HierarchicalNSW("cosine", this.dim);
4725
+ lib.initIndex(Math.max(1024, max), 16, 100);
4726
+ return lib;
4073
4727
  } catch {
4074
- this.lib = null;
4728
+ return null;
4075
4729
  }
4076
4730
  }
4731
+ /**
4732
+ * Point this store's native side at a shared module root and, when the binding was unavailable
4733
+ * until now, rebuild the index from the tracked vectors under it.
4734
+ *
4735
+ * This is the case the whole hook exists for: `mem` builds its vector store during mount, while the
4736
+ * native self-heal item is still installing, so the constructor may have fallen back. When the item
4737
+ * settles, the plugin calls this — and a store that already serves natively is left alone (the
4738
+ * binding is the same one).
4739
+ */
4740
+ useModuleDir(dir) {
4741
+ useHnswlibModuleDir(dir);
4742
+ if (this.lib !== null) return;
4743
+ this.rebuild(this.liveRows());
4744
+ }
4077
4745
  add(id, vec) {
4078
4746
  if (vec.length !== this.dim) throw new Error(`向量维度不匹配:得到 ${vec.length},需要 ${this.dim}`);
4079
4747
  if (this.lib) try {
@@ -4229,10 +4897,8 @@ var HnswlibVectorStore = class {
4229
4897
  /** Recreate the serving index (native when possible) from the tracked vectors. */
4230
4898
  reindex() {
4231
4899
  const stored = [...this.vectors.entries()];
4232
- if (this.lib) try {
4233
- const { HierarchicalNSW } = require("hnswlib-node");
4234
- const lib = new HierarchicalNSW("cosine", this.dim);
4235
- lib.initIndex(Math.max(1024, (stored.length || 1) * 2), 16, 100);
4900
+ const lib = this.openNative(Math.max(1024, (stored.length || 1) * 2));
4901
+ if (lib !== null) {
4236
4902
  this.lib = lib;
4237
4903
  this.efOn = null;
4238
4904
  for (const [i, v] of stored) this.lib.addPoint(Array.from(v), i, false);
@@ -4240,9 +4906,8 @@ var HnswlibVectorStore = class {
4240
4906
  this.rebuilds += 1;
4241
4907
  this.persist();
4242
4908
  return;
4243
- } catch {
4244
- this.lib = null;
4245
4909
  }
4910
+ this.lib = null;
4246
4911
  this.fallback = new LocalNumpyVectorStore(this.dim);
4247
4912
  for (const [i, v] of stored) this.fallback.add(i, v);
4248
4913
  this.graphElements = stored.length;
@@ -4268,15 +4933,8 @@ var HnswlibVectorStore = class {
4268
4933
  for (const r of list) this.vectors.set(r.id, r.vec);
4269
4934
  return;
4270
4935
  }
4271
- if (this.lib) try {
4272
- const { HierarchicalNSW } = require("hnswlib-node");
4273
- const lib = new HierarchicalNSW("cosine", this.dim);
4274
- lib.initIndex(Math.max(1024, (list.length || 1) * 2), 16, 100);
4275
- this.lib = lib;
4276
- this.efOn = null;
4277
- } catch {
4278
- this.lib = null;
4279
- }
4936
+ this.lib = this.openNative(Math.max(1024, (list.length || 1) * 2));
4937
+ if (this.lib !== null) this.efOn = null;
4280
4938
  for (const r of list) this.add(r.id, r.vec);
4281
4939
  this.rebuilds += 1;
4282
4940
  this.persist();
@@ -4454,6 +5112,19 @@ var AutoVectorStore = class {
4454
5112
  this.pendingPersistence = path;
4455
5113
  this.delegate.attachPersistence?.(path);
4456
5114
  }
5115
+ /**
5116
+ * Point the ANN side at a shared module root (the plugin's native self-heal item) — for THIS store
5117
+ * when it already migrated, and for the one it may migrate to later.
5118
+ *
5119
+ * The delegate call covers the first case; the module-level root (set by
5120
+ * `HnswlibVectorStore.useModuleDir` / `useHnswlibModuleDir`) is what a FUTURE
5121
+ * `new HnswlibVectorStore` in `maybeUpgrade` reads. Below the threshold nothing changes: an
5122
+ * unmigrated store keeps serving from brute force, which is the intended backend at that size.
5123
+ */
5124
+ useModuleDir(dir) {
5125
+ if (this.delegate.useModuleDir !== void 0) this.delegate.useModuleDir(dir);
5126
+ else useHnswlibModuleDir(dir);
5127
+ }
4457
5128
  flush() {
4458
5129
  this.delegate.flush?.();
4459
5130
  }
@@ -9795,7 +10466,7 @@ function pathRecency(path) {
9795
10466
  for (const edge of path.edges) newest = Math.max(newest, edgeTime(edge));
9796
10467
  return newest;
9797
10468
  }
9798
- /** Dedup key of a path: the ordered edge ids (§5 "路径去重按 (edge ids)"). */
10469
+ /** Dedup key of a path: the ordered edge ids ("路径去重按 (edge ids)"). */
9799
10470
  function pathKey(path) {
9800
10471
  return path.edges.map((edge) => edge.id).join("\0");
9801
10472
  }
@@ -16194,6 +16865,36 @@ var MemoryStore = class {
16194
16865
  await (this.semantic.warmUp?.() ?? Promise.resolve());
16195
16866
  return this.semantic.isAvailable();
16196
16867
  }
16868
+ /**
16869
+ * Point the semantic backend at a shared runtime-dependency root (the base provider's
16870
+ * `runtime-deps` handle) before it warms. Best-effort by contract: a backend without external
16871
+ * module resolution ignores it, and a missing root is reported by the backend itself.
16872
+ */
16873
+ useSemanticModuleDir(dir) {
16874
+ this.semantic.useModuleDir?.(dir);
16875
+ }
16876
+ /**
16877
+ * Point the vector store at a shared runtime-dependency root for its NATIVE binding
16878
+ * (`hnswlib-node`), after the DSH plugin's native self-heal item is ready.
16879
+ *
16880
+ * Same best-effort contract as {@link useSemanticModuleDir}: a store with no native binding (the
16881
+ * brute-force backend, or an `auto` store still on it) ignores it, and a store that already fell
16882
+ * back to brute force rebuilds its tracked vectors under the new root. Nothing here decides whether
16883
+ * a root should exist — the caller's dependency audit does.
16884
+ */
16885
+ useNativeModuleDir(dir) {
16886
+ this.vstore.useModuleDir?.(dir);
16887
+ }
16888
+ /**
16889
+ * Vectors currently in the LIVE index (sync, one map read).
16890
+ *
16891
+ * Exposed for the plugin's dependency audit: an `hnswlib-node` notice is conditioned on the corpus
16892
+ * actually being large enough for an ANN index to matter (`min_vectors`), and a session line that
16893
+ * asks "how big is the store right now" must not run a database query to answer.
16894
+ */
16895
+ liveVectorCount() {
16896
+ return this.vstore.count();
16897
+ }
16197
16898
  /** List contradiction candidates (`resolved` 0 = open, 1 = closed with a recorded verdict). */
16198
16899
  async listContradictions(opts) {
16199
16900
  return await this.contradictions.list(opts?.resolved ?? 0, opts?.kind, opts?.threshold, opts?.limit ?? 10);
@@ -16632,7 +17333,10 @@ var MemoryStore = class {
16632
17333
  * The exact, facts and relation passes reinforce the facts they return, exactly like the fuzzy
16633
17334
  * pass does for its hits: all are recalls the caller actually reads. A caller that must stay
16634
17335
  * read-only passes `track: false` (the conditional hint's probe does). Exact, facts, relation and
16635
- * time also honour the caller's output budget (their text is the caller's to bound).
17336
+ * time also honour the caller's output budget (their text is the caller's to bound). The one
17337
+ * difference is the DEFAULT: a time window applies no budget unless the caller names one — its
17338
+ * page size (`limit`) is what bounds the list, and the budget would only rewrite rows the caller
17339
+ * asked for (see {@link timePage}).
16636
17340
  *
16637
17341
  * It does NOT emit a retrieval-health event. Those counters describe the retrieval PIPELINE
16638
17342
  * (legs, fusion, semantic-live rate); none of an exact, facts, relation or time answer ran a leg,
@@ -16678,32 +17382,46 @@ var MemoryStore = class {
16678
17382
  * happened that day" reads in the order it happened; `score` is the constant 1 exactly like the
16679
17383
  * exact page's, because there is no relevance number here for the model to compare.
16680
17384
  *
16681
- * It honours `kind`/`source`, pages with the same `offset`/`limit` (default 10, cap 50) and the
16682
- * same `max_tokens` budget as every other recall, and reinforces the rows it returns unless
16683
- * `track: false` — a window the caller reads is a recall like any other.
17385
+ * The PAGE is the contract's, not the caller's guess: `limit` omitted means one page of
17386
+ * {@link TIME_DEFAULT_LIMIT} rows (the same 50 the contract caps an explicit page at), and an
17387
+ * explicit `limit` (1-50) still wins. A window that holds more than one page is the normal case
17388
+ * for a busy day, so the envelope does not leave the caller to infer the shortfall: it reports
17389
+ * `remaining = total - (offset + hits.length)`, the count still unseen.
17390
+ *
17391
+ * The output budget is honoured only when the caller names it (`max_tokens`). Falling back to the
17392
+ * configured `retrieval.max_output_tokens` here would rewrite the text of a window's rows behind a
17393
+ * page the caller already bounded with `limit` — the page size is the knob for "how much of the
17394
+ * list", and the budget stays a knob for a RANKED result (exact/facts/relation/hybrid unchanged).
17395
+ * `max_tokens` (including the contract's `0` = unlimited) is passed through unchanged.
17396
+ *
17397
+ * It honours `kind`/`source`, and reinforces the rows it returns unless `track: false` — a window
17398
+ * the caller reads is a recall like any other.
16684
17399
  */
16685
17400
  async timePage(input) {
16686
17401
  if (input.time === void 0) throw new Error("mem_recall.search 需要 `query`,或只给 `time`(纯时间召回);两者都没有时无法检索。");
16687
17402
  const time = normalizeTimeFilter(input.time);
16688
- const { offset, limit } = exactPage(input.offset, input.limit);
17403
+ const page = exactPage(input.offset, input.limit);
16689
17404
  const filters = {
16690
17405
  time,
16691
17406
  ...input.kind === void 0 ? {} : { kind: input.kind },
16692
17407
  ...input.source === void 0 ? {} : { source: input.source }
16693
17408
  };
16694
17409
  const total = await this.searchIndex.countInTimeWindow(filters);
17410
+ const limit = input.limit === void 0 ? 50 : page.limit;
17411
+ const offset = page.offset;
16695
17412
  const ids = total === 0 ? [] : await this.searchIndex.timeWindowRows({
16696
17413
  ...filters,
16697
17414
  limit,
16698
17415
  offset
16699
17416
  });
16700
- const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens);
17417
+ const { kept } = applyOutputBudget(this.config, await this.factHits(ids, () => 1), input.maxTokens ?? 0);
16701
17418
  if (input.track !== false) await this.reinforce(kept.map((hit) => hit.ref_id));
16702
17419
  return {
16703
17420
  mode: "time",
16704
17421
  total,
16705
17422
  offset,
16706
17423
  limit,
17424
+ remaining: Math.max(0, total - (offset + kept.length)),
16707
17425
  truncated: offset + kept.length < total,
16708
17426
  hits: kept
16709
17427
  };
@@ -19254,7 +19972,7 @@ const VERSION_PACKAGES = [
19254
19972
  /** This plugin's npm package id, which names the probe and verification keys. */
19255
19973
  const PACKAGE_ID = String(hostContribution.package ?? "@avantf/dsh-mem");
19256
19974
  /** Best-effort one-line reason for a caught value. */
19257
- function reasonOf(error) {
19975
+ function reasonOf$2(error) {
19258
19976
  return error instanceof Error ? error.message : String(error);
19259
19977
  }
19260
19978
  /** Emit a warning without ever letting the logger turn it into a crash. */
@@ -19294,7 +20012,7 @@ function runtimeFromCompat(module) {
19294
20012
  }
19295
20013
  /** The "the check itself failed" outcome: keep loading, say why. */
19296
20014
  function cannotDetermine(error, log, prefix) {
19297
- const message = `${prefix} WARNING — the compatibility check itself failed (${reasonOf(error)}); loading anyway`;
20015
+ const message = `${prefix} WARNING — the compatibility check itself failed (${reasonOf$2(error)}); loading anyway`;
19298
20016
  warn(log, message);
19299
20017
  return { verdict: {
19300
20018
  load: true,
@@ -19378,6 +20096,290 @@ function verifyRegisteredFaces(input) {
19378
20096
  toolNames: input.toolNames
19379
20097
  });
19380
20098
  }
20099
+ //#endregion
20100
+ //#region lib/types/deps_audit.js
20101
+ /**
20102
+ * The STARTUP DEPENDENCY AUDIT: one cached, synchronous answer per CONFIGURED entry to "can this
20103
+ * process actually load that piece right now?", recorded at the end of the mount and refreshed when a
20104
+ * self-heal item settles.
20105
+ *
20106
+ * WHY IT EXISTS. A session notice is the last step of the fallback chain
20107
+ * `npm optional → self-heal → notice`, and the decision "is this really missing?" cannot be made on
20108
+ * the prompt path: `pre-step` runs on every user message, and resolving a package there would be a
20109
+ * filesystem hit (and, for a native addon, a `dlopen`) per step. So the expensive half happens ONCE,
20110
+ * off the mount path, and `pre-step` only consumes the registry, whose predicates read this snapshot.
20111
+ *
20112
+ * CONFIG-DRIVEN, NOT PACKAGE-DRIVEN. This module knows two probe KINDS and nothing about which
20113
+ * packages exist:
20114
+ *
20115
+ * - {@link moduleProbe} — resolve AND load one specifier (a real `require`/`import`, so a native
20116
+ * addon's binding is loaded rather than merely found), trying the ready shared roots first and
20117
+ * this tree last;
20118
+ * - {@link itemProbe} — the state of one runtime-deps item, through a caller-supplied lookup (the
20119
+ * provisioner's own `resolve`, with the "declared but not settled" gate on top).
20120
+ *
20121
+ * Which entries exist, how loud each one is, and what it says come from the plugin's SHIPPED
20122
+ * `res/runtime-deps.yaml` (`RuntimeDepsResourceSchema` in the contract). No package name, sentence or
20123
+ * severity is written here.
20124
+ *
20125
+ * WHAT IT IS NOT:
20126
+ *
20127
+ * - **Not a retry loop.** `run()` at startup, `run()` again when a self-heal item settles. A package
20128
+ * installed by hand afterwards is picked up on the next restart, not polled for.
20129
+ * - **Not throwing.** It runs beside a live host: a probe that fails is a recorded state plus a log
20130
+ * line, never an exception out of the mount.
20131
+ * - **Not a second source of truth about an item.** `itemProbe` ASKS the caller; the states here are
20132
+ * only a cache of that answer, refreshed on every settlement.
20133
+ */
20134
+ /** The states this build understands (a JS caller can hand over anything). */
20135
+ const STATES = [
20136
+ "available",
20137
+ "failed",
20138
+ "missing",
20139
+ "skipped",
20140
+ "pending",
20141
+ "unknown"
20142
+ ];
20143
+ /** Reason text for a thrown value, without trusting it. */
20144
+ function reasonOf$1(error) {
20145
+ try {
20146
+ if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
20147
+ return String(error);
20148
+ } catch {
20149
+ return "unknown";
20150
+ }
20151
+ }
20152
+ /**
20153
+ * Build the audit.
20154
+ *
20155
+ * Every configured entry starts `unknown`; nothing is assumed about an entry that has not been
20156
+ * checked. The caller runs it once (fire-and-forget) and again for the entries a settlement could
20157
+ * change — that ordering is what makes a session line appear only after there is an observation.
20158
+ */
20159
+ function createDependencyAudit(options) {
20160
+ const states = /* @__PURE__ */ new Map();
20161
+ const pending = /* @__PURE__ */ new Map();
20162
+ const probes = /* @__PURE__ */ new Map();
20163
+ for (const probe of options.probes) {
20164
+ probes.set(probe.id, probe);
20165
+ states.set(probe.id, "unknown");
20166
+ }
20167
+ const warn = (message) => {
20168
+ try {
20169
+ options.logger?.warn(message);
20170
+ } catch {}
20171
+ };
20172
+ const info = (message) => {
20173
+ try {
20174
+ options.logger?.info(message);
20175
+ } catch {}
20176
+ };
20177
+ const runId = (id) => {
20178
+ const running = pending.get(id);
20179
+ if (running !== void 0) return running;
20180
+ const probe = probes.get(id);
20181
+ if (probe === void 0) return Promise.resolve();
20182
+ const task = (async () => {
20183
+ let next;
20184
+ try {
20185
+ next = await probe.check();
20186
+ } catch (error) {
20187
+ warn(`deps audit: probing ${id} failed (${reasonOf$1(error)}); its state stays unknown`);
20188
+ next = "unknown";
20189
+ }
20190
+ if (!STATES.includes(next)) next = "unknown";
20191
+ const before = states.get(id);
20192
+ states.set(id, next);
20193
+ if (before !== next) info(`deps audit: ${id} is ${next}`);
20194
+ })().finally(() => {
20195
+ pending.delete(id);
20196
+ });
20197
+ pending.set(id, task);
20198
+ return task;
20199
+ };
20200
+ return {
20201
+ state(id) {
20202
+ return states.get(id) ?? "unknown";
20203
+ },
20204
+ vectors() {
20205
+ try {
20206
+ return options.vectors();
20207
+ } catch {
20208
+ return 0;
20209
+ }
20210
+ },
20211
+ async run() {
20212
+ await Promise.all([...probes.keys()].map((id) => runId(id)));
20213
+ },
20214
+ runId
20215
+ };
20216
+ }
20217
+ /**
20218
+ * Resolve and LOAD one specifier, from the first root that can serve it and then from this tree.
20219
+ *
20220
+ * The `createRequire(<root>/package.json)` anchor is the same rule the semantic adapter uses: a
20221
+ * shared runtime-deps root's `package.json` is what makes `resolve` look in THAT root's
20222
+ * `node_modules`, so the audit observes the copy the runtime would load. The import is by FILE URL
20223
+ * because the resolved entry is inside the root, not a bare specifier this module could resolve.
20224
+ *
20225
+ * The three answers are deliberately distinct, because they lead to different repairs:
20226
+ *
20227
+ * - `available` — it loaded;
20228
+ * - `failed` — something RESOLVED but loading it threw (the node-gyp build that exists and does not
20229
+ * work: the case a bare `resolve()` check would report as healthy);
20230
+ * - `missing` — nothing resolved anywhere.
20231
+ *
20232
+ * A root that cannot serve the specifier is not an error: the next candidate is tried, which is what
20233
+ * makes a garbage-collected store no worse than no self-heal. Never throws.
20234
+ */
20235
+ async function moduleProbe(id, specifier, roots) {
20236
+ const candidates = [];
20237
+ for (const root of roots()) {
20238
+ if (root === "") continue;
20239
+ try {
20240
+ candidates.push(createRequire(join(root, "package.json")).resolve(specifier));
20241
+ } catch {}
20242
+ }
20243
+ try {
20244
+ candidates.push(createRequire(import.meta.url).resolve(specifier));
20245
+ } catch {}
20246
+ if (candidates.length === 0) return "missing";
20247
+ let resolvedButBroken = false;
20248
+ for (const entry of candidates) try {
20249
+ await import(pathToFileURL(entry).href);
20250
+ return "available";
20251
+ } catch {
20252
+ resolvedButBroken = true;
20253
+ }
20254
+ return resolvedButBroken ? "failed" : "missing";
20255
+ }
20256
+ /**
20257
+ * Observe one runtime-deps ITEM's state through the caller's own lookup.
20258
+ *
20259
+ * The caller owns the mapping (its provisioner's `resolve` plus the "declared but not settled" gate),
20260
+ * so this probe adds nothing but a defensive boundary: a throwing lookup is `unknown`, not a failure
20261
+ * claim. `pending` is passed through because it is a MEANINGFUL state — an install in flight — and the
20262
+ * notice predicate treats it as "not reportable yet".
20263
+ */
20264
+ function itemProbe(id, itemId, stateOf) {
20265
+ return {
20266
+ id,
20267
+ check: async () => {
20268
+ try {
20269
+ return stateOf(itemId);
20270
+ } catch {
20271
+ return "unknown";
20272
+ }
20273
+ }
20274
+ };
20275
+ }
20276
+ //#endregion
20277
+ //#region lib/types/runtime_deps_resource.js
20278
+ /**
20279
+ * The plugin's OWN dependency declaration: `res/runtime-deps.yaml`, read from the INSTALL directory.
20280
+ *
20281
+ * WHY A SHIPPED RESOURCE. The package set, the sources, the attempts, the verify list and the audit
20282
+ * sentences describe THIS BUILD — which module the local backend imports, which native bindings this
20283
+ * release looks for, what a session should be told when one of them is missing. Keeping them in a
20284
+ * user's `configs/common.yaml` made every deployment responsible for a list only the package can know
20285
+ * (and made a stale copy install a module no code loads). The operator keeps the one real policy
20286
+ * decision — the two `enabled` switches — and the DATA travels with the code that depends on it.
20287
+ *
20288
+ * WHERE IT IS READ FROM. `<package>/res/runtime-deps.yaml`, located from `import.meta.url`:
20289
+ * `lib/index.js` (bundled) and `src/*.ts` (tests, dev) are both one level below the package root, so
20290
+ * the same relative URL resolves to the SAME file in the checkout and in an installed tarball. It is
20291
+ * deliberately NOT resolved through `node_modules` lookups, `~/.avantf`, or the cwd: a host may mount
20292
+ * the plugin from a linked checkout, a global install or a packed tarball, and all three must read
20293
+ * their own copy.
20294
+ *
20295
+ * FAILURE IS EXPLICIT AND NON-FATAL. A missing/unreadable/malformed file is a PACKAGING bug, not an
20296
+ * environment one, so the answer is a structured failure with the path and the reason — never a
20297
+ * throw out of the mount and never a silent "no dependencies declared". The caller logs it as an
20298
+ * ERROR and degrades (no self-heal item, no audit): the plugin keeps its tools, service, prompt and
20299
+ * Remote face, and the runtime stays on its optional-dependency resolution.
20300
+ *
20301
+ * The base never sees this file: `envinit` maps the validated object onto the provider's own
20302
+ * `RuntimeDepsSpec` field by field (see `runtimeDepsSpecOf`).
20303
+ */
20304
+ /**
20305
+ * `res/runtime-deps.yaml` next to the package that owns this module.
20306
+ *
20307
+ * `import.meta.url` is the anchor (not `process.cwd()`, not a config path): the file ships beside the
20308
+ * code, and an installed plugin must read the copy its own package manager put there.
20309
+ */
20310
+ function runtimeDepsResourceUrl() {
20311
+ return new URL("../res/runtime-deps.yaml", import.meta.url);
20312
+ }
20313
+ /** Reason text for a thrown value, without trusting it. */
20314
+ function reasonOf(error) {
20315
+ try {
20316
+ if (error instanceof Error && typeof error.message === "string" && error.message !== "") return error.message;
20317
+ return String(error);
20318
+ } catch {
20319
+ return "unknown";
20320
+ }
20321
+ }
20322
+ /**
20323
+ * Read and validate the shipped declaration.
20324
+ *
20325
+ * Synchronous on purpose: it is a few kilobytes next to the entry module, read once at mount, and the
20326
+ * caller's plan (the item specs and the audit entries) has to exist before anything is declared.
20327
+ *
20328
+ * `url` is injectable so a test can point at a scratch install directory; production always uses
20329
+ * {@link runtimeDepsResourceUrl}.
20330
+ */
20331
+ function loadRuntimeDepsResource(url = runtimeDepsResourceUrl()) {
20332
+ let path;
20333
+ try {
20334
+ path = fileURLToPath(url);
20335
+ } catch (error) {
20336
+ return {
20337
+ ok: false,
20338
+ path: String(url),
20339
+ reason: `不是可读的文件 URL(${reasonOf(error)})`
20340
+ };
20341
+ }
20342
+ let text;
20343
+ try {
20344
+ text = readFileSync(path, "utf8");
20345
+ } catch (error) {
20346
+ return {
20347
+ ok: false,
20348
+ path,
20349
+ reason: `无法读取(${reasonOf(error)})`
20350
+ };
20351
+ }
20352
+ let raw;
20353
+ try {
20354
+ raw = parse(text);
20355
+ } catch (error) {
20356
+ return {
20357
+ ok: false,
20358
+ path,
20359
+ reason: `YAML 解析失败(${reasonOf(error)})`
20360
+ };
20361
+ }
20362
+ if (raw === null || raw === void 0) return {
20363
+ ok: false,
20364
+ path,
20365
+ reason: "文件是空的(没有任何声明)"
20366
+ };
20367
+ const parsed = RuntimeDepsResourceSchema.safeParse(raw);
20368
+ if (!parsed.success) {
20369
+ const issue = parsed.error.issues[0];
20370
+ const where = issue === void 0 ? "" : `${issue.path.join(".") === "" ? "" : `${issue.path.join(".")}: `}`;
20371
+ return {
20372
+ ok: false,
20373
+ path,
20374
+ reason: `声明不符合 RuntimeDepsResourceSchema(${where}${issue?.message ?? "invalid"})`
20375
+ };
20376
+ }
20377
+ return {
20378
+ ok: true,
20379
+ path,
20380
+ resource: parsed.data
20381
+ };
20382
+ }
19381
20383
  const PACKAGE = "@avantf/dsh-plugin-base";
19382
20384
  function parseVersion(input) {
19383
20385
  const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(input.trim());
@@ -19761,6 +20763,25 @@ function rangeOf(value) {
19761
20763
  * models elsewhere (`AVANTF_MEM_MODEL_CACHE`) — that would install a second, never-read copy.
19762
20764
  * A model that cannot be fetched is a WARNING: the semantic path degrades to FTS+entity, it
19763
20765
  * never refuses the mount.
20766
+ * 3. **`mem:semantic-deps`** (`runtime-deps`, `startup: 'background'`) — optional: repair a missing
20767
+ * `@huggingface/transformers` install through the base's shared runtime-dependency store. It is
20768
+ * OFF unless `semantic.self_heal.enabled` says otherwise (that is a network decision, not a
20769
+ * startup default), needs base >= 0.7.0 / interface v6 (the `createRuntimeDeps` helper), and is
20770
+ * declared against the SAME content-addressed root any other family plugin would reuse. The
20771
+ * item, the shared root handle and the operator's configured session notices all come from that
20772
+ * one handle. The handle directory is handed back to the caller, which points the embedding
20773
+ * backend at it before the semantic warm.
20774
+ * 4. **`mem:native-deps`** (`runtime-deps`, `startup: 'background'`) — optional: repair the NATIVE
20775
+ * optional dependencies (`nodejieba`, `hnswlib-node`) when `semantic.native_self_heal.enabled`
20776
+ * is on. A SEPARATE item on purpose: a different package set is a different content key, so the
20777
+ * base's store gives it its own immutable root instead of mixing two node-gyp builds into the
20778
+ * transformers install. The handle directory is handed back to the caller, which points the
20779
+ * tokenizer and the vector store's ANN binding at it.
20780
+ *
20781
+ * Sessions are told about a missing package by NOTICES, and a notice is judged on two observations
20782
+ * that this module neither owns alone: the item's live state (below) and the startup dependency
20783
+ * audit's cached answer (`deps_audit.ts`, supplied in the plan). A repair that is still installing
20784
+ * matches nothing, and a failed repair whose package is loadable stays silent.
19764
20785
  *
19765
20786
  * Two hard rules:
19766
20787
  *
@@ -19783,6 +20804,26 @@ const PLUGIN = "mem";
19783
20804
  */
19784
20805
  const MODEL_ITEM = "mem:model";
19785
20806
  /**
20807
+ * The shared runtime-dependency item: `@huggingface/transformers` repaired through the base's
20808
+ * content-addressed store when `semantic.self_heal.enabled` is on. Same root as every other family
20809
+ * plugin's item for the same package set.
20810
+ */
20811
+ const SEMANTIC_DEPS_ITEM = "mem:semantic-deps";
20812
+ /**
20813
+ * The NATIVE runtime-dependency item: `nodejieba` + `hnswlib-node`, repaired through the same base
20814
+ * mechanism when `semantic.native_self_heal.enabled` is on.
20815
+ *
20816
+ * A SEPARATE item, not a third package in {@link SEMANTIC_DEPS_ITEM}, for two reasons that both
20817
+ * matter:
20818
+ *
20819
+ * - the store is content-addressed, so this spec's package set (two node-gyp builds) keys a
20820
+ * DIFFERENT immutable root under `<home>/deps/runtime/` — these packages are never installed into
20821
+ * the transformers root, and vice versa;
20822
+ * - the two have independent failure modes and independent consequences (semantic leg vs.
20823
+ * entity/jaccard legs vs. ANN index), so one install failing must not report the other as broken.
20824
+ */
20825
+ const NATIVE_DEPS_ITEM = "mem:native-deps";
20826
+ /**
19786
20827
  * The default ONNX embedding repository — the one `semantic.local_model` defaults to and the one
19787
20828
  * {@link MODEL_FILES} describes. A DIFFERENT repo is a different file set, which is why the item
19788
20829
  * omits `spec.files` for it (see {@link managedModelSpec}).
@@ -19824,6 +20865,23 @@ const DEADLINE_MS = 15e3;
19824
20865
  * rather than ship the inversion.
19825
20866
  */
19826
20867
  const MODEL_REQUEST_TIMEOUT_MS = 3e5;
20868
+ /**
20869
+ * Per-attempt budget for the `mem:semantic-deps` install. A package-manager install of
20870
+ * transformers.js + onnxruntime-node is tens of MB; the base's 30 s network default would time out on
20871
+ * a slow link and report a failure the next attempt could have avoided. The item is `background`, so
20872
+ * this budget spends none of the startup allowance; it only bounds one attempt.
20873
+ */
20874
+ const SEMANTIC_DEPS_ATTEMPT_TIMEOUT_MS = 6e5;
20875
+ /**
20876
+ * Per-attempt budget for the `mem:native-deps` install.
20877
+ *
20878
+ * Longer than the semantic one on purpose: `nodejieba` and `hnswlib-node` are node-gyp packages, so
20879
+ * the attempt includes downloading a toolchain-compatible source tree and COMPILING it — measured
20880
+ * minutes on a cold cache, and slower on a laptop that is also running the host. A budget that
20881
+ * expires mid-compile would report a failure the very next attempt (with the compiler cache warm)
20882
+ * would have avoided, and the item is `background`, so this spends no startup allowance.
20883
+ */
20884
+ const NATIVE_DEPS_ATTEMPT_TIMEOUT_MS = 9e5;
19827
20885
  /** This module's warning token, so one grep finds every environment-initialisation line. */
19828
20886
  const OWN_PREFIX = "envinit:";
19829
20887
  /**
@@ -19858,6 +20916,104 @@ function sameDir(a, b) {
19858
20916
  return resolve(expandHome$1(a)) === resolve(expandHome$1(b));
19859
20917
  }
19860
20918
  /**
20919
+ * The `runtime-deps` spec the base's shared store should own, or `undefined` when it should not.
20920
+ *
20921
+ * Refused when:
20922
+ *
20923
+ * 1. **self-heal is off** — the shipped default. Enabling it is a network decision the operator
20924
+ * makes (`semantic.self_heal.enabled`), never a startup surprise.
20925
+ * 2. **the backend is not `local_bge`** — this item repairs the module the local ONNX adapter
20926
+ * loads; a third-party backend owns its own dependencies.
20927
+ * 3. **no package was declared** — `spec.packages` must be non-empty (the base rejects an empty
20928
+ * map); declaring nothing is cleaner than a guaranteed `invalid-option`.
20929
+ *
20930
+ * The config layers are copied field by field, not passed through: the schema's defaults include an
20931
+ * empty `cache` placeholder that has to become the family root here (`<home>/cache/npm`), because
20932
+ * the schema cannot know which family root the host will resolve. An explicitly configured cache is
20933
+ * `~`-expanded, exactly like every other path in this plugin.
20934
+ */
20935
+ function managedSemanticDepsSpec(semantic, declaration, home) {
20936
+ if (!semantic.self_heal.enabled) return void 0;
20937
+ if (semantic.backend !== "local_bge") return void 0;
20938
+ if (Object.keys(declaration.packages).length === 0) return void 0;
20939
+ return runtimeDepsSpecOf(declaration, home);
20940
+ }
20941
+ /**
20942
+ * The `runtime-deps` spec for the NATIVE packages, or `undefined` when nothing should be declared.
20943
+ *
20944
+ * Refused only when `semantic.native_self_heal.enabled` is off or the package map is empty. There is
20945
+ * no backend condition here (unlike the semantic spec): the tokenizer and the vector store are used
20946
+ * by every backend, and a `vectorStore.backend: local_numpy` deployment still pays for `nodejieba`.
20947
+ */
20948
+ function managedNativeDepsSpec(semantic, declaration, home) {
20949
+ if (!semantic.native_self_heal.enabled) return void 0;
20950
+ if (Object.keys(declaration.packages).length === 0) return void 0;
20951
+ return runtimeDepsSpecOf(declaration, home);
20952
+ }
20953
+ /**
20954
+ * The shared shape of both `runtime-deps` specs: packages, sources, attempts, verify.
20955
+ *
20956
+ * One implementation, because the two sections use ONE provider vocabulary — a second copy of the
20957
+ * cache/`~` rules is how a private registry ends up working in one section and not the other. The
20958
+ * config layers are copied field by field, not passed through: the schema's defaults include an empty
20959
+ * `cache` placeholder that has to become the family root here (`<home>/cache/npm`), because the
20960
+ * schema cannot know which family root the host will resolve.
20961
+ */
20962
+ function runtimeDepsSpecOf(heal, home) {
20963
+ const packages = {};
20964
+ for (const [name, range] of Object.entries(heal.packages)) packages[name] = range;
20965
+ return {
20966
+ packages,
20967
+ sources: heal.sources.map((source) => {
20968
+ const registry = source.registry.trim();
20969
+ const configuredCache = source.cache.trim();
20970
+ const cache = registry === "" && configuredCache === "" ? join(home, "cache", "npm") : configuredCache === "" ? "" : expandHome$1(configuredCache);
20971
+ return {
20972
+ id: source.id,
20973
+ ...registry === "" ? {} : { registry },
20974
+ ...cache === "" ? {} : { cache }
20975
+ };
20976
+ }),
20977
+ attempts: heal.attempts.map((attempt) => ({
20978
+ source: attempt.source,
20979
+ ...attempt.manager === void 0 ? {} : { manager: attempt.manager },
20980
+ ...Object.keys(attempt.env).length === 0 ? {} : { env: { ...attempt.env } },
20981
+ ...attempt.allowScripts === void 0 ? {} : { allowScripts: attempt.allowScripts }
20982
+ })),
20983
+ verify: [...heal.verify]
20984
+ };
20985
+ }
20986
+ /**
20987
+ * The configured dependency-audit entries, copied field by field and resolved for execution.
20988
+ *
20989
+ * Why this is not part of a self-heal spec: the spec is what the base INSTALLS (and part of the shared
20990
+ * root's content key, so it has to be exactly the provider's vocabulary), while an audit entry is a
20991
+ * PROMPT line plus the observation behind it. Two consumers, two lifetimes — editing a sentence must
20992
+ * not look like a new dependency set.
20993
+ *
20994
+ * The entries are returned VERBATIM (text, severity, condition) and no default is invented: the
20995
+ * config's default is the empty array, so a profile that never wrote one gets no injection at all.
20996
+ * `min_vectors` is defaulted to `0` here, and to nothing else — this section names no package, so it
20997
+ * also invents no package-specific threshold.
20998
+ */
20999
+ function managedDependencyAudit(audit) {
21000
+ if (!Array.isArray(audit)) return [];
21001
+ return audit.map((entry) => ({
21002
+ id: entry.id,
21003
+ severity: entry.severity,
21004
+ when: entry.when,
21005
+ minVectors: entry.min_vectors ?? 0,
21006
+ body: entry.body,
21007
+ probe: entry.kind === "module" ? {
21008
+ kind: "module",
21009
+ specifier: entry.specifier
21010
+ } : {
21011
+ kind: "runtime-deps",
21012
+ item: entry.item
21013
+ }
21014
+ }));
21015
+ }
21016
+ /**
19861
21017
  * The family root: `$AVANTF_HOME`, else `~/.avantf/env`.
19862
21018
  *
19863
21019
  * `AVANTF_HOME` is the same variable the engine uses for its DATA home (`memory/`),
@@ -19947,6 +21103,121 @@ function modelItem(framework, model) {
19947
21103
  };
19948
21104
  }
19949
21105
  /**
21106
+ * A `runtime-deps` item with the DELIVERY policy this plugin owns (the helper supplies only the
21107
+ * shape): both `onMissing` axes `degrade` — a failed install may not refuse the mount — `background`
21108
+ * (an install may take minutes and spends no startup budget), and the caller's per-attempt budget.
21109
+ *
21110
+ * `target.root: 'deps/runtime'` is the base's canonical store, already set by the helper. Two items
21111
+ * with different package sets are different content keys and therefore different immutable roots
21112
+ * inside it, which is what keeps `mem:semantic-deps` and `mem:native-deps` from sharing an install.
21113
+ */
21114
+ function runtimeDepsItem(item, timeoutMs) {
21115
+ return {
21116
+ ...item,
21117
+ onMissing: {
21118
+ atStartup: "degrade",
21119
+ atUse: "degrade"
21120
+ },
21121
+ startup: "background",
21122
+ policy: { timeoutMs }
21123
+ };
21124
+ }
21125
+ /** The provisioner's five states in the audit's vocabulary. */
21126
+ function dependencyStateOf(state) {
21127
+ switch (state?.state) {
21128
+ case "ready": return "available";
21129
+ case "failed": return "failed";
21130
+ case "missing": return "missing";
21131
+ case "skipped": return "skipped";
21132
+ case "pending": return "pending";
21133
+ default: return "unknown";
21134
+ }
21135
+ }
21136
+ /**
21137
+ * Which observed audit state one configured `when` covers.
21138
+ *
21139
+ * The same word means the same thing for both probe KINDS, so one mapping serves both:
21140
+ * `failed` → a load that threw / an item that failed; `missing` → nothing resolved, or the item is
21141
+ * missing/skipped; `degraded` → any of those three. `available`, `pending` and `unknown` match
21142
+ * NOTHING: a background install still in flight and an unobserved entry are both "not reportable
21143
+ * yet", and a healthy dependency is not a finding.
21144
+ */
21145
+ function matchesWhen(when, state) {
21146
+ switch (when) {
21147
+ case "failed": return state === "failed";
21148
+ case "missing": return state === "missing" || state === "skipped";
21149
+ case "degraded": return state === "failed" || state === "missing" || state === "skipped";
21150
+ }
21151
+ }
21152
+ /**
21153
+ * Register one session notice per configured audit entry, judged on the AUDIT's cached state.
21154
+ *
21155
+ * The predicate is a pure read of two snapshots (`state()` is a map read, `vectors()` is the live
21156
+ * index count), so the prompt path never probes anything — the expensive half ran once at startup and
21157
+ * again on each settlement (`startDependencyAudit` / the `onSettled` path). A line is delivered when:
21158
+ *
21159
+ * - the entry's recorded state matches the configured `when` (see {@link matchesWhen}), and
21160
+ * - the live vector index has reached `minVectors` (0 = no threshold).
21161
+ *
21162
+ * Registration itself is total: the base registry warns and ignores a duplicate/malformed id, and an
21163
+ * id that is already taken costs one line, never the step.
21164
+ */
21165
+ function registerDependencyNotices(input) {
21166
+ for (const notice of input.notices) input.registry.register({
21167
+ id: notice.id,
21168
+ severity: notice.severity,
21169
+ when: notice.when,
21170
+ body: notice.body
21171
+ }, () => matchesWhen(notice.when, input.audit.state(notice.id)) && input.audit.vectors() >= notice.minVectors);
21172
+ }
21173
+ /** Build the probes an audit entry needs — one per configured KIND, no package named here. */
21174
+ function probesOf(notices, roots, itemState) {
21175
+ return notices.map((notice) => {
21176
+ switch (notice.probe.kind) {
21177
+ case "module": {
21178
+ const { specifier } = notice.probe;
21179
+ return {
21180
+ id: notice.id,
21181
+ check: () => moduleProbe(notice.id, specifier, roots)
21182
+ };
21183
+ }
21184
+ case "runtime-deps": {
21185
+ const { item } = notice.probe;
21186
+ return itemProbe(notice.id, item, itemState);
21187
+ }
21188
+ }
21189
+ });
21190
+ }
21191
+ /**
21192
+ * Whether the loaded base can serve the shared `runtime-deps` item AND observe it.
21193
+ *
21194
+ * Three members are required, and all three are one capability here: `RUNTIME_DEPS_KIND` /
21195
+ * `runtimeDepsProvider` (interface v5, base 0.6.0) install the root, and `createRuntimeDeps`
21196
+ * (interface v6, base 0.7.0) is what DECLARES the item and binds the notices to its state. Since the
21197
+ * item is now built by that helper, a base with the provider but not the helper cannot serve this
21198
+ * plugin at all — so the predicate answers the caller's real question ("may I declare, wait for and
21199
+ * translate this item?") instead of a narrower one that would let a half-wired declaration through.
21200
+ *
21201
+ * An OLDER generation does not normally reach this point — the interface gate withholds the whole
21202
+ * base first (`gateOrDegrade`), so the plugin takes its legacy path. This guard covers the remaining
21203
+ * shape: a module that reports a current generation but does not actually carry the members (a
21204
+ * partial/hostile module, or a packaging mistake). Enabling self-heal must then degrade to the
21205
+ * runtime's optional-dependency path, not throw from `declare`. It is also what lets the fake
21206
+ * framework in the tests exercise that degradation instead of a TypeError.
21207
+ *
21208
+ * Exported because the startup caller asks the same question: a skipped item will never settle, so
21209
+ * `startManagedResources` must not wait for it (see `packages/plugin/src/index.ts`). One predicate,
21210
+ * two readers — the loader's skip and the caller's "do not wait" can never disagree.
21211
+ *
21212
+ * `undefined` (the test seam's partial framework, or no module at all) answers `false`: nothing is
21213
+ * served, so nothing may be awaited.
21214
+ */
21215
+ function supportsRuntimeDeps(framework) {
21216
+ if (framework === void 0) return false;
21217
+ const candidate = framework;
21218
+ return typeof candidate.RUNTIME_DEPS_KIND === "string" && typeof candidate.runtimeDepsProvider === "function" && typeof candidate.createRuntimeDeps === "function";
21219
+ }
21220
+ /**
19950
21221
  * The plugin-specific halves of the shared loader's sentences: mem says the FRAMEWORK is skipped and
19951
21222
  * names the legacy provisioning path among the withheld capabilities. The mechanism is the shim's
19952
21223
  * (`interface_gate.ts`), the wording is this plugin's.
@@ -19980,6 +21251,8 @@ function loadEnvinit(options = {}) {
19980
21251
  /** The body of {@link loadEnvinit}; the cache above makes it run at most once per process. */
19981
21252
  async function loadOnce(options) {
19982
21253
  const home = options.home ?? resolveHome();
21254
+ const declaration = loadRuntimeDepsResource(options.depsResourceUrl);
21255
+ if (!declaration.ok) options.log?.error(`${OWN_PREFIX} ERROR — ${declaration.path} is unusable (${declaration.reason}); self-heal and the startup dependency audit are SKIPPED for this run (the plugin mounts, and every optional dependency keeps its own fallback)`);
19983
21256
  const range = await readDependencyRange(fileURLToPath(import.meta.url));
19984
21257
  const autoDownload = options.autoDownload ?? envDownloadsAllowed();
19985
21258
  const deadlineMs = options.deadlineMs ?? DEADLINE_MS;
@@ -20001,9 +21274,15 @@ async function loadOnce(options) {
20001
21274
  try {
20002
21275
  compat = runtimeFromCompat(options.compatModule ?? framework);
20003
21276
  } catch (error) {
20004
- 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`);
21277
+ warn(options.log, `${OWN_PREFIX} WARNING — the ${COMPAT_PACKAGE} compatibility gate could not be initialised (${reasonOf$2(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
20005
21278
  }
20006
21279
  let disposed = false;
21280
+ /** The shared `runtime-deps` handles: set when `provisionResources` declares each item. */
21281
+ let deps;
21282
+ let native;
21283
+ /** The startup dependency audit and its notice registry; both exist once `provisionResources` ran. */
21284
+ let audit;
21285
+ let noticeRegistry;
20007
21286
  return {
20008
21287
  home,
20009
21288
  kit: framework,
@@ -20012,40 +21291,148 @@ async function loadOnce(options) {
20012
21291
  tools: join(home, "tools"),
20013
21292
  models: join(home, "models")
20014
21293
  },
20015
- provisionResources(plan, onSettled) {
21294
+ runtimeDeps() {
21295
+ return deps;
21296
+ },
21297
+ nativeDeps() {
21298
+ return native;
21299
+ },
21300
+ depsResource() {
21301
+ return declaration;
21302
+ },
21303
+ notices() {
21304
+ return noticeRegistry;
21305
+ },
21306
+ async startDependencyAudit() {
21307
+ if (audit === void 0) return;
21308
+ try {
21309
+ await audit.run();
21310
+ } catch (error) {
21311
+ warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit failed (${reasonOf$2(error)}); notices stay silent`);
21312
+ }
21313
+ },
21314
+ provisionResources(plan, onSettled, onFailure) {
20016
21315
  if (disposed) return;
21316
+ const canServe = supportsRuntimeDeps(framework);
21317
+ const wantedDeps = plan.semanticDeps;
21318
+ const wantedNative = plan.nativeDeps;
21319
+ if (wantedDeps !== void 0 && !canServe) warn(options.log, `${OWN_PREFIX} WARNING — semantic self-heal is enabled but the loaded ${FRAMEWORK_PACKAGE} has no runtime-deps helper (it needs base >= 0.7.0 / interface v6); the item is SKIPPED and the plugin will mount anyway`);
21320
+ if (wantedNative !== void 0 && !canServe) warn(options.log, `${OWN_PREFIX} WARNING — native self-heal is enabled but the loaded ${FRAMEWORK_PACKAGE} has no runtime-deps helper (it needs base >= 0.7.0 / interface v6); the item is SKIPPED and the plugin will mount anyway`);
20017
21321
  const items = [];
20018
21322
  if (plan.model !== void 0) items.push(modelItem(framework, plan.model));
20019
- if (items.length === 0) return;
21323
+ /**
21324
+ * Declare one shared item through the helper.
21325
+ *
21326
+ * The spec fields are forwarded verbatim (the helper copies them, so the content key cannot
21327
+ * move under a mutating caller). Notices are NOT passed in: the helper's registry binds a line
21328
+ * to its ITEM's state alone, while an audit entry may observe a package that no item
21329
+ * of ours installs — so every notice is registered below, on one registry, from the audit.
21330
+ */
21331
+ const declare = (itemId, spec, timeoutMs, label) => {
21332
+ try {
21333
+ const handle = framework.createRuntimeDeps({
21334
+ namespace: PLUGIN,
21335
+ itemId,
21336
+ ...spec
21337
+ });
21338
+ items.push(runtimeDepsItem(handle.item, timeoutMs));
21339
+ return handle;
21340
+ } catch (error) {
21341
+ warn(options.log, `${OWN_PREFIX} WARNING — the shared ${label} runtime-deps item could not be declared (${reasonOf$2(error)}); self-heal for it is SKIPPED and the plugin will mount anyway`);
21342
+ return;
21343
+ }
21344
+ };
21345
+ if (wantedDeps !== void 0 && canServe) deps = declare(SEMANTIC_DEPS_ITEM, wantedDeps.spec, SEMANTIC_DEPS_ATTEMPT_TIMEOUT_MS, "semantic");
21346
+ if (wantedNative !== void 0 && canServe) native = declare(NATIVE_DEPS_ITEM, wantedNative.spec, NATIVE_DEPS_ATTEMPT_TIMEOUT_MS, "native");
21347
+ const handles = [deps, native].filter((handle) => handle !== void 0);
20020
21348
  const maxItemTimeoutMs = items.reduce((max, item) => Math.max(max, item.policy?.timeoutMs ?? 0), 0);
20021
- const resources = framework.createProvisioner({
20022
- home,
20023
- envinitRange: range,
20024
- logger: provisionLogger(options.log),
20025
- policy: {
20026
- autoDownload: autoDownload ? { [framework.MODEL_CACHE_KIND]: plan.modelAutoDownload } : false,
20027
- deadlineMs: Math.max(deadlineMs, maxItemTimeoutMs),
20028
- mirrors: { archive: [...plan.archiveMirrors] }
21349
+ /** The shared roots a module probe may resolve from, in declaration order (ready ones only). */
21350
+ const roots = () => handles.map((handle) => handle.resolve()?.dir).filter((dir) => dir !== void 0 && dir !== "");
21351
+ let resources;
21352
+ if (items.length > 0) {
21353
+ resources = framework.createProvisioner({
21354
+ home,
21355
+ envinitRange: range,
21356
+ logger: provisionLogger(options.log),
21357
+ policy: {
21358
+ autoDownload: autoDownload ? {
21359
+ [framework.MODEL_CACHE_KIND]: plan.modelAutoDownload,
21360
+ ...handles.length === 0 ? {} : { [framework.RUNTIME_DEPS_KIND]: true }
21361
+ } : false,
21362
+ deadlineMs: Math.max(deadlineMs, maxItemTimeoutMs),
21363
+ mirrors: { archive: [...plan.archiveMirrors] }
21364
+ }
21365
+ });
21366
+ resources.register(framework.binaryArchiveProvider());
21367
+ resources.register(framework.modelCacheProvider());
21368
+ if (handles.length > 0) resources.register(framework.runtimeDepsProvider());
21369
+ resources.declare({
21370
+ plugin: PLUGIN,
21371
+ items
21372
+ });
21373
+ disposers.push(resources);
21374
+ }
21375
+ const inflight = new Set(items.map((item) => item.id));
21376
+ const itemState = (id) => {
21377
+ if (inflight.has(id)) return "pending";
21378
+ if (resources === void 0) return "missing";
21379
+ return dependencyStateOf(resources.resolve(id));
21380
+ };
21381
+ const notices = plan.dependencyAudit ?? [];
21382
+ if (notices.length > 0) {
21383
+ const createRegistry = framework.createSessionNotices;
21384
+ if (typeof createRegistry !== "function") warn(options.log, `${OWN_PREFIX} WARNING — ${notices.length} dependency-audit entr(y/ies) are declared but the loaded ${FRAMEWORK_PACKAGE} has no session-notice registry (it needs base >= 0.7.0 / interface v6); no notice will be delivered`);
21385
+ audit = createDependencyAudit({
21386
+ probes: probesOf(notices, roots, itemState),
21387
+ vectors: plan.vectorCount ?? (() => 0),
21388
+ logger: {
21389
+ info: (message) => {
21390
+ options.log?.info(`${OWN_PREFIX} ${message}`);
21391
+ },
21392
+ warn: (message) => {
21393
+ options.log?.warn(`${OWN_PREFIX} ${message}`);
21394
+ }
21395
+ }
21396
+ });
21397
+ if (typeof createRegistry === "function") {
21398
+ noticeRegistry = createRegistry.call(framework, provisionLogger(options.log));
21399
+ registerDependencyNotices({
21400
+ registry: noticeRegistry,
21401
+ notices,
21402
+ audit
21403
+ });
20029
21404
  }
21405
+ }
21406
+ if (resources === void 0) return;
21407
+ for (const handle of handles) handle.ensure(resources).catch((error) => {
21408
+ warn(options.log, `${OWN_PREFIX} WARNING — the shared runtime-deps item could not be dispatched (${reasonOf$2(error)}); the plugin continues`);
20030
21409
  });
20031
- resources.register(framework.binaryArchiveProvider());
20032
- resources.register(framework.modelCacheProvider());
20033
- resources.declare({
20034
- plugin: PLUGIN,
20035
- items
20036
- });
20037
- disposers.push(resources);
20038
21410
  resources.ensure({
20039
21411
  only: items.map((item) => item.id),
20040
21412
  onSettled: (entry) => {
20041
21413
  try {
20042
- onSettled(entry);
21414
+ inflight.delete(entry.id);
21415
+ const shared = handles.find((handle) => handle.item.id === entry.id)?.resolve();
21416
+ const state = resources.resolve(entry.id);
21417
+ onSettled(entry, shared !== void 0 ? shared.dir : state !== void 0 && state.state === "ready" ? state.handle.dir : void 0);
21418
+ if (audit !== void 0) audit.run().catch((error) => {
21419
+ warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit after ${entry.id} failed (${reasonOf$2(error)})`);
21420
+ });
20043
21421
  } catch (error) {
20044
- warn(options.log, `${OWN_PREFIX} WARNING — the onSettled callback failed (${reasonOf(error)})`);
21422
+ warn(options.log, `${OWN_PREFIX} WARNING — the onSettled callback failed (${reasonOf$2(error)})`);
20045
21423
  }
20046
21424
  }
20047
21425
  }).catch((error) => {
20048
- warn(options.log, `${OWN_PREFIX} WARNING — declaring the background resources failed (${reasonOf(error)}); the plugin continues (tools stay registered)`);
21426
+ warn(options.log, `${OWN_PREFIX} WARNING — declaring the background resources failed (${reasonOf$2(error)}); the plugin continues (tools stay registered)`);
21427
+ inflight.clear();
21428
+ if (audit !== void 0) audit.run().catch((auditError) => {
21429
+ warn(options.log, `${OWN_PREFIX} WARNING — the dependency audit after a dispatch failure failed (${reasonOf$2(auditError)})`);
21430
+ });
21431
+ try {
21432
+ onFailure?.(error);
21433
+ } catch (callbackError) {
21434
+ warn(options.log, `${OWN_PREFIX} WARNING — the onFailure callback failed (${reasonOf$2(callbackError)})`);
21435
+ }
20049
21436
  });
20050
21437
  },
20051
21438
  dispose() {
@@ -20682,6 +22069,26 @@ function degradedLegNote(value) {
20682
22069
  const result = value?.result;
20683
22070
  return result !== null && typeof result === "object" && result.degraded === true ? DEGRADED_LEG_NOTE : void 0;
20684
22071
  }
22072
+ /**
22073
+ * The paging sentence of a PURE TIME window (`mode:"time"`, RECALL_INTERFACE §2.4), as one line the
22074
+ * model can read without doing arithmetic: 「共 N 条,已显示 M 条,还有 K 条未显示」.
22075
+ *
22076
+ * The fields it spells out (`total` / `hits.length` / `remaining`) are already in the JSON block; the
22077
+ * sentence exists because a page of 50 out of a 286-row day otherwise looks like the whole day unless
22078
+ * the reader stops to compare four numbers. Emitted ONLY when something is left unseen — a window that
22079
+ * fits on its page needs no note, and `remaining` is 0 exactly then.
22080
+ *
22081
+ * Same rules as {@link degradedLegNote}: a SECOND block, appended after the parseable JSON one, and
22082
+ * absent whenever the payload is not a time result (including older hosts, which send no `remaining`).
22083
+ */
22084
+ function timePageNote(value) {
22085
+ const result = value?.result;
22086
+ if (result === null || typeof result !== "object" || result.mode !== "time") return void 0;
22087
+ const { total, remaining, hits } = result;
22088
+ if (typeof total !== "number" || typeof remaining !== "number" || remaining <= 0) return void 0;
22089
+ if (!Array.isArray(hits)) return void 0;
22090
+ return `时间窗口共 ${String(total)} 条,已显示 ${String(hits.length)} 条,还有 ${String(remaining)} 条未显示。`;
22091
+ }
20685
22092
  /** The tool output spec registered on every mem tool. */
20686
22093
  const OUTPUT = {
20687
22094
  schema: {
@@ -20693,10 +22100,15 @@ const OUTPUT = {
20693
22100
  type: "text",
20694
22101
  text: JSON.stringify(toWellFormedDeep(value), null, 2)
20695
22102
  }];
20696
- const note = degradedLegNote(value);
20697
- if (note !== void 0) blocks.push({
22103
+ const degraded = degradedLegNote(value);
22104
+ if (degraded !== void 0) blocks.push({
20698
22105
  type: "text",
20699
- text: note
22106
+ text: degraded
22107
+ });
22108
+ const paging = timePageNote(value);
22109
+ if (paging !== void 0) blocks.push({
22110
+ type: "text",
22111
+ text: paging
20700
22112
  });
20701
22113
  return blocks;
20702
22114
  }
@@ -20954,6 +22366,22 @@ function hostLogger(ctx) {
20954
22366
  };
20955
22367
  }
20956
22368
  /**
22369
+ * The host's agent registry (`ctx.agents`), read defensively.
22370
+ *
22371
+ * It is the one place this plugin learns whether a session is a top-level (owner) agent or a
22372
+ * worker/subagent, and that decision only decides whether a session NOTICE is delivered. The read is
22373
+ * deliberately structural ({@link isOwnerSession} takes `unknown`): a host may not load the agent
22374
+ * registry at all, and `agent/pre-step` itself is emitted by the agent loop, so an absent registry is
22375
+ * "cannot tell" — never a reason to throw on the prompt path.
22376
+ */
22377
+ function agentRegistry(ctx) {
22378
+ try {
22379
+ return ctx.agents;
22380
+ } catch {
22381
+ return;
22382
+ }
22383
+ }
22384
+ /**
20957
22385
  * Declare the expensive resources through the family framework, warm the tokenizer and the embedding
20958
22386
  * model, and continue initialisation as each item settles.
20959
22387
  *
@@ -20970,12 +22398,37 @@ function hostLogger(ctx) {
20970
22398
  * — `managedModelSpec` decides): the framework's root would then collect a copy nothing reads. In
20971
22399
  * that case, and whenever no item will warm the model, the warm is started here directly.
20972
22400
  *
22401
+ * **Optional dependency repair.** `mem:semantic-deps` (shared `runtime-deps` store) is declared only
22402
+ * when `semantic.self_heal.enabled` is on (`managedSemanticDepsSpec` decides). When it is present, the
22403
+ * semantic warm waits for BOTH items: the shared root has to exist before the backend chooses it, and
22404
+ * the model files have to be on disk before the pipeline opens them. On success the handle directory
22405
+ * is handed to the backend (`useSemanticModuleDir`) so the import comes from the verified shared root;
22406
+ * on failure the backend keeps its own optional-dependency resolution.
22407
+ *
22408
+ * SESSION NOTICES are NOT carried by these handles: they are the `audit` entries of the plugin's own
22409
+ * `res/runtime-deps.yaml`, registered on one registry (`env.notices()`) whose predicates read the
22410
+ * startup dependency audit's cached state (`deps_audit.ts`). That is why a repair that is still
22411
+ * installing cannot be reported as a failure, and why an entry about a PACKAGE (rather than an item)
22412
+ * can be delivered at all.
22413
+ *
22414
+ * **NATIVE optional dependency repair.** `mem:native-deps` (a SEPARATE `runtime-deps` item: different
22415
+ * package set ⇒ different content-addressed root) repairs `nodejieba` and `hnswlib-node` when
22416
+ * `semantic.native_self_heal.enabled` is on. When it is present the TOKENIZER warm is deferred to its
22417
+ * settlement: the dictionary parse (~1 s, synchronous) has to happen once, from the root the process
22418
+ * will actually use, not from a tree that is about to be superseded. On success both module roots are
22419
+ * handed to their loaders (`useTokenizerModuleDir`, `MemoryStore.useNativeModuleDir`).
22420
+ *
20973
22421
  * Outcomes handled below:
20974
22422
  *
20975
22423
  * - `mem:model` settles (present/installed) → warm the semantic path from the managed root, now
20976
22424
  * that the files are on disk. That ordering is the whole point: the runtime's constructor warm is
20977
22425
  * deferred while the framework is up (`deferWarm`), so nothing can race the install, and the
20978
22426
  * explicit warm here is what starts the model.
22427
+ * - `mem:semantic-deps` settles (present/installed) → point the backend at the shared root, then
22428
+ * warm. failed/skipped → keep the optional path and still warm. Either way the package's audit
22429
+ * entry is refreshed, because the notice's other half is "is it loadable NOW".
22430
+ * - `mem:native-deps` settles → hand both module roots over, refresh the two native audit entries,
22431
+ * then warm the tokenizer (or retry it, when an earlier lazy write already failed).
20979
22432
  * - `mem:model` failed/skipped → one WARNING, then STILL warm: with `semantic.auto_download` on the
20980
22433
  * runtime takes its own download path, and with it off the semantic leg degrades to FTS+entity.
20981
22434
  * Either way the mount is untouched.
@@ -20989,20 +22442,65 @@ function hostLogger(ctx) {
20989
22442
  function startManagedResources(env, rt, logger, waitFor) {
20990
22443
  const { semantic, tools } = rt.config.common;
20991
22444
  const model = managedModelSpec(semantic, env.roots.models);
22445
+ const loaded = env.depsResource();
22446
+ const spec = loaded.ok ? loaded.resource : void 0;
22447
+ const depsSpec = spec === void 0 ? void 0 : managedSemanticDepsSpec(semantic, spec.semantic, env.home);
22448
+ const semanticDeps = depsSpec === void 0 ? void 0 : { spec: depsSpec };
22449
+ const nativeSpec = spec === void 0 ? void 0 : managedNativeDepsSpec(semantic, spec.native, env.home);
22450
+ const nativeDeps = nativeSpec === void 0 ? void 0 : { spec: nativeSpec };
20992
22451
  const plan = {
20993
22452
  archiveMirrors: tools.mirror,
20994
22453
  modelAutoDownload: semantic.auto_download,
20995
- model
22454
+ model,
22455
+ semanticDeps,
22456
+ nativeDeps,
22457
+ dependencyAudit: managedDependencyAudit(spec?.audit),
22458
+ vectorCount: () => rt.memory.liveVectorCount()
22459
+ };
22460
+ const waitsForNative = nativeDeps !== void 0 && supportsRuntimeDeps(env.kit);
22461
+ let tokenizerWarmStarted = false;
22462
+ const warmTokenizer = () => {
22463
+ if (tokenizerWarmStarted) return;
22464
+ tokenizerWarmStarted = true;
22465
+ warmTokenizerAsync(rt, { ...waitFor === void 0 ? {} : { waitFor } });
20996
22466
  };
20997
- warmTokenizerAsync(rt, { ...waitFor === void 0 ? {} : { waitFor } });
20998
- if (model === void 0 && semantic.backend === "local_bge") warmSemanticAsync(rt);
20999
- env.provisionResources(plan, (entry) => {
21000
- if (entry.id === "mem:model") warmSemanticAsync(rt);
22467
+ if (!waitsForNative) warmTokenizer();
22468
+ const pending = /* @__PURE__ */ new Set();
22469
+ if (model !== void 0) pending.add(MODEL_ITEM);
22470
+ if (semanticDeps !== void 0 && supportsRuntimeDeps(env.kit)) pending.add(SEMANTIC_DEPS_ITEM);
22471
+ const warmOnce = () => {
22472
+ if (pending.size === 0) warmSemanticAsync(rt);
22473
+ };
22474
+ if (pending.size === 0 && semantic.backend === "local_bge") warmSemanticAsync(rt);
22475
+ env.provisionResources(plan, (entry, handleDir) => {
22476
+ if (entry.id === "mem:model" || entry.id === "mem:semantic-deps") {
22477
+ if (entry.id === "mem:semantic-deps" && handleDir !== void 0) {
22478
+ rt.memory.useSemanticModuleDir(handleDir);
22479
+ logger.info(`envinit: semantic deps root ${handleDir}`);
22480
+ }
22481
+ pending.delete(entry.id);
22482
+ warmOnce();
22483
+ }
22484
+ if (entry.id === "mem:native-deps") {
22485
+ if (handleDir !== void 0) {
22486
+ useTokenizerModuleDir(handleDir);
22487
+ rt.memory.useNativeModuleDir(handleDir);
22488
+ logger.info(`envinit: native deps root ${handleDir}`);
22489
+ }
22490
+ warmTokenizer();
22491
+ }
21001
22492
  if (entry.action === "failed" || entry.action === "skipped") {
21002
22493
  logger.warn(`envinit: ${entry.id} is ${entry.action} (${String(entry.code)}${entry.reason === void 0 ? "" : ` — ${entry.reason}`})`);
22494
+ if (entry.id === "mem:semantic-deps") logger.warn("envinit: semantic self-heal did not complete — the runtime keeps its own optional-dependency resolution, and retrieval degrades to FTS+entity if that is absent too");
22495
+ if (entry.id === "mem:native-deps") logger.warn("envinit: native self-heal did not complete — the runtime keeps its own optional-dependency resolution; entity/triple extraction falls back to the regex path and the vector store stays brute-force");
21003
22496
  return;
21004
22497
  }
21005
22498
  logger.info(`envinit: ${entry.id} ${entry.action} (${entry.source}${entry.version === void 0 ? "" : ` ${entry.version}`})`);
22499
+ }, (error) => {
22500
+ logger.warn(`envinit: dispatching the background resources failed (${describeError$1(error)}); warming from the runtime's own paths`);
22501
+ pending.clear();
22502
+ warmOnce();
22503
+ warmTokenizer();
21006
22504
  });
21007
22505
  }
21008
22506
  async function apply(ctx, config) {
@@ -21130,7 +22628,7 @@ async function apply(ctx, config) {
21130
22628
  }
21131
22629
  for (const warning of promptTextWarnings(promptSections)) logger.warn(`prompt text: ${warning}`);
21132
22630
  logger.info(`prompt files: ${promptDir} (${loadedPrompts.map((entry) => `${entry.file}:${entry.source}`).join(", ")})`);
21133
- ctx.on("agent/pre-step", async ({ signal }, next) => {
22631
+ ctx.on("agent/pre-step", async ({ agent, signal }, next) => {
21134
22632
  const decision = await next();
21135
22633
  if (decision.kind === "reject" || signal.aborted) return decision;
21136
22634
  const text = freshUserText(decision.messages);
@@ -21141,18 +22639,22 @@ async function apply(ctx, config) {
21141
22639
  } catch {
21142
22640
  hit = false;
21143
22641
  }
21144
- if (!hit) return decision;
22642
+ const sessionId = String(agent.id);
22643
+ const owner = isOwnerSession(agentRegistry(ctx), sessionId);
22644
+ const notices = noticeLines(env?.notices(), sessionId, owner);
22645
+ const injection = stepInjection(hit, notices);
22646
+ if (injection === "") return decision;
21145
22647
  return {
21146
22648
  ...decision,
21147
22649
  messages: [...decision.messages, createUserMessage({
21148
22650
  content: [{
21149
22651
  type: "text",
21150
- text: RELEVANCE_HINT
22652
+ text: injection
21151
22653
  }],
21152
22654
  source: {
21153
22655
  kind: "mem-hint",
21154
22656
  form: "notice",
21155
- summary: RELEVANCE_HINT
22657
+ summary: injection
21156
22658
  }
21157
22659
  })]
21158
22660
  };
@@ -21179,6 +22681,7 @@ async function apply(ctx, config) {
21179
22681
  log: logger,
21180
22682
  compat
21181
22683
  });
22684
+ env?.startDependencyAudit();
21182
22685
  ctx.effect(() => async () => {
21183
22686
  logger.info("plugin unmount: disposing runtime (closing databases)");
21184
22687
  await rt.shutdown();