@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.
- package/lib/client.js +1088 -303
- package/lib/engine/graph-recall/index.d.ts +2 -2
- package/lib/engine/graph-recall/traverse.d.ts +2 -2
- package/lib/engine/graph-recall/types.d.ts +1 -1
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/defaults.d.ts +4 -1
- package/lib/engine/mem/entities/extract.d.ts +34 -14
- package/lib/engine/mem/index.d.ts +1 -0
- package/lib/engine/mem/modelBootstrap.d.ts +29 -0
- package/lib/engine/mem/store/graph_reader.d.ts +2 -2
- package/lib/engine/mem/store/memory.d.ts +53 -6
- package/lib/engine/mem/store/recall_policy.d.ts +10 -0
- package/lib/engine/mem-contract/config.d.ts +135 -0
- package/lib/engine/mem-contract/types.d.ts +11 -0
- package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +10 -0
- package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +28 -0
- package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +48 -6
- package/lib/engine/mem-retrieval/interfaces.d.ts +22 -2
- package/lib/engine/mem-retrieval/text_budget.d.ts +31 -0
- package/lib/index.js +1617 -114
- package/lib/interface-version.json +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paging.d.ts +58 -8
- package/lib/types/client/paging.d.ts.map +1 -1
- package/lib/types/client/time_filter.d.ts +54 -0
- package/lib/types/client/time_filter.d.ts.map +1 -0
- package/lib/types/deps_audit.d.ts +88 -0
- package/lib/types/deps_audit.d.ts.map +1 -0
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/envinit.d.ts +224 -4
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +58 -0
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/render.d.ts.map +1 -1
- package/lib/types/runtime_deps_resource.d.ts +29 -0
- package/lib/types/runtime_deps_resource.d.ts.map +1 -0
- package/package.json +5 -4
- 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
|
|
735
|
-
*
|
|
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
|
|
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
|
|
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$
|
|
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$
|
|
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
|
|
2714
|
-
|
|
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
|
-
|
|
2722
|
-
|
|
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
|
-
|
|
2918
|
-
|
|
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
|
-
*
|
|
4292
|
+
* The window-sized slices `text` is encoded in (see `text_budget.splitToWindows`).
|
|
3812
4293
|
*
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3815
|
-
*
|
|
3816
|
-
*
|
|
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
|
-
|
|
3819
|
-
|
|
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
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
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=${
|
|
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 <
|
|
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
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
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(
|
|
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
|
|
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
|
|
4070
|
-
|
|
4071
|
-
lib
|
|
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
|
-
|
|
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
|
-
|
|
4233
|
-
|
|
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
|
-
|
|
4272
|
-
|
|
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 (
|
|
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
|
-
*
|
|
16682
|
-
*
|
|
16683
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
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
|
-
|
|
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
|
|
20697
|
-
if (
|
|
22103
|
+
const degraded = degradedLegNote(value);
|
|
22104
|
+
if (degraded !== void 0) blocks.push({
|
|
20698
22105
|
type: "text",
|
|
20699
|
-
text:
|
|
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
|
-
|
|
20998
|
-
|
|
20999
|
-
|
|
21000
|
-
|
|
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
|
-
|
|
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:
|
|
22652
|
+
text: injection
|
|
21151
22653
|
}],
|
|
21152
22654
|
source: {
|
|
21153
22655
|
kind: "mem-hint",
|
|
21154
22656
|
form: "notice",
|
|
21155
|
-
summary:
|
|
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();
|