@avantf/dsh-mem 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +89 -28
  2. package/cordis.patch.yml +17 -0
  3. package/lib/client.js +155 -14
  4. package/lib/engine/mem/config/config_files.d.ts +26 -0
  5. package/lib/engine/mem/config/domains.d.ts +20 -0
  6. package/lib/engine/mem/config/loader.d.ts +35 -0
  7. package/lib/engine/mem/config/paths.d.ts +53 -0
  8. package/lib/engine/mem/db/chunk.d.ts +26 -0
  9. package/lib/engine/mem/db/conn.d.ts +13 -0
  10. package/lib/engine/mem/db/dao/chunks.d.ts +150 -0
  11. package/lib/engine/mem/db/dao/contradictions.d.ts +98 -0
  12. package/lib/engine/mem/db/dao/documents.d.ts +51 -0
  13. package/lib/engine/mem/db/dao/entities.d.ts +67 -0
  14. package/lib/engine/mem/db/dao/facts.d.ts +472 -0
  15. package/lib/engine/mem/db/dao/stats.d.ts +14 -0
  16. package/lib/engine/mem/db/dao/triples.d.ts +80 -0
  17. package/lib/engine/mem/db/hash.d.ts +2 -0
  18. package/lib/engine/mem/db/knowledge.d.ts +17 -0
  19. package/lib/engine/mem/db/migrations.d.ts +71 -0
  20. package/lib/engine/mem/db/port.d.ts +57 -0
  21. package/lib/engine/mem/db/schema.d.ts +26 -0
  22. package/lib/engine/mem/db/sqlite.d.ts +89 -0
  23. package/lib/engine/mem/db/store.d.ts +35 -0
  24. package/lib/engine/mem/db/tokenizer.d.ts +61 -0
  25. package/lib/engine/mem/db/vectors.d.ts +50 -0
  26. package/lib/engine/mem/dispatch.d.ts +51 -0
  27. package/lib/engine/mem/entities/extract.d.ts +84 -0
  28. package/lib/engine/mem/eval/loader.d.ts +19 -0
  29. package/lib/engine/mem/eval/metrics.d.ts +25 -0
  30. package/lib/engine/mem/eval/runner.d.ts +10 -0
  31. package/lib/engine/mem/eval/write_metrics.d.ts +88 -0
  32. package/lib/engine/mem/git.d.ts +73 -0
  33. package/lib/engine/mem/hrr/algebra.d.ts +30 -0
  34. package/lib/engine/mem/hrr/atoms.d.ts +22 -0
  35. package/lib/engine/mem/hrr/encode.d.ts +32 -0
  36. package/lib/engine/mem/hrr/index.d.ts +3 -0
  37. package/lib/engine/mem/index.d.ts +27 -0
  38. package/lib/engine/mem/lifecycle/contradiction.d.ts +156 -0
  39. package/lib/engine/mem/lifecycle/index.d.ts +5 -0
  40. package/lib/engine/mem/lifecycle/maintenance.d.ts +48 -0
  41. package/lib/engine/mem/lifecycle/presence.d.ts +29 -0
  42. package/lib/engine/mem/lifecycle/tick.d.ts +43 -0
  43. package/lib/engine/mem/lifecycle/trust.d.ts +90 -0
  44. package/lib/engine/mem/modelBootstrap.d.ts +207 -0
  45. package/lib/engine/mem/router.d.ts +51 -0
  46. package/lib/engine/mem/runtime.d.ts +232 -0
  47. package/lib/engine/mem/store/common.d.ts +82 -0
  48. package/lib/engine/mem/store/doc_files.d.ts +125 -0
  49. package/lib/engine/mem/store/document_text.d.ts +47 -0
  50. package/lib/engine/mem/store/floors.d.ts +191 -0
  51. package/lib/engine/mem/store/hybrid.d.ts +232 -0
  52. package/lib/engine/mem/store/ingest_guard.d.ts +25 -0
  53. package/lib/engine/mem/store/knowledge.d.ts +483 -0
  54. package/lib/engine/mem/store/lexical.d.ts +67 -0
  55. package/lib/engine/mem/store/memory.d.ts +543 -0
  56. package/lib/engine/mem/store/source_picker.d.ts +43 -0
  57. package/lib/engine/mem-contract/config.d.ts +129 -0
  58. package/lib/engine/mem-contract/env.d.ts +31 -0
  59. package/lib/engine/mem-contract/family.d.ts +25 -0
  60. package/lib/engine/mem-contract/index.d.ts +8 -0
  61. package/lib/engine/mem-contract/log.d.ts +29 -0
  62. package/lib/engine/mem-contract/remote.d.ts +80 -0
  63. package/lib/engine/mem-contract/text.d.ts +92 -0
  64. package/lib/engine/mem-contract/tools.d.ts +284 -0
  65. package/lib/engine/mem-contract/types.d.ts +615 -0
  66. package/lib/engine/mem-retrieval/adapters/auto_vstore.d.ts +47 -0
  67. package/lib/engine/mem-retrieval/adapters/hnswlib.d.ts +174 -0
  68. package/lib/engine/mem-retrieval/adapters/local_bge.d.ts +121 -0
  69. package/lib/engine/mem-retrieval/adapters/local_numpy.d.ts +28 -0
  70. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +52 -0
  71. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +10 -0
  72. package/lib/engine/mem-retrieval/adapters/warm_gate.d.ts +46 -0
  73. package/lib/engine/mem-retrieval/budget.d.ts +30 -0
  74. package/lib/engine/mem-retrieval/fusion.d.ts +53 -0
  75. package/lib/engine/mem-retrieval/index.d.ts +14 -0
  76. package/lib/engine/mem-retrieval/interfaces.d.ts +82 -0
  77. package/lib/engine/mem-retrieval/log.d.ts +11 -0
  78. package/lib/engine/mem-retrieval/registry.d.ts +23 -0
  79. package/lib/engine/mem-retrieval/rerank.d.ts +13 -0
  80. package/lib/engine/mem-retrieval/stats.d.ts +64 -0
  81. package/lib/engine/mem-retrieval/text_budget.d.ts +52 -0
  82. package/lib/index.js +1773 -473
  83. package/lib/interface-version.json +2 -2
  84. package/lib/types/client/domains.d.ts +5 -4
  85. package/lib/types/client/domains.d.ts.map +1 -1
  86. package/lib/types/client/index.d.ts.map +1 -1
  87. package/lib/types/client/ingest.d.ts +1 -1
  88. package/lib/types/client/wire.d.ts +36 -0
  89. package/lib/types/client/wire.d.ts.map +1 -0
  90. package/lib/types/envinit-bootstrap.d.ts +3 -2
  91. package/lib/types/envinit.d.ts +2 -19
  92. package/lib/types/envinit.d.ts.map +1 -1
  93. package/lib/types/hints.d.ts +1 -1
  94. package/lib/types/index.d.ts +8 -3
  95. package/lib/types/index.d.ts.map +1 -1
  96. package/lib/types/open.d.ts +61 -11
  97. package/lib/types/open.d.ts.map +1 -1
  98. package/lib/types/prompt.d.ts +19 -1
  99. package/lib/types/prompt.d.ts.map +1 -1
  100. package/lib/types/provision.d.ts +1 -1
  101. package/lib/types/provision.d.ts.map +1 -1
  102. package/lib/types/reconcile.d.ts +64 -0
  103. package/lib/types/reconcile.d.ts.map +1 -0
  104. package/lib/types/remote.d.ts +53 -0
  105. package/lib/types/remote.d.ts.map +1 -1
  106. package/lib/types/render.d.ts +35 -0
  107. package/lib/types/render.d.ts.map +1 -0
  108. package/lib/types/tool_schema.d.ts +3 -13
  109. package/lib/types/tool_schema.d.ts.map +1 -1
  110. package/package.json +24 -15
  111. package/lib/client.js.map +0 -1
  112. package/lib/types/client/domains.js +0 -25
  113. package/lib/types/client/domains.js.map +0 -1
  114. package/lib/types/client/index.js +0 -1371
  115. package/lib/types/client/index.js.map +0 -1
  116. package/lib/types/client/ingest.js +0 -30
  117. package/lib/types/client/ingest.js.map +0 -1
  118. package/lib/types/client/paging.js +0 -36
  119. package/lib/types/client/paging.js.map +0 -1
  120. package/lib/types/client/summarize.js +0 -79
  121. package/lib/types/client/summarize.js.map +0 -1
  122. package/lib/types/data_home.js +0 -26
  123. package/lib/types/data_home.js.map +0 -1
  124. package/lib/types/envinit-bootstrap.js +0 -333
  125. package/lib/types/envinit.js +0 -402
  126. package/lib/types/envinit.js.map +0 -1
  127. package/lib/types/hints.js +0 -53
  128. package/lib/types/hints.js.map +0 -1
  129. package/lib/types/index.js +0 -755
  130. package/lib/types/index.js.map +0 -1
  131. package/lib/types/inject.js +0 -14
  132. package/lib/types/inject.js.map +0 -1
  133. package/lib/types/interface_gate.js +0 -84
  134. package/lib/types/interface_gate.js.map +0 -1
  135. package/lib/types/open.js +0 -133
  136. package/lib/types/open.js.map +0 -1
  137. package/lib/types/prompt.js +0 -148
  138. package/lib/types/prompt.js.map +0 -1
  139. package/lib/types/provision.js +0 -177
  140. package/lib/types/provision.js.map +0 -1
  141. package/lib/types/remote.js +0 -247
  142. package/lib/types/remote.js.map +0 -1
  143. package/lib/types/tool_schema.js +0 -149
  144. package/lib/types/tool_schema.js.map +0 -1
@@ -1,755 +0,0 @@
1
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
- var useValue = arguments.length > 2;
3
- for (var i = 0; i < initializers.length; i++) {
4
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
- }
6
- return useValue ? value : void 0;
7
- };
8
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
- var _, done = false;
14
- for (var i = decorators.length - 1; i >= 0; i--) {
15
- var context = {};
16
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
- if (kind === "accessor") {
21
- if (result === void 0) continue;
22
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
- if (_ = accept(result.get)) descriptor.get = _;
24
- if (_ = accept(result.set)) descriptor.set = _;
25
- if (_ = accept(result.init)) initializers.unshift(_);
26
- }
27
- else if (_ = accept(result)) {
28
- if (kind === "field") initializers.unshift(_);
29
- else descriptor[key] = _;
30
- }
31
- }
32
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
- done = true;
34
- };
35
- import { existsSync } from 'node:fs';
36
- import { join } from 'node:path';
37
- import { buildPromptSections, promptFileSpecs, promptTextWarnings } from './prompt.js';
38
- import { hintText, messageText } from './hints.js';
39
- import z from '@deepseek-ai/schemastery';
40
- import { defineTool } from '@deepseek-ai/dsh-tools';
41
- import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
42
- import { buildRuntime, dispatchToolKey, provisionToolchainAsync, supportsToolKey, warmSemanticAsync, warmTokenizerAsync, } from '@avantf/mem';
43
- import { REMEMBER_TOOL, RECALL_TOOL, ADMIN_TOOL, KB_TOOL, QUERY_TOOL, TOOL_SPECS, createConsoleLogger, describeError, modelFacingToolResult, toolOk, toolErr, validationError, remoteErrorText, } from '@avantf/mem-contract';
44
- import { hostContribution } from './remote.js';
45
- import { configDataHome } from './data_home.js';
46
- import { provision, registerCompatMegaphone, verifyRegisteredFaces } from './provision.js';
47
- import { managedModelSpec, MODEL_ITEM, PANDOC_ITEM, loadEnvinit, } from './envinit.js';
48
- import { parseToolsConfig, resolveToolsDir } from '@avantf/mem-provision';
49
- import { resetPandocResolution, setPandocProvisioning } from '@avantf/mem-convert';
50
- import { openDocumentPath } from './open.js';
51
- import { browseDirectory, classifySource } from '@avantf/mem';
52
- import { flattenToolSpec } from './tool_schema.js';
53
- export const name = 'avantf-mem';
54
- export { inject } from './inject.js';
55
- // The section text lives in its own module so it is testable without this entry's harness imports.
56
- export { KNOWLEDGE_PROMPT_SECTION, MEMORY_PROMPT_SECTION } from './prompt.js';
57
- export const Config = z.object({
58
- mode: z.union([z.const('cordis'), z.const('mcp')]).default('cordis'),
59
- // Deliberately NO default: this is a CONFIGURED value (layer ②), so `$AVANTF_HOME` (④) has to be
60
- // able to outrank it — and a literal `~/.avantf` default would make "unset" indistinguishable from
61
- // "configured as the default", which is how merely having a default used to kill the env var. Left
62
- // unset, resolution falls through ④ to the default (see `configDataHome`).
63
- dataHome: z.string(),
64
- });
65
- /**
66
- * Facts whose entity/triple rows the heartbeat may rebuild per beat.
67
- *
68
- * Tagging is ~0.02 ms for an ordinary fact (nodejieba, measured), so this batch is tens of
69
- * milliseconds on a quiet moment — small enough for a 60-minute heartbeat and large enough to
70
- * adopt a whole corpus over a few beats after an extraction change.
71
- */
72
- const ENTITY_SWEEP_PER_HEARTBEAT = 2000;
73
- /** Minimum spacing between corpus drift checks (`tools/result` fires for EVERY tool). */
74
- const RECONCILE_MIN_INTERVAL_MS = 2000;
75
- // ─── dispatch: validate with the contract schema, never leak a raw exception ──────────────────
76
- //
77
- // The key → runtime table is the ENGINE's (`@avantf/mem`'s `dispatchToolKey`), shared with the MCP
78
- // server. It used to be duplicated here, and the copy that drifted was the MCP one: the four `kb_*`
79
- // tools were advertised to the model and answered `unknown tool key` on every call.
80
- async function runToolSpec(spec, rt, args) {
81
- const parsed = spec.input.safeParse(args ?? {});
82
- if (!parsed.success) {
83
- return validationError(spec.name, parsed.error.issues);
84
- }
85
- if (!supportsToolKey(spec.key))
86
- return toolErr(`unknown tool ${spec.key}`);
87
- try {
88
- const value = await dispatchToolKey(rt, spec.key, parsed.data);
89
- // Uniform agent-facing envelope, identical to the MCP surface (`ToolEnvelope`):
90
- // every success is `{ok:true,result}` regardless of the payload's shape.
91
- // `mem_admin`'s fact views lose the retention diagnostics here; the Remote gateway that
92
- // drives the 记忆/知识 tabs calls the same table and keeps them.
93
- const shaped = modelFacingToolResult(spec.key, parsed.data.action, value);
94
- return toolOk(shaped);
95
- }
96
- catch (error) {
97
- return toolErr(error);
98
- }
99
- }
100
- const OUTPUT = {
101
- schema: { type: 'object', additionalProperties: true },
102
- render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
103
- };
104
- function present(spec, args) {
105
- return { card: 'generic', title: spec.name, kind: 'other', rawInput: args };
106
- }
107
- /** Drop wire keys explicitly set to undefined so they cannot clobber defaults. */
108
- function defined(args) {
109
- return Object.fromEntries(Object.entries(args ?? {}).filter(([, value]) => value !== undefined));
110
- }
111
- /**
112
- * Pass-through gateway mirroring the five agent tools. Every payload is
113
- * re-validated with the contract zod union before dispatch (same guarantee the
114
- * tools give the model), and results are wrapped in the contract's
115
- * `{ok:true,value}` / `{ok:false,error,violations}` RemoteEnvelope the client decodes.
116
- */
117
- let AvantfMemGateway = (() => {
118
- let _classSuper = TypertRemoteService;
119
- let _instanceExtraInitializers = [];
120
- let _remember_decorators;
121
- let _recall_decorators;
122
- let _admin_decorators;
123
- let _kb_decorators;
124
- let _query_decorators;
125
- let _classifySource_decorators;
126
- let _browseDir_decorators;
127
- let _kbDomains_decorators;
128
- let _kbAddDomain_decorators;
129
- let _openDoc_decorators;
130
- return class AvantfMemGateway extends _classSuper {
131
- static {
132
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
133
- _remember_decorators = [Remote('remember')];
134
- _recall_decorators = [Remote('recall')];
135
- _admin_decorators = [Remote('admin')];
136
- _kb_decorators = [Remote('kb')];
137
- _query_decorators = [Remote('query')];
138
- _classifySource_decorators = [Remote('classifySource')];
139
- _browseDir_decorators = [Remote('browseDir')];
140
- _kbDomains_decorators = [Remote('kbDomains')];
141
- _kbAddDomain_decorators = [Remote('kbAddDomain')];
142
- _openDoc_decorators = [Remote('openDoc')];
143
- __esDecorate(this, null, _remember_decorators, { kind: "method", name: "remember", static: false, private: false, access: { has: obj => "remember" in obj, get: obj => obj.remember }, metadata: _metadata }, null, _instanceExtraInitializers);
144
- __esDecorate(this, null, _recall_decorators, { kind: "method", name: "recall", static: false, private: false, access: { has: obj => "recall" in obj, get: obj => obj.recall }, metadata: _metadata }, null, _instanceExtraInitializers);
145
- __esDecorate(this, null, _admin_decorators, { kind: "method", name: "admin", static: false, private: false, access: { has: obj => "admin" in obj, get: obj => obj.admin }, metadata: _metadata }, null, _instanceExtraInitializers);
146
- __esDecorate(this, null, _kb_decorators, { kind: "method", name: "kb", static: false, private: false, access: { has: obj => "kb" in obj, get: obj => obj.kb }, metadata: _metadata }, null, _instanceExtraInitializers);
147
- __esDecorate(this, null, _query_decorators, { kind: "method", name: "query", static: false, private: false, access: { has: obj => "query" in obj, get: obj => obj.query }, metadata: _metadata }, null, _instanceExtraInitializers);
148
- __esDecorate(this, null, _classifySource_decorators, { kind: "method", name: "classifySource", static: false, private: false, access: { has: obj => "classifySource" in obj, get: obj => obj.classifySource }, metadata: _metadata }, null, _instanceExtraInitializers);
149
- __esDecorate(this, null, _browseDir_decorators, { kind: "method", name: "browseDir", static: false, private: false, access: { has: obj => "browseDir" in obj, get: obj => obj.browseDir }, metadata: _metadata }, null, _instanceExtraInitializers);
150
- __esDecorate(this, null, _kbDomains_decorators, { kind: "method", name: "kbDomains", static: false, private: false, access: { has: obj => "kbDomains" in obj, get: obj => obj.kbDomains }, metadata: _metadata }, null, _instanceExtraInitializers);
151
- __esDecorate(this, null, _kbAddDomain_decorators, { kind: "method", name: "kbAddDomain", static: false, private: false, access: { has: obj => "kbAddDomain" in obj, get: obj => obj.kbAddDomain }, metadata: _metadata }, null, _instanceExtraInitializers);
152
- __esDecorate(this, null, _openDoc_decorators, { kind: "method", name: "openDoc", static: false, private: false, access: { has: obj => "openDoc" in obj, get: obj => obj.openDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
153
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
154
- }
155
- rt = __runInitializers(this, _instanceExtraInitializers);
156
- constructor(ctx, rt) {
157
- super(ctx, 'avantfMem');
158
- this.rt = rt;
159
- }
160
- /**
161
- * Run one already-validated tool call and wrap the answer in the contract envelope.
162
- *
163
- * `T` is the payload the tool key is DECLARED to answer, spelled at each `@Remote` below; this used
164
- * to be `unknown` everywhere, which made the host half the one seam in the type chain where nothing
165
- * said what crosses the wire. The single `as T` is unavoidable and is the honest part: the dispatch
166
- * table is keyed by `ToolSpec.key` (a `string`), so its result is `unknown` by construction. Naming
167
- * the type is what turns "the client assumed a shape" into "the engine's own type is what the host
168
- * claims to send".
169
- */
170
- async call(key, spec, payload) {
171
- const parsed = spec.input.safeParse(payload);
172
- if (!parsed.success) {
173
- // Same message + violations format the agent tools return (contract helper).
174
- return validationError(spec.name, parsed.error.issues);
175
- }
176
- try {
177
- const value = await dispatchToolKey(this.rt, key, parsed.data);
178
- return { ok: true, value: (value ?? null) };
179
- }
180
- catch (error) {
181
- return { ok: false, error: remoteErrorText(error) };
182
- }
183
- }
184
- /**
185
- * Each payload is written as the ENGINE type that produces it (`StoreResult<…>`, or a contract
186
- * interface), never as a hand-copied shape: if a store method's result changes, the declaration
187
- * here stops compiling instead of quietly misdescribing the wire.
188
- */
189
- remember(args) {
190
- const payload = defined(args);
191
- return this.call('remember', REMEMBER_TOOL, { action: 'add', ...payload });
192
- }
193
- recall(args) {
194
- const payload = defined(args);
195
- return this.call('recall', RECALL_TOOL, { action: 'search', ...payload });
196
- }
197
- admin(args) {
198
- const payload = defined(args);
199
- return this.call('admin', ADMIN_TOOL, { action: 'list', ...payload });
200
- }
201
- /**
202
- * The one method that keeps `unknown`, and deliberately: its payload is whichever of the seven
203
- * corpus actions the caller asked for (a page, one document, an ingest report, a conflict report, a
204
- * reindex plan …), so there is no single engine type to name. The engine's `KbDispatch` spells all
205
- * seven out; the client names its expectation at each call site (`callRemote<…>(…)`).
206
- */
207
- kb(args) {
208
- // No safe default action for kb — a missing action fails contract validation
209
- // with an explicit violation instead of guessing.
210
- return this.call('kb', KB_TOOL, defined(args));
211
- }
212
- query(args) {
213
- return this.call('query', QUERY_TOOL, defined(args));
214
- }
215
- /**
216
- * UI-only: what IS this string? Answered against the ingestion boundary, so the UI can label the
217
- * input and pick the right `kb_manage` action (URL → fetch, file → ingest, directory → import,
218
- * text → ingest the string itself, missing → refuse and say why).
219
- */
220
- async classifySource(args) {
221
- try {
222
- const payload = defined(args);
223
- const text = typeof payload.text === 'string' ? payload.text : '';
224
- return { ok: true, value: classifySource(text, this.rt.config.knowledge.ingest) };
225
- }
226
- catch (error) {
227
- return { ok: false, error: remoteErrorText(error) };
228
- }
229
- }
230
- /** UI-only: one directory listing for the 选择 picker (same boundary as ingestion). */
231
- async browseDir(args) {
232
- try {
233
- const payload = defined(args);
234
- const path = typeof payload.path === 'string' && payload.path !== '' ? payload.path : undefined;
235
- return { ok: true, value: browseDirectory(path, this.rt.config.knowledge.ingest) };
236
- }
237
- catch (error) {
238
- return { ok: false, error: remoteErrorText(error) };
239
- }
240
- }
241
- /**
242
- * UI-only: the 知识页 domain picker's options — the configured `knowledge.domains` allowlist
243
- * together with the domains the library already holds — plus whether the allowlist is active
244
- * (non-empty). The host is the only side that knows either half.
245
- */
246
- async kbDomains(_args) {
247
- try {
248
- return { ok: true, value: this.rt.knowledge.domainCatalog() };
249
- }
250
- catch (error) {
251
- return { ok: false, error: remoteErrorText(error) };
252
- }
253
- }
254
- /**
255
- * UI-only: add one 知识域 to the store config's `domains` allowlist AND to the live set, so the
256
- * 知识 tab's 「+」 works without a restart. No agent tool mirrors this on purpose — the model must
257
- * stay bounded by the configured list, while the user can widen it.
258
- */
259
- async kbAddDomain(args) {
260
- try {
261
- const payload = defined(args);
262
- const domain = typeof payload.domain === 'string' ? payload.domain : '';
263
- return { ok: true, value: this.rt.knowledge.addDomain(domain) };
264
- }
265
- catch (error) {
266
- return { ok: false, error: remoteErrorText(error) };
267
- }
268
- }
269
- /**
270
- * UI-only: hand one managed document file — or the directory that holds it — to the user's
271
- * editor. No agent tool mirrors this, so it carries its own envelope instead of going through
272
- * `dispatchToolKey`; the path is resolved from `doc_id` here, so nothing the client sends is
273
- * trusted.
274
- */
275
- async openDoc(args) {
276
- try {
277
- const payload = defined(args);
278
- const docId = typeof payload.doc_id === 'number' ? payload.doc_id : undefined;
279
- if (docId === undefined)
280
- return { ok: false, error: 'openDoc 需要 doc_id' };
281
- const path = this.rt.knowledge.docFilePath(docId);
282
- if (path === null)
283
- return { ok: false, error: `文档 #${String(docId)} 不存在` };
284
- const target = payload.target === 'dir' ? 'dir' : 'file';
285
- if (target === 'file' && !existsSync(path)) {
286
- return { ok: false, error: `受管文件不存在:${path}(重新摄入会重建它)` };
287
- }
288
- const outcome = openDocumentPath(path, target, this.rt.config.knowledge.open.editor);
289
- return { ok: true, value: outcome };
290
- }
291
- catch (error) {
292
- return { ok: false, error: remoteErrorText(error) };
293
- }
294
- }
295
- };
296
- })();
297
- export { AvantfMemGateway };
298
- /**
299
- * Startup/lifecycle logging sink.
300
- *
301
- * DSH's `ctx.logger` currently has only a buffered exporter, so lines written
302
- * through it never reach the terminal. Startup diagnostics therefore go to
303
- * stderr (the CLI/MCP convention, keeping stdout free for data) and are mirrored
304
- * into the host logger for any in-app log surface.
305
- */
306
- function hostLogger(ctx) {
307
- const local = createConsoleLogger('[avantf-mem]');
308
- const host = ctx.logger;
309
- return {
310
- info: (message) => { local.info(message); host.info(message); },
311
- warn: (message) => { local.warn(message); host.warn(message); },
312
- error: (message) => { local.error(message); host.error(message); },
313
- };
314
- }
315
- /**
316
- * Whether this plugin's fiber is still alive after the environment await.
317
- *
318
- * The environment gate can take the whole startup budget (15 s on a first run) and the framework may
319
- * even fetch the gate's base during it. A profile reload or unload landing inside that window clears
320
- * the fiber's uid, after which every Cordis call throws `INACTIVE_EFFECT` — the plugin would then
321
- * half-register (the Remote face cannot be rolled back) instead of stopping cleanly. An unknown
322
- * shape is "cannot tell", so only an explicitly cleared uid counts as disposed.
323
- */
324
- function stillActive(ctx) {
325
- const fiber = ctx.fiber;
326
- return fiber === undefined || fiber.uid !== null;
327
- }
328
- /**
329
- * Declare the expensive resources through the family framework, warm the tokenizer and the embedding
330
- * model, and continue initialisation as each item settles.
331
- *
332
- * Everything here is fire-and-forget: `background` items never occupy the startup budget, and the
333
- * callbacks ARE the "continue once the resource is there" step (framework DESIGN §7.2 step 5).
334
- *
335
- * **What the framework owns here.** `mem:pandoc` and `mem:model` are framework items. The embedding
336
- * model is a `model-cache` in the framework's `flat` layout, so its files land at
337
- * `<home>/models/<repo>/<file>` — the tree `semantic.cache_dir` resolves to (the family root, via
338
- * `managedRoots`) and the shape `@huggingface/transformers` reads under `env.cacheDir`. The runtime
339
- * therefore loads the framework's copy instead of fetching a second one.
340
- *
341
- * The model is NOT declared when the runtime's cache dir is somewhere else (`AVANTF_MEM_MODEL_CACHE`
342
- * — `managedModelSpec` decides): the framework's root would then collect a copy nothing reads. In
343
- * that case, and whenever no item will warm the model, the warm is started here directly.
344
- *
345
- * Outcomes handled below:
346
- *
347
- * - `mem:model` settles (present/installed) → warm the semantic path from the managed root, now
348
- * that the files are on disk. That ordering is the whole point: the runtime's constructor warm is
349
- * deferred while the framework is up (`deferWarm`), so nothing can race the install, and the
350
- * explicit warm here is what starts the model.
351
- * - `mem:model` failed/skipped → one WARNING, then STILL warm: with `semantic.auto_download` on the
352
- * runtime takes its own download path, and with it off the semantic leg degrades to FTS+entity.
353
- * Either way the mount is untouched.
354
- * - `mem:pandoc` unavailable → point the converter back at the pre-migration tools directory, so an
355
- * already-installed copy (or one on PATH) keeps document ingestion working.
356
- * - anything failed/skipped → one WARNING, with the framework's stable reason code.
357
- *
358
- * @param env - the loaded framework runtime.
359
- * @param rt - the built engine runtime (its resolved config drives the specs).
360
- * @param logger - the plugin's logger.
361
- * @param waitFor - the host's readiness signal, for the tokenizer warm.
362
- */
363
- function startManagedResources(env, rt, logger, waitFor) {
364
- const { semantic, tools } = rt.config.common;
365
- // `undefined` in two cases: the semantic backend is not a local model, or the runtime reads models
366
- // from an operator override instead of the framework root (see `managedModelSpec`).
367
- const model = managedModelSpec(semantic, env.roots.models);
368
- const plan = {
369
- pandoc: tools.auto_install,
370
- archiveMirrors: tools.mirror,
371
- // The item is still declared when downloads are off — its `skipped (policy/download-disabled)`
372
- // line is the diagnosis — but the framework then probes disk only, never the network.
373
- modelAutoDownload: semantic.auto_download,
374
- model,
375
- };
376
- // The tokenizer needs no provisioning — nodejieba is an optional npm dependency — so it warms
377
- // behind the same host-readiness signal the legacy sweep used.
378
- warmTokenizerAsync(rt, { ...(waitFor === undefined ? {} : { waitFor }) });
379
- if (model === undefined && semantic.backend === 'local_bge') {
380
- // No `mem:model` item owns the model: nobody will call back, so warm from wherever the runtime
381
- // reads (the `AVANTF_MEM_MODEL_CACHE` directory). `semantic.auto_download` decides whether a
382
- // missing cache is fetched or degrades.
383
- warmSemanticAsync(rt);
384
- }
385
- env.provisionResources(plan, (entry) => {
386
- if (entry.id === MODEL_ITEM) {
387
- // The model files are (or are not) on disk now; only now may the runtime look. On success it
388
- // reads `<repo>/<file>` from the managed root; on failure it takes its own download or degrades
389
- // to FTS+entity. Warming on both paths is deliberate — the warning below is the diagnosis.
390
- warmSemanticAsync(rt);
391
- }
392
- const unavailable = entry.action === 'failed' || entry.action === 'skipped';
393
- if (unavailable) {
394
- logger.warn(`envinit: ${entry.id} is ${entry.action} (${String(entry.code)}${entry.reason === undefined ? '' : ` — ${entry.reason}`})`);
395
- if (entry.id === PANDOC_ITEM) {
396
- // No legacy directory to fall back TO any more: the resolved managed root is the family
397
- // root, so this is simply "the converter may install/read it itself".
398
- const fallback = resolveToolsDir(parseToolsConfig(rt.config.common.tools));
399
- logger.warn(`envinit: pandoc unavailable from the framework; the converter will use ${fallback} (override with tools.dir or AVANTF_PANDOC)`);
400
- setPandocProvisioning({
401
- toolsDir: fallback,
402
- mirror: rt.config.common.tools.mirror,
403
- autoInstall: rt.config.common.tools.auto_install,
404
- logger,
405
- });
406
- resetPandocResolution();
407
- }
408
- return;
409
- }
410
- logger.info(`envinit: ${entry.id} ${entry.action} (${entry.source}${entry.version === undefined ? '' : ` ${entry.version}`})`);
411
- });
412
- }
413
- export async function apply(ctx, config) {
414
- const logger = hostLogger(ctx);
415
- // ── environment initialisation — FIRST, before the runtime allocates anything ────────────────
416
- // The family base's fixed sequence (its DESIGN §7.2): bootstrap (inlined) → load
417
- // `@avantf/dsh-plugin-base` → derive the compatibility gate from the loaded module → declare the
418
- // remaining resources. The check runs before anything is registered, so "do not load" costs
419
- // nothing to unwind.
420
- //
421
- // The base (`@avantf/dsh-plugin-base`, whose `compat` module used to be `@avantf/dsh-compat`) IS
422
- // the framework the bootstrap loads, and it brings the gate with it: no `mem:compat` item and no
423
- // managed copy to download. The plugin no longer carries its own self-heal through
424
- // `@avantf/mem-provision`, whose private root the base deliberately does not read. When the base
425
- // itself is unavailable the plugin keeps mounting (see below) and falls back to its legacy
426
- // provisioning path, whose default directories are the pre-migration ones.
427
- //
428
- // When the base cannot be obtained at all we do NOT refuse: a `compat:` WARNING is logged and the
429
- // plugin mounts normally (tools/service/prompt/remote all register), because the gate is a safety
430
- // net and "cannot tell" is not "incompatible". Only a PROVEN break (`probe-failed`) refuses.
431
- const env = await loadEnvinit({ log: logger });
432
- // The await above can last the whole startup budget (15 s on a first run). A profile reload or
433
- // unload during it clears this fiber's uid, and every later Cordis call — `ctx.effect`, `ctx.on`,
434
- // `ctx.tools.register` — then throws `INACTIVE_EFFECT`. Re-assert liveness and stop cleanly here
435
- // rather than half-registering (the Remote face cannot be rolled back).
436
- if (!stillActive(ctx)) {
437
- logger.warn('unmounted while preparing the environment; stopping before registering anything');
438
- return;
439
- }
440
- const compat = env?.compat;
441
- let verdict;
442
- if (compat === undefined) {
443
- logger.warn('plugin mount: the dsh compatibility gate is ABSENT (@avantf/dsh-plugin-base unavailable — see the WARNING above); mounting the full plugin anyway');
444
- }
445
- else {
446
- const run = provision(ctx, logger, compat);
447
- if (!run.verdict.load) {
448
- registerCompatMegaphone(ctx, run.verdict, logger, compat);
449
- logger.warn('plugin not loaded: the host dsh API is incompatible (compat check above) — nothing was registered; rebuild with `pnpm build:dsh`');
450
- return;
451
- }
452
- verdict = run.verdict;
453
- }
454
- // The model-facing set comes from the contract's `TOOL_SPECS` — ONE list, so adding or splitting a
455
- // tool cannot leave this file behind. (It WAS hardcoded here, and that is exactly what happened:
456
- // splitting `kb_manage` into `kb_add`/`kb_list`/`kb_remove`/`kb_reindex` left the plugin still
457
- // registering the old five until this line was found.)
458
- const toolSpecs = TOOL_SPECS;
459
- if (config.mode === 'mcp') {
460
- // The MCP entry is a standalone stdio process (@avantf/mem-mcp); an in-process
461
- // DSH plugin cannot own the host's stdio. Warn and mount the cordis toolset.
462
- logger.warn('config mode=mcp is not served in-process — mounting the cordis toolset; run @avantf/mem-mcp standalone for MCP');
463
- }
464
- // Resolved through the CONFIG layer, not used as an explicit value: `$AVANTF_HOME` outranks the
465
- // profile's `dataHome`, the same way it outranks the work plugin's (see `data_home.ts`).
466
- const dataHome = configDataHome(config.dataHome);
467
- // Both halves are logged: what was configured, and where it actually landed — the runtime's own
468
- // init line repeats the latter, but a reader of this line should not have to join two log lines to
469
- // find out that a configured value was overridden by the environment.
470
- logger.info(`plugin mount: mode=${config.mode ?? 'cordis'} dataHome=${dataHome}`
471
- + ` (configured ${config.dataHome ?? 'unset'})`);
472
- let rt;
473
- try {
474
- rt = buildRuntime({
475
- dataHome,
476
- logger,
477
- // The family framework owns pandoc and the embedding model when it is up: its managed roots
478
- // become this process's built-in defaults, so the converter and the semantic backend look
479
- // where the framework publishes. A `tools.dir` / `semantic.cache_dir` in config.yaml (or an
480
- // `AVANTF_*` escape hatch) still wins — the framework roots are only layer ①.
481
- ...(env === undefined ? {} : { managedRoots: { tools: env.roots.tools, models: env.roots.models } }),
482
- });
483
- }
484
- catch (error) {
485
- // A memory plugin must not be able to stop the host from booting. Everything that can fail here
486
- // is environmental — a corrupt or locked database, an unwritable data home, or a database
487
- // written by a NEWER version (`SchemaDowngradeError`) — and the loader treats a throwing `apply`
488
- // as a failed row, which is enough to fail the whole `dsh web` boot (observed). Degrade instead:
489
- // the five tools stay registered and each answers with the REASON, so the model and the operator
490
- // get a diagnosis instead of a missing tool and a silent absence.
491
- const reason = describeError(error);
492
- logger.error(`runtime unavailable — mounting DEGRADED (tools answer with the reason): ${reason}`);
493
- const message = `memory unavailable: ${reason}`;
494
- for (const spec of toolSpecs) {
495
- ctx.tools.register(defineTool({
496
- name: spec.name,
497
- description: spec.description,
498
- parameters: flattenToolSpec(spec),
499
- output: OUTPUT,
500
- execute: () => Promise.resolve(toolErr(message)),
501
- presentCall: (args) => present(spec, args),
502
- }));
503
- logger.info(`tool registered (DEGRADED): ${spec.name}`);
504
- }
505
- logger.warn('client tabs unavailable: no runtime to serve the `avantfMem` remote (fix the error above, then restart)');
506
- return;
507
- }
508
- ctx.provide('avantfMemory', rt);
509
- logger.info('service registered: avantfMemory');
510
- // ONE initialization path for everything this plugin must PROVIDE: the pandoc binary every
511
- // document conversion goes through, and the embedding model + tokenizer warm-up.
512
- //
513
- // WHICH mechanism does the fetching is decided by whether the family framework came up:
514
- //
515
- // - framework available → `mem:pandoc` and `mem:model` are declared as BACKGROUND items and
516
- // dispatched now; the plugin keeps initialising and continues on each `onSettled`. Nothing here
517
- // waits for a 35 MB archive or a ~90 MB model. The engine's managed roots were already pointed
518
- // at the family root above, so the semantic backend reads the very tree the framework fills,
519
- // and the warm starts from the model item's settle callback rather than racing it.
520
- // - framework unavailable → the legacy `@avantf/mem-provision` sweep, over the pre-migration
521
- // default directories, exactly as before.
522
- //
523
- // nodejieba's dictionary parse is ~1.2 s of SYNCHRONOUS main-thread work, so it must not run
524
- // while the host is still booting: measured, `dsh web` printed its URL line 0.09 s after our
525
- // "tokenizer ready" line, i.e. the parse was the last thing gating startup. The host's own
526
- // readiness signal is its loader tree settling (`loader.await()` — what the web app awaits before
527
- // printing the URL); both paths warm the tokenizer behind it.
528
- const loader = ctx.get('loader');
529
- const waitFor = loader?.await === undefined ? undefined : loader.await();
530
- if (env !== undefined) {
531
- startManagedResources(env, rt, logger, waitFor);
532
- }
533
- else {
534
- // The verdict the front gate JUST computed is handed to the sweep, whose first step reads `load`
535
- // (see `provisionToolchainAsync`): an incompatible host provisions NOTHING. Dispatched, not
536
- // awaited. Omitted when the gate could not run at all (`compat === undefined`), in which case
537
- // "cannot tell" means the sweep runs as usual.
538
- void provisionToolchainAsync(rt, {
539
- ...(waitFor === undefined ? {} : { waitFor }),
540
- ...(verdict === undefined ? {} : { dshCompat: verdict }),
541
- });
542
- }
543
- // Derived state follows the RULES, not the write: a fact written before an extraction change
544
- // keeps the old entity/triple rows (and the HRR bundle derived from them) until a sweep visits
545
- // it. ONE bounded batch per call — the store's in-flight guard drops an overlapping call rather
546
- // than letting two passes select the same rows — and the remainder is picked up next time.
547
- const sweepEntities = () => {
548
- void rt.memory.reindexEntities(ENTITY_SWEEP_PER_HEARTBEAT)
549
- .then((swept) => {
550
- if (swept.rebuilt > 0) {
551
- logger.info(`entity sweep: rebuilt ${swept.rebuilt} fact(s) written by older rules (${swept.deferred} still stale)`);
552
- }
553
- })
554
- .catch((error) => {
555
- logger.warn(`entity sweep failed: ${error instanceof Error ? error.message : String(error)}`);
556
- });
557
- };
558
- // FIRST trigger is here, at mount — not on the first beat. A rule change used to reach old facts
559
- // only after `heartbeat_minutes` (60 by default), and never at all when that was 0.
560
- sweepEntities();
561
- // Trust heartbeat: the active-day clock advances on presence, and the tick performs
562
- // the settle/forget/idle/purge sweeps (TRUST_MODEL.md §5). `0` = startup pass only.
563
- const heartbeatMinutes = rt.config.common.trust.presence.heartbeat_minutes;
564
- if (heartbeatMinutes > 0) {
565
- ctx.effect(() => {
566
- const timer = setInterval(() => {
567
- try {
568
- rt.memory.trustTick();
569
- }
570
- catch (error) {
571
- logger.warn(`trust tick failed: ${error instanceof Error ? error.message : String(error)}`);
572
- }
573
- sweepEntities();
574
- }, heartbeatMinutes * 60_000);
575
- const unref = timer.unref;
576
- if (typeof unref === 'function')
577
- unref.call(timer);
578
- logger.info(`trust heartbeat: every ${heartbeatMinutes}min (active-day clock + lifecycle sweep)`);
579
- return () => clearInterval(timer);
580
- });
581
- }
582
- // Three usage sections, and only usage: when a fact is worth writing / when to read memory before
583
- // answering, when the document library is worth querying, and how to CHANGE a document. Retention mechanics stay out of
584
- // both (the store's business, and the only action such text invites is rewriting a fact to
585
- // refresh it — which defeats the policy it describes); the module-level reasons are on
586
- // MEMORY_PROMPT_SECTION and KNOWLEDGE_PROMPT_SECTION.
587
- //
588
- // The TEXT is the user's to edit: one `.md` per section under `<data home>/prompts`, ensured and
589
- // read by the engine's generic `PromptFiles` on this path only (a degraded mount registers no
590
- // sections at all — see the `catch` above). Read ONCE, here: an edit takes effect on the next
591
- // start, which keeps "what is in the prompt" the same for the whole life of the process.
592
- //
593
- // The directory is SHARED by the family's plugins and each plugin owns its prefix (`mem-*` here,
594
- // `work-*` in the work engine): every avantf plugin's system prompt sits in one place, and no
595
- // plugin has to guess which files are its own.
596
- const promptDir = join(rt.config.home, 'prompts');
597
- // The loader is the BASE's, taken at RUNTIME off the module the bootstrap already loaded: fixing
598
- // or extending the shared prompt-file logic takes one base release, not a plugin rebuild.
599
- // DEGRADATION, when the base (or its kit) is unavailable: the section texts fall back to this
600
- // plugin's OWN built-in defaults — the defaults are this plugin's content, not shared code — and
601
- // nothing is written to disk.
602
- const kit = env?.kit;
603
- const loadedPrompts = kit?.PromptFiles === undefined
604
- ? promptFileSpecs().map((spec) => ({
605
- file: spec.file,
606
- path: join(promptDir, spec.file),
607
- text: spec.fallback,
608
- source: 'default',
609
- wrote: false,
610
- }))
611
- : new kit.PromptFiles({ dir: promptDir, logger }).load(promptFileSpecs());
612
- const promptSections = buildPromptSections(loadedPrompts);
613
- for (const section of promptSections) {
614
- ctx.systemPrompt.section(section);
615
- logger.info(`prompt section registered: ${section.name} (order ${String(section.order)})`);
616
- }
617
- // The defaults are held to hard rules by `prompt_section.spec.ts`; edited text is the user's, so
618
- // it only gets these warnings — injected exactly as written either way.
619
- for (const warning of promptTextWarnings(promptSections))
620
- logger.warn(`prompt text: ${warning}`);
621
- logger.info(`prompt files: ${promptDir} (${loadedPrompts.map((entry) => `${entry.file}:${entry.source}`).join(', ')})`);
622
- // The CONDITIONAL hint, ONE contribution (DESIGN §12). Recomputed synchronously the moment the
623
- // agent is handed a USER message, so it is in place before THIS step's prompt is assembled: the
624
- // provider below cannot await, which is exactly why the probe is the lexical one (`@avantf/mem`'s
625
- // `relevance` — the semantic leg needs an encode and would land a step late).
626
- const hints = new WeakMap();
627
- ctx.on('agent/inbox/inserted', (payload) => {
628
- const agent = payload.agent;
629
- if (agent === undefined)
630
- return;
631
- // Only what the USER sent moves the verdict. The inbox also carries this plugin's own wake-ups
632
- // and other plugins' notices; probing those would let a "工作 n1 已结束" wake rewrite a hint
633
- // that claims to be about the user's last message.
634
- if (payload.message?.source?.kind !== 'user')
635
- return;
636
- const text = messageText(payload.message);
637
- // Never let a probe failure reach the event bus: a missing hint is harmless (the usage
638
- // sections still tell the model to query), a throwing listener is only log noise.
639
- try {
640
- hints.set(agent, text !== '' && rt.relevance(text));
641
- }
642
- catch {
643
- hints.set(agent, false);
644
- }
645
- });
646
- ctx.systemPrompt.context({
647
- name: 'avantf:mem-hint',
648
- order: 130,
649
- // The assembly scope IS the agent object (`dsh-agent`'s `assembleContextFor` passes
650
- // `scope: agent`), so keying by it keeps two sessions from seeing each other's hint. No state
651
- // for this agent (or a false verdict) renders as empty text, which contributes nothing.
652
- text: (context) => {
653
- const hit = context.scope === undefined ? undefined : hints.get(context.scope);
654
- return hit === undefined ? '' : hintText(hit);
655
- },
656
- });
657
- logger.info('prompt context registered: avantf:mem-hint (order 130)');
658
- // Corpus reconciliation, done PRECISELY rather than on a timer or a watcher: the agent reaches
659
- // the corpus through tool calls, so a finished tool call is the moment to ask "did anything under
660
- // `knowledge.docs.dir` change?" — and `corpusDrift()` answers that with one `stat` per document,
661
- // reading nothing. Only the files whose stamp moved get re-ingested, which is what makes this
662
- // cheap enough to run after EVERY tool result.
663
- let reconciling = false;
664
- // THROTTLE, with a trailing run. Drift checks run after EVERY tool result — including other
665
- // plugins' — so on a busy session a full stat pass plus a recursive readdir would fire many times
666
- // a second. Skipping alone would risk dropping the LAST edit (the one that matters), so a
667
- // throttled call schedules exactly one trailing check instead of being discarded.
668
- let lastDriftAt = 0;
669
- let trailingDrift = null;
670
- const reconcile = async (full) => {
671
- if (reconciling)
672
- return;
673
- if (!full) {
674
- const wait = RECONCILE_MIN_INTERVAL_MS - (Date.now() - lastDriftAt);
675
- if (wait > 0) {
676
- if (trailingDrift === null) {
677
- trailingDrift = setTimeout(() => { trailingDrift = null; void reconcile(false); }, wait);
678
- }
679
- return;
680
- }
681
- lastDriftAt = Date.now();
682
- }
683
- reconciling = true;
684
- try {
685
- if (full) {
686
- await rt.knowledge.sync({});
687
- // Take the baseline AFTER the sync: seeding it before would hide an edit that landed in
688
- // between, which is the one window this whole mechanism exists to close.
689
- rt.knowledge.corpusDrift();
690
- return;
691
- }
692
- const drift = rt.knowledge.corpusDrift();
693
- if (drift.changed.length === 0 && drift.missing.length === 0 && !drift.fileSetChanged)
694
- return;
695
- if (drift.fileSetChanged || drift.missing.length > 0) {
696
- // A new, vanished or frontmatter-destroyed `.md` is a corpus-level question (orphan /
697
- // missing), not a doc-level one — so escalate to the full reconcile, which is the surface
698
- // that reports it honestly instead of silently leaving the document un-ingested.
699
- await rt.knowledge.sync({});
700
- return;
701
- }
702
- for (const docId of drift.changed)
703
- await rt.knowledge.sync({ docId });
704
- }
705
- catch (error) {
706
- logger.warn(`知识库自动同步失败(不影响已有内容):${error instanceof Error ? error.message : String(error)}`);
707
- }
708
- finally {
709
- reconciling = false;
710
- }
711
- };
712
- // Once at mount, for whatever changed while the host was not running; then after every tool call.
713
- void reconcile(true);
714
- ctx.on('tools/result', () => {
715
- void reconcile(false);
716
- });
717
- for (const spec of toolSpecs) {
718
- ctx.tools.register(defineTool({
719
- name: spec.name,
720
- description: spec.description,
721
- // Structurally a ParameterSchemaSpec; the derivation lives in `tool_schema.ts`
722
- // so it can be unit-tested without a harness checkout.
723
- parameters: flattenToolSpec(spec),
724
- output: OUTPUT,
725
- execute: args => runToolSpec(spec, rt, args),
726
- presentCall: (args) => present(spec, args),
727
- }));
728
- logger.info(`tool registered: ${spec.name}`);
729
- }
730
- new AvantfMemGateway(ctx, rt); // binder for the client `ctx.remote.avantfMem.*` bridge
731
- logger.info('remote registered: avantfMem (client ctx.remote.avantfMem.*)');
732
- // Strict host wire face: hand-written descriptors registered through the
733
- // documented manual path (see src/remote.ts). Without this the gateway falls
734
- // back to SRC discovery, which needs the generator workspace.
735
- const disposeTypert = ctx.typert.register(hostContribution);
736
- logger.info('typert registered: avantfMem host face (5 tools + 5 UI-only)');
737
- // Post-registration health check: every REAL schema key (and every real tool name) must have
738
- // landed, matched by exact key — never by a bare `list()` total, which a not-yet-withdrawn probe
739
- // could inflate. A failure here is a WARNING only; the plugin is already mounted, so there is no
740
- // clean "do not load" left to take. Skipped when the gate itself could not run (no base).
741
- if (compat !== undefined) {
742
- verifyRegisteredFaces({ ctx, toolNames: toolSpecs.map(spec => spec.name), log: logger, compat });
743
- }
744
- ctx.effect(() => () => {
745
- logger.info('plugin unmount: disposing runtime (closing databases)');
746
- rt.shutdown();
747
- void disposeTypert();
748
- // Release the framework's leases (and drop its per-process cache) with the fiber: a disposed
749
- // plugin must not keep a managed copy marked as in use.
750
- env?.dispose();
751
- logger.info('plugin unmount: runtime disposed');
752
- });
753
- logger.info(`plugin ready: ${toolSpecs.length} tools + avantfMemory service + avantfMem remote`);
754
- }
755
- //# sourceMappingURL=index.js.map