renor-engine 0.0.0-stage → 1.0.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/LICENSE +661 -0
- package/NOTICE +73 -0
- package/README.md +105 -2
- package/dist/BUILD.json +6 -0
- package/dist/api/apiAuth.js +43 -0
- package/dist/api/server.js +172 -0
- package/dist/buildIdentity.js +132 -0
- package/dist/cli-extractors.js +46 -0
- package/dist/cli.js +491 -0
- package/dist/config.js +65 -0
- package/dist/context/composer.js +320 -0
- package/dist/context/readHonesty.js +478 -0
- package/dist/context/rowStore.js +238 -0
- package/dist/conversation/formatter.js +81 -0
- package/dist/conversation/parser.js +189 -0
- package/dist/conversation/types.js +2 -0
- package/dist/conversation/writer.js +42 -0
- package/dist/db/client.js +147 -0
- package/dist/db/indexSalvage.js +81 -0
- package/dist/db/migrations.js +642 -0
- package/dist/db/readCache.js +109 -0
- package/dist/db/schema.js +5 -0
- package/dist/db/statements.js +89 -0
- package/dist/digests/digestCache.js +142 -0
- package/dist/digests/digests.js +597 -0
- package/dist/digests/worker.js +348 -0
- package/dist/embeddings/caching.js +337 -0
- package/dist/embeddings/coalescing.js +177 -0
- package/dist/embeddings/interactive.js +103 -0
- package/dist/embeddings/local.js +68 -0
- package/dist/embeddings/openai.js +105 -0
- package/dist/embeddings/rebuild.js +166 -0
- package/dist/embeddings/simdScan.js +258 -0
- package/dist/embeddings/space.js +21 -0
- package/dist/embeddings/types.js +14 -0
- package/dist/embeddings/vectorIndex.js +675 -0
- package/dist/embeddings/vertex.js +224 -0
- package/dist/eval/artifacts.js +42 -0
- package/dist/eval/cacheReport.js +139 -0
- package/dist/eval/comparison.js +182 -0
- package/dist/eval/context700-candidate.js +145 -0
- package/dist/eval/evalCache.js +525 -0
- package/dist/eval/hashEmbedding.js +35 -0
- package/dist/eval/longmemeval.js +56 -0
- package/dist/eval/metrics.js +181 -0
- package/dist/eval/renorbenchBaseline.js +486 -0
- package/dist/eval/renorbenchDocuments.js +134 -0
- package/dist/eval/renorbenchProduct.js +182 -0
- package/dist/eval/runLedger.js +279 -0
- package/dist/eval/runManifest.js +479 -0
- package/dist/eval/scoring.js +197 -0
- package/dist/evidence/attach.js +267 -0
- package/dist/evidence/background.js +147 -0
- package/dist/evidence/evidenceCache.js +182 -0
- package/dist/evidence/gate.js +37 -0
- package/dist/evidence/keyPool.js +85 -0
- package/dist/evidence/ollamaChat.js +84 -0
- package/dist/evidence/optin.js +165 -0
- package/dist/evidence/options.js +130 -0
- package/dist/evidence/secondPass.js +456 -0
- package/dist/extraction/classifyCapture.js +34 -0
- package/dist/extraction/decision-model.js +174 -0
- package/dist/extraction/eventExtractor.js +113 -0
- package/dist/extraction/extractionClient.js +122 -0
- package/dist/extraction/factExtractor.js +46 -0
- package/dist/extraction/factsWritePath.js +179 -0
- package/dist/extraction/preferenceExtractor.js +153 -0
- package/dist/extraction/secretGuard.js +176 -0
- package/dist/extraction/secretScanner.js +665 -0
- package/dist/extraction/typedFacts.js +224 -0
- package/dist/extraction/writePathAdjudication.js +110 -0
- package/dist/importing/apply.js +644 -0
- package/dist/importing/bundle-write.js +80 -0
- package/dist/importing/cli.js +407 -0
- package/dist/importing/existing.js +49 -0
- package/dist/importing/facts.js +123 -0
- package/dist/importing/forgetImported.js +79 -0
- package/dist/importing/hosted.js +467 -0
- package/dist/importing/index.js +29 -0
- package/dist/importing/journalLog.js +96 -0
- package/dist/importing/markdown.js +98 -0
- package/dist/importing/plan.js +206 -0
- package/dist/importing/secretScreen.js +113 -0
- package/dist/importing/serialize.js +188 -0
- package/dist/importing/source.js +463 -0
- package/dist/importing/types.js +6 -0
- package/dist/indexer/chunker.js +319 -0
- package/dist/indexer/dailyEntries.js +63 -0
- package/dist/indexer/hasher.js +10 -0
- package/dist/indexer/laneRuns.js +48 -0
- package/dist/indexer/o200k.js +481 -0
- package/dist/indexer/o200k.json +8 -0
- package/dist/indexer/outsideRoot.js +77 -0
- package/dist/indexer/parser.js +167 -0
- package/dist/indexer/pipeline.js +1168 -0
- package/dist/indexer/staleness.js +201 -0
- package/dist/indexer/tokenizer.js +77 -0
- package/dist/indexer/walker.js +184 -0
- package/dist/mcp/hive.js +88 -0
- package/dist/mcp/requestErrors.js +47 -0
- package/dist/mcp/server.js +396 -0
- package/dist/memory/agentLease.js +43 -0
- package/dist/memory/confinedWrite.js +315 -0
- package/dist/memory/controls.js +73 -0
- package/dist/memory/dailyNote.js +265 -0
- package/dist/memory/durableAppend.js +57 -0
- package/dist/memory/factConflicts.js +203 -0
- package/dist/memory/factTerms.js +321 -0
- package/dist/memory/hiveNameHeader.cjs +10 -0
- package/dist/memory/inventory.js +55 -0
- package/dist/memory/invisibleText.js +20 -0
- package/dist/memory/lifecycle.js +1756 -0
- package/dist/memory/limits.js +13 -0
- package/dist/memory/requests.js +183 -0
- package/dist/memory/sourceName.js +200 -0
- package/dist/memory/whatsNew.js +158 -0
- package/dist/memory/writer.js +175 -0
- package/dist/memory/writerConflicts.js +85 -0
- package/dist/memorymap/memoryMap.js +1568 -0
- package/dist/memorymap/replacedJournal.js +58 -0
- package/dist/memorymap/replacements.js +405 -0
- package/dist/mvp/activity.js +86 -0
- package/dist/mvp/agentMemory.js +1744 -0
- package/dist/mvp/auth.js +531 -0
- package/dist/mvp/cli.js +73 -0
- package/dist/mvp/client-store.js +462 -0
- package/dist/mvp/compileCache.js +249 -0
- package/dist/mvp/compileSeed.js +27 -0
- package/dist/mvp/config.js +288 -0
- package/dist/mvp/connectionStatus.js +80 -0
- package/dist/mvp/contextRelevance.js +413 -0
- package/dist/mvp/engine.js +1065 -0
- package/dist/mvp/exportAll.js +359 -0
- package/dist/mvp/factLanePack.js +77 -0
- package/dist/mvp/factMarkers.js +49 -0
- package/dist/mvp/factsLane.js +203 -0
- package/dist/mvp/hiddenSpans.js +585 -0
- package/dist/mvp/history.js +413 -0
- package/dist/mvp/hitDates.js +31 -0
- package/dist/mvp/hitFloor.js +36 -0
- package/dist/mvp/identity.js +18 -0
- package/dist/mvp/idle.js +113 -0
- package/dist/mvp/memoryDir.js +98 -0
- package/dist/mvp/posthogMcp.js +210 -0
- package/dist/mvp/readScope.js +104 -0
- package/dist/mvp/readUsefulness.js +222 -0
- package/dist/mvp/sanitize.js +48 -0
- package/dist/mvp/schemaRejections.js +91 -0
- package/dist/mvp/scope.js +127 -0
- package/dist/mvp/server.js +2089 -0
- package/dist/mvp/serverCard.js +29 -0
- package/dist/mvp/storedSize.js +139 -0
- package/dist/mvp/telemetry.js +441 -0
- package/dist/retrieval/adviceLines.js +195 -0
- package/dist/retrieval/adviceQuestion.js +44 -0
- package/dist/retrieval/conversationShape.js +44 -0
- package/dist/retrieval/dailyEntryTime.js +55 -0
- package/dist/retrieval/entityLookup.js +113 -0
- package/dist/retrieval/factsFirst.js +188 -0
- package/dist/retrieval/keyword.js +84 -0
- package/dist/retrieval/ledgerEventTime.js +51 -0
- package/dist/retrieval/ledgerLiveness.js +83 -0
- package/dist/retrieval/ledgerPresentation.js +161 -0
- package/dist/retrieval/noteIdentity.js +57 -0
- package/dist/retrieval/orchestrator.js +1086 -0
- package/dist/retrieval/queryHelpers.js +142 -0
- package/dist/retrieval/questionSpan.js +475 -0
- package/dist/retrieval/readerEvents.js +690 -0
- package/dist/retrieval/readerLayout.js +503 -0
- package/dist/retrieval/relativeTimeHints.js +113 -0
- package/dist/retrieval/semantic.js +67 -0
- package/dist/router/routerCache.js +106 -0
- package/dist/router/sessionRouter.js +726 -0
- package/dist/toolRefusal.js +120 -0
- package/dist/watcher/watcher.js +75 -0
- package/package.json +120 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Vault-staleness guard (D1). The production indexer once served a vault
|
|
3
|
+
// checkout that was 330 commits / 12 days stale and nothing alarmed — the fix
|
|
4
|
+
// is to make staleness a loud, machine-readable signal at startup instead of a
|
|
5
|
+
// silent property of a mis-configured VAULT_PATH.
|
|
6
|
+
//
|
|
7
|
+
// LOCAL-ONLY by design: this module never runs `git fetch` or any network call.
|
|
8
|
+
// It compares HEAD against the ALREADY-FETCHED upstream ref (refs/remotes/*)
|
|
9
|
+
// and HEAD's own commit age. A stale *fetch* is a deployment concern (a cron
|
|
10
|
+
// `git fetch` upstream); a stale *index* at read time is the failure D1
|
|
11
|
+
// shipped, and both signals catch that class.
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.checkVaultStaleness = checkVaultStaleness;
|
|
17
|
+
exports.stalenessMode = stalenessMode;
|
|
18
|
+
exports.stalenessArmed = stalenessArmed;
|
|
19
|
+
exports.stalenessJsonField = stalenessJsonField;
|
|
20
|
+
exports.stalenessGate = stalenessGate;
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
const fs_1 = __importDefault(require("fs"));
|
|
23
|
+
const path_1 = __importDefault(require("path"));
|
|
24
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
function git(cwd, args) {
|
|
26
|
+
try {
|
|
27
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
28
|
+
.toString()
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the upstream ref of the current branch WITHOUT any network I/O:
|
|
37
|
+
* branch.<name>.remote + branch.<name>.merge → refs/remotes/<remote>/<branch>.
|
|
38
|
+
* Detached HEAD or no upstream → null (age signal still applies).
|
|
39
|
+
*/
|
|
40
|
+
function upstreamRef(cwd) {
|
|
41
|
+
const branch = git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
42
|
+
if (!branch || branch === 'HEAD')
|
|
43
|
+
return null;
|
|
44
|
+
const remote = git(cwd, ['config', '--get', `branch.${branch}.remote`]);
|
|
45
|
+
const merge = git(cwd, ['config', '--get', `branch.${branch}.merge`]);
|
|
46
|
+
if (!remote || !merge)
|
|
47
|
+
return null;
|
|
48
|
+
const ref = merge.replace('refs/heads/', `refs/remotes/${remote}/`);
|
|
49
|
+
// The ref must EXIST locally (fetched at some point). A ref that only
|
|
50
|
+
// exists on the remote is invisible here — that is by design.
|
|
51
|
+
if (!git(cwd, ['rev-parse', '--verify', '--quiet', ref]))
|
|
52
|
+
return null;
|
|
53
|
+
return ref;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate vault staleness for the directory the indexer is about to serve.
|
|
57
|
+
* Zero model calls, zero network. Pure filesystem + local git metadata.
|
|
58
|
+
*/
|
|
59
|
+
function checkVaultStaleness(vaultPath, opts = {}) {
|
|
60
|
+
const maxHeadAgeDays = opts.maxHeadAgeDays ?? 3;
|
|
61
|
+
// WHICH checkout is this verdict about? The vault path itself when it is a work-tree root
|
|
62
|
+
// (`vaultPath/.git` exists — a directory, or a file for a linked worktree or submodule),
|
|
63
|
+
// otherwise the ENCLOSING work tree that contains it. A vault that is a subdirectory of a
|
|
64
|
+
// project repo is an ordinary layout, and answering "plain, never alarm" for it would
|
|
65
|
+
// silently serve exactly the stale checkout this guard exists to catch: measured on a
|
|
66
|
+
// 28-day-old repository, its root reported stale while `<root>/notes` reported
|
|
67
|
+
// `{stale:false, kind:'plain'}`.
|
|
68
|
+
//
|
|
69
|
+
// A nested vault is judged by the SAME checkout-level signals as a root one — behind the
|
|
70
|
+
// already-fetched upstream ref, and HEAD commit age — so two vaults inside one repository
|
|
71
|
+
// cannot mean two different things.
|
|
72
|
+
//
|
|
73
|
+
// The enclosing root is often the PLATFORM repository, and that is stated here rather than
|
|
74
|
+
// rediscovered: `engine/` is a subtree of the platform repo, so `git rev-parse
|
|
75
|
+
// --show-toplevel` run from inside `engine/` returns the platform root — which is exactly why
|
|
76
|
+
// `engine/scripts/ci/local.sh` (lines 25-28) and `engine/scripts/eval/run-cycle.sh`
|
|
77
|
+
// (lines 3-6) resolve their own root from `$0` instead of asking git. So a vault under this
|
|
78
|
+
// checkout is judged against the platform checkout, and `repoRoot` records that.
|
|
79
|
+
const isWorkTreeRoot = fs_1.default.existsSync(path_1.default.join(vaultPath, '.git'));
|
|
80
|
+
const repoRoot = isWorkTreeRoot ? vaultPath : git(vaultPath, ['rev-parse', '--show-toplevel']);
|
|
81
|
+
if (!repoRoot) {
|
|
82
|
+
// In no work tree at all: nothing to judge, never alarm on a plain folder.
|
|
83
|
+
return { stale: false, reasons: [], kind: 'plain' };
|
|
84
|
+
}
|
|
85
|
+
const reasons = [];
|
|
86
|
+
const out = { stale: false, reasons: [], kind: 'git', repoRoot };
|
|
87
|
+
// A nested vault is judged against the ENCLOSING checkout, so every reason it produces names
|
|
88
|
+
// that root: a refusal whose banner read only "HEAD is 3 commit(s) behind" would not tell the
|
|
89
|
+
// operator WHICH repository is behind, and `fatal` exits before anything else is printed.
|
|
90
|
+
const scope = repoRoot === vaultPath ? '' : ` (enclosing checkout ${repoRoot})`;
|
|
91
|
+
const head = git(repoRoot, ['rev-parse', 'HEAD']);
|
|
92
|
+
out.head = head ?? undefined;
|
|
93
|
+
if (!head)
|
|
94
|
+
return out; // unreadable repo — do not guess, do not alarm
|
|
95
|
+
const upstream = upstreamRef(repoRoot);
|
|
96
|
+
out.upstream = upstream;
|
|
97
|
+
if (upstream) {
|
|
98
|
+
const aheadBehind = git(repoRoot, ['rev-list', '--left-right', '--count', `${upstream}...${head}`]);
|
|
99
|
+
if (aheadBehind) {
|
|
100
|
+
const [behindStr] = aheadBehind.split(/\s+/);
|
|
101
|
+
const behind = parseInt(behindStr, 10);
|
|
102
|
+
out.behind = behind;
|
|
103
|
+
if (behind > 0) {
|
|
104
|
+
reasons.push(`HEAD is ${behind} commit(s) behind local upstream ref ${upstream}${scope}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const ts = git(repoRoot, ['log', '-1', '--format=%ct', 'HEAD']);
|
|
109
|
+
if (ts) {
|
|
110
|
+
const ageDays = Math.max(0, (Date.now() - parseInt(ts, 10) * 1000) / DAY_MS);
|
|
111
|
+
out.headAgeDays = Math.round(ageDays * 10) / 10;
|
|
112
|
+
if (ageDays > maxHeadAgeDays) {
|
|
113
|
+
reasons.push(`HEAD commit is ${out.headAgeDays}d old (>${maxHeadAgeDays}d) — the checkout may not be receiving updates${scope}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
out.stale = reasons.length > 0;
|
|
117
|
+
out.reasons = reasons;
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/** RENOR_VAULT_STALENESS=off|warn|fatal (default warn; empty/unset = warn). */
|
|
121
|
+
function stalenessMode() {
|
|
122
|
+
const raw = (process.env.RENOR_VAULT_STALENESS ?? '').trim().toLowerCase();
|
|
123
|
+
if (raw === 'off' || raw === 'warn' || raw === 'fatal')
|
|
124
|
+
return raw;
|
|
125
|
+
return 'warn';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Is the guard EXPLICITLY armed?
|
|
129
|
+
*
|
|
130
|
+
* `RENOR_VAULT_STALENESS` unset means "the operator asked for nothing", which is not the
|
|
131
|
+
* same as asking for `warn`: `stalenessMode()` defaults to `warn` so the startup banner is
|
|
132
|
+
* loud by default, but a default the operator never typed must never change a response
|
|
133
|
+
* shape. Only the two modes that name a behaviour explicitly count as armed.
|
|
134
|
+
*/
|
|
135
|
+
function stalenessArmed(env = process.env) {
|
|
136
|
+
const raw = (env.RENOR_VAULT_STALENESS ?? '').trim().toLowerCase();
|
|
137
|
+
return raw === 'warn' || raw === 'fatal';
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The `search --json` field for a staleness verdict, or nothing.
|
|
141
|
+
*
|
|
142
|
+
* The machine-readable transport is a FROZEN public contract: its own test
|
|
143
|
+
* (`tests/context/product-read-context-contract.test.ts`, "adds nothing to the
|
|
144
|
+
* response when the flag is unset") requires the un-armed response to be
|
|
145
|
+
* byte-identical to the shipped one — no new top-level key.
|
|
146
|
+
*
|
|
147
|
+
* That is why the verdict is OPT-IN, and why the earlier "stale-only" rule was not enough.
|
|
148
|
+
* Stale-only kept the key off a healthy vault, but the shipped contract test's fixture is a
|
|
149
|
+
* plain, non-git folder (`kind: 'plain'`, never stale), so it could not see the stale path:
|
|
150
|
+
* a stale vault with `RENOR_VAULT_STALENESS` UNSET still returned `['hits','vaultStaleness']`
|
|
151
|
+
* and the repo's own assertion failed on exactly that vault
|
|
152
|
+
* (`expected [ 'hits', 'vaultStaleness' ] to deeply equal [ 'hits' ]`). Measured, not reasoned.
|
|
153
|
+
*
|
|
154
|
+
* So the field is attached only when the guard is EXPLICITLY armed (see `stalenessArmed`) AND
|
|
155
|
+
* the vault is stale. An operator who types `RENOR_VAULT_STALENESS=warn` gets the signal on
|
|
156
|
+
* the client surface as well as on stderr; an operator who types nothing gets a response
|
|
157
|
+
* byte-identical to the shipped one, stale or not. `off` mutes the field as it mutes the banner.
|
|
158
|
+
*/
|
|
159
|
+
function stalenessJsonField(staleness, opts = {}) {
|
|
160
|
+
const mode = opts.mode ?? stalenessMode();
|
|
161
|
+
const armed = opts.armed ?? stalenessArmed();
|
|
162
|
+
if (!armed || mode === 'off' || !staleness.stale)
|
|
163
|
+
return {};
|
|
164
|
+
return { vaultStaleness: staleness };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Gate helper for cli.ts setup(): evaluate + render. Pure — no process.exit,
|
|
168
|
+
* no console. The caller prints stderrLines and may refuse to continue.
|
|
169
|
+
*
|
|
170
|
+
* WHICH SIGNAL IS FATAL, stated because the two are different kinds of evidence:
|
|
171
|
+
*
|
|
172
|
+
* - HARD, fatal-capable: `behind > 0` — HEAD is behind the ALREADY-FETCHED upstream ref.
|
|
173
|
+
* The local checkout provably does not have commits upstream already has, so refusing to
|
|
174
|
+
* serve is refusing to serve content that is known to be old.
|
|
175
|
+
* - ADVISORY, never fatal: `headAgeDays > maxHeadAgeDays` — the checkout has received no
|
|
176
|
+
* commits for N days. It exists because `behind` cannot see a stale FETCH (nothing fetched
|
|
177
|
+
* means nothing to be behind of), but age alone is a heuristic about activity, not proof
|
|
178
|
+
* that the content is outdated: a quiet vault whose HEAD still matches its upstream would
|
|
179
|
+
* otherwise be taken offline by `fatal` with no knob to tune the threshold.
|
|
180
|
+
*
|
|
181
|
+
* So `fatal` fires only on the hard signal. An age-only verdict still renders its banner and
|
|
182
|
+
* still reaches the JSON field when the guard is armed; it just does not stop serving.
|
|
183
|
+
*/
|
|
184
|
+
function stalenessGate(vaultPath, opts = {}) {
|
|
185
|
+
const mode = opts.mode ?? stalenessMode();
|
|
186
|
+
const staleness = checkVaultStaleness(vaultPath, opts);
|
|
187
|
+
const hardEvidence = (staleness.behind ?? 0) > 0;
|
|
188
|
+
const stderrLines = [];
|
|
189
|
+
if (staleness.stale && mode !== 'off') {
|
|
190
|
+
for (const r of staleness.reasons) {
|
|
191
|
+
stderrLines.push(`[staleness] VAULT STALE: ${r}`);
|
|
192
|
+
}
|
|
193
|
+
// The hint must not contradict the narrowed rule: telling an operator who is ALREADY in
|
|
194
|
+
// `fatal` mode to "set =fatal to fail startup" is noise, and calling an age-only verdict
|
|
195
|
+
// "out-of-date" asserts more than age establishes.
|
|
196
|
+
stderrLines.push(hardEvidence
|
|
197
|
+
? '[staleness] serving an out-of-date vault silently caused defect D1; set RENOR_VAULT_STALENESS=fatal to fail startup, or off to mute'
|
|
198
|
+
: '[staleness] the age signal is ADVISORY and never refuses service; only HEAD behind its already-fetched upstream is fatal. Set RENOR_VAULT_STALENESS=off to mute');
|
|
199
|
+
}
|
|
200
|
+
return { stderrLines, fatal: mode === 'fatal' && hardEvidence, staleness };
|
|
201
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The engine's token counter, loaded on first use rather than at import time.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS IS LAZY, with the measurement rather than the intuition: requiring
|
|
6
|
+
* `gpt-tokenizer` cost ~344 ms on Node 22, of which ~205 ms is the o200k_base
|
|
7
|
+
* BPE ranks table — the single heaviest module on a cold start of
|
|
8
|
+
* dist/mvp/cli.js, ahead of express (97 ms) and the MCP SDK (205 ms). Both of
|
|
9
|
+
* the engine's entry paths need a count eventually (the chunker when it cuts a
|
|
10
|
+
* note, the context assembler when it budgets a reply), so this is not a module
|
|
11
|
+
* that can be dropped; it is one that a wake does not need yet.
|
|
12
|
+
*
|
|
13
|
+
* WHY IT IS ./o200k AND NOT gpt-tokenizer ANY MORE: gpt-tokenizer builds a Map of
|
|
14
|
+
* 200,000 strings, which is ~45 MiB of resident memory in every running
|
|
15
|
+
* workspace for ever after its first count (~60 MiB under load). ./o200k counts
|
|
16
|
+
* the same tokens from ~4 MiB of typed arrays; tests/indexer/o200k-counter.test.ts
|
|
17
|
+
* holds its counts equal to gpt-tokenizer's. gpt-tokenizer stays a dependency for
|
|
18
|
+
* the evaluation harness and for those tests, which are the oracle.
|
|
19
|
+
* docs/performance/MEMORY-PER-WORKSPACE.md has the measurement.
|
|
20
|
+
*
|
|
21
|
+
* WHY IT IS SAFE: a count is a pure function of its input. There is no
|
|
22
|
+
* construction cost, no ordering requirement, no ambient state to initialise and
|
|
23
|
+
* nothing that observes whether it was imported eagerly. The only observable
|
|
24
|
+
* difference is WHEN the vocabulary is built, and this file is the single place
|
|
25
|
+
* that decides that.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.encodeTokens = encodeTokens;
|
|
29
|
+
exports.tokenizerLoaded = tokenizerLoaded;
|
|
30
|
+
const o200k_1 = require("./o200k");
|
|
31
|
+
/**
|
|
32
|
+
* A run of one kind of character (all whitespace, or none) this long is not encoded. The encoder's merge loop costs
|
|
33
|
+
* time quadratic in the length of such a run: 60,000 characters of `x` take ~5 s, where prose of the same size takes
|
|
34
|
+
* ~3 ms. Real text has no runs this long, so only a pathological or pasted blob reaches it.
|
|
35
|
+
*/
|
|
36
|
+
const LONG_RUN = 1_000;
|
|
37
|
+
const RUN_RE = /\S+|\s+/g;
|
|
38
|
+
/**
|
|
39
|
+
* Characters per token for a long run, measured on this encoder (o200k): 60,000 `x` are 7,500 tokens and 60,000
|
|
40
|
+
* spaces are 471. A blob of base64 or minified code is nearer the first, so the estimate is a middle figure for
|
|
41
|
+
* non-space runs, and a generous one for whitespace.
|
|
42
|
+
*/
|
|
43
|
+
const CHARS_PER_TOKEN = { text: 4, space: 128 };
|
|
44
|
+
/**
|
|
45
|
+
* The token count of `text`, with the counter loaded on first call.
|
|
46
|
+
*
|
|
47
|
+
* Special token syntax (`<|im_start|>`, `<|endoftext|>`, …) is counted as plain
|
|
48
|
+
* text, not refused: it can legitimately appear inside note CONTENT, and
|
|
49
|
+
* gpt-tokenizer rejects such text as chat protocol by default. Counting it as
|
|
50
|
+
* plain text is what keeps indexing from failing closed on an imported ShareGPT
|
|
51
|
+
* history.
|
|
52
|
+
*
|
|
53
|
+
* A run of at least LONG_RUN characters is estimated (see CHARS_PER_TOKEN) and taken out of the text the encoder
|
|
54
|
+
* sees, so the estimate is not its exact count. Text without such a run is counted exactly, as before.
|
|
55
|
+
*/
|
|
56
|
+
function encodeTokens(text) {
|
|
57
|
+
let estimated = 0;
|
|
58
|
+
let rest = text;
|
|
59
|
+
if (text.length >= LONG_RUN) {
|
|
60
|
+
const runs = text.match(RUN_RE) ?? [];
|
|
61
|
+
if (runs.some(run => run.length >= LONG_RUN)) {
|
|
62
|
+
rest = runs
|
|
63
|
+
.map(run => {
|
|
64
|
+
if (run.length < LONG_RUN)
|
|
65
|
+
return run;
|
|
66
|
+
estimated += Math.ceil(run.length / (/\s/.test(run) ? CHARS_PER_TOKEN.space : CHARS_PER_TOKEN.text));
|
|
67
|
+
return ' ';
|
|
68
|
+
})
|
|
69
|
+
.join('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return (0, o200k_1.countO200k)(rest) + estimated;
|
|
73
|
+
}
|
|
74
|
+
/** True once the counter module has actually been loaded. Diagnostics and tests. */
|
|
75
|
+
function tokenizerLoaded() {
|
|
76
|
+
return (0, o200k_1.o200kLoaded)();
|
|
77
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.walkVault = walkVault;
|
|
7
|
+
exports.walkVaultStats = walkVaultStats;
|
|
8
|
+
exports.statVaultFiles = statVaultFiles;
|
|
9
|
+
exports.vaultIgnores = vaultIgnores;
|
|
10
|
+
exports.isIgnoredByVault = isIgnoredByVault;
|
|
11
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const buffer_1 = require("buffer");
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const DEFAULT_IGNORE = ['.obsidian/', '.trash/', '.semantic-memory/', '.memory-index/', 'node_modules/'];
|
|
16
|
+
/**
|
|
17
|
+
* The vault's ignore test, built once per walk instead of rebuilt for every entry: a relative path is
|
|
18
|
+
* ignored when a pattern names it, a directory above it, a directory anywhere inside it, or its last segment.
|
|
19
|
+
*/
|
|
20
|
+
function compileIgnore(patterns) {
|
|
21
|
+
const rules = patterns.map(p => {
|
|
22
|
+
const clean = p.replace(/\/$/, '');
|
|
23
|
+
return { clean, dir: clean + '/', inside: '/' + clean + '/', tail: '/' + clean };
|
|
24
|
+
});
|
|
25
|
+
return rel => rules.some(r => rel === r.clean || rel.startsWith(r.dir) || rel.includes(r.inside) || rel.endsWith(r.tail));
|
|
26
|
+
}
|
|
27
|
+
async function walkVault(vaultPath, options = {}) {
|
|
28
|
+
let patterns = [...DEFAULT_IGNORE, ...(options.ignorePatterns ?? [])];
|
|
29
|
+
try {
|
|
30
|
+
const raw = await promises_1.default.readFile(path_1.default.join(vaultPath, '.semanticignore'), 'utf8');
|
|
31
|
+
const fromFile = raw
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(l => l.trim())
|
|
34
|
+
.filter(l => l && !l.startsWith('#'));
|
|
35
|
+
patterns = [...patterns, ...fromFile];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* no .semanticignore */
|
|
39
|
+
}
|
|
40
|
+
const ignored = compileIgnore(patterns);
|
|
41
|
+
const results = [];
|
|
42
|
+
/**
|
|
43
|
+
* `relDir` is the folder's path below the vault. `raw` is the folder as bytes and `shown` its path below the
|
|
44
|
+
* vault as a message writes it; both are set once a name on the way down is not valid UTF-8, because `dir`
|
|
45
|
+
* then names nothing real.
|
|
46
|
+
*/
|
|
47
|
+
async function walk(dir, relDir, raw, shown) {
|
|
48
|
+
// Names are read as bytes only to report the ones that are not UTF-8; otherwise as strings, which Node decodes
|
|
49
|
+
// the same way. Every save walks the vault, so this and the join below are paid per file per save.
|
|
50
|
+
const entries = options.onNonUtf8Markdown
|
|
51
|
+
? await promises_1.default.readdir(raw ?? dir, { withFileTypes: true, encoding: 'buffer' })
|
|
52
|
+
: await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
53
|
+
// `path.join(dir, name)` for a name read from a folder (one segment, never `.` or `..`): the folder as join
|
|
54
|
+
// writes it, joined once per folder rather than once per entry.
|
|
55
|
+
const folder = path_1.default.join(dir, 'x').slice(0, -1);
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const bytes = typeof entry.name === 'string' ? null : entry.name;
|
|
58
|
+
const name = bytes ? bytes.toString('utf8') : entry.name;
|
|
59
|
+
const full = folder + name;
|
|
60
|
+
const rel = relDir ? relDir + path_1.default.sep + name : name;
|
|
61
|
+
if (ignored(rel))
|
|
62
|
+
continue;
|
|
63
|
+
if (options.onNonUtf8Markdown && bytes && (raw || !(0, buffer_1.isUtf8)(bytes))) {
|
|
64
|
+
const prefix = shown ?? relDir.split(path_1.default.sep).filter(Boolean).map(showName).join('/');
|
|
65
|
+
const label = prefix ? `${prefix}/${showName(bytes)}` : showName(bytes);
|
|
66
|
+
if (entry.isDirectory())
|
|
67
|
+
await walk(full, rel, Buffer.concat([raw ?? Buffer.from(dir), Buffer.from(path_1.default.sep), bytes]), label);
|
|
68
|
+
else if (entry.isFile() && name.endsWith('.md'))
|
|
69
|
+
options.onNonUtf8Markdown(label);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry.isDirectory())
|
|
73
|
+
await walk(full, rel);
|
|
74
|
+
else if (entry.isFile() && name.endsWith('.md'))
|
|
75
|
+
results.push(full);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
await walk(vaultPath, '');
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A name as a message writes it: valid UTF-8 as it is, every other byte as `\xNN`, and a
|
|
83
|
+
* backslash or control character the same way (the backslash doubled), so the result is
|
|
84
|
+
* unambiguous, one line, and cannot carry a terminal escape sequence.
|
|
85
|
+
*/
|
|
86
|
+
function showName(name) {
|
|
87
|
+
const bytes = typeof name === 'string' ? Buffer.from(name) : name;
|
|
88
|
+
let shown = '';
|
|
89
|
+
for (let i = 0; i < bytes.length;) {
|
|
90
|
+
const lead = bytes[i];
|
|
91
|
+
const length = lead < 0xc2 ? 1 : lead < 0xe0 ? 2 : lead < 0xf0 ? 3 : lead < 0xf5 ? 4 : 0;
|
|
92
|
+
const isC1Control = lead === 0xc2 && bytes[i + 1] < 0xa0; // U+0080-U+009F
|
|
93
|
+
if (lead >= 0x80 && length > 0 && i + length <= bytes.length && (0, buffer_1.isUtf8)(bytes.subarray(i, i + length)) && !isC1Control) {
|
|
94
|
+
shown += bytes.toString('utf8', i, i + length);
|
|
95
|
+
i += length;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (lead === 0x5c)
|
|
99
|
+
shown += '\\\\';
|
|
100
|
+
else if (lead >= 0x80 || lead < 0x20 || lead === 0x7f)
|
|
101
|
+
shown += `\\x${lead.toString(16).padStart(2, '0')}`;
|
|
102
|
+
else
|
|
103
|
+
shown += String.fromCharCode(lead);
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
return shown;
|
|
107
|
+
}
|
|
108
|
+
const STAT_CHUNK = 512;
|
|
109
|
+
/**
|
|
110
|
+
* The files walkVault lists, each with the stat fields above, for a caller that wants to know what changed
|
|
111
|
+
* without reading anything. Stats are synchronous (a few microseconds each, against tens for an asynchronous
|
|
112
|
+
* call that fans out over the thread pool) in chunks, with a turn of the event loop between them. A file that
|
|
113
|
+
* vanishes between the listing and its stat is left out; one whose stat fails for another reason is listed with
|
|
114
|
+
* a stamp that never matches, so whoever reads it reports the error as a full pass would.
|
|
115
|
+
*/
|
|
116
|
+
async function walkVaultStats(vaultPath, options = {}) {
|
|
117
|
+
const files = await walkVault(vaultPath, options);
|
|
118
|
+
const stats = [];
|
|
119
|
+
for (let i = 0; i < files.length; i += STAT_CHUNK) {
|
|
120
|
+
for (const file of files.slice(i, i + STAT_CHUNK)) {
|
|
121
|
+
const s = statVaultFile(file);
|
|
122
|
+
if (s)
|
|
123
|
+
stats.push(s);
|
|
124
|
+
}
|
|
125
|
+
if (i + STAT_CHUNK < files.length)
|
|
126
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
127
|
+
}
|
|
128
|
+
return stats;
|
|
129
|
+
}
|
|
130
|
+
/** One file's stat (see walkVaultStats): undefined when it is gone, a stamp that never matches when its stat fails. */
|
|
131
|
+
function statVaultFile(file) {
|
|
132
|
+
try {
|
|
133
|
+
const s = fs_1.default.statSync(file, { throwIfNoEntry: false });
|
|
134
|
+
return s ? { file, mtimeMs: s.mtimeMs, ctimeMs: s.ctimeMs, size: s.size, ino: s.ino } : undefined;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { file, mtimeMs: NaN, ctimeMs: NaN, size: -1, ino: -1 };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The stat of each of the listed files that is there, for a caller that names the files it wants to check (a save's)
|
|
142
|
+
* instead of walking the vault. A file that is gone is left out, as walkVaultStats leaves it out.
|
|
143
|
+
*/
|
|
144
|
+
function statVaultFiles(files) {
|
|
145
|
+
const stats = [];
|
|
146
|
+
for (const file of files) {
|
|
147
|
+
const s = statVaultFile(file);
|
|
148
|
+
if (s)
|
|
149
|
+
stats.push(s);
|
|
150
|
+
}
|
|
151
|
+
return stats;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The vault's ignore rules as walkVault applies them, for a caller that names files instead of listing them: true when the
|
|
155
|
+
* walk would never reach the file, which it prunes at an ignored folder as well as skipping an ignored file. Reads
|
|
156
|
+
* .semanticignore once, when asked.
|
|
157
|
+
*/
|
|
158
|
+
async function vaultIgnores(vaultPath) {
|
|
159
|
+
let patterns = [...DEFAULT_IGNORE];
|
|
160
|
+
try {
|
|
161
|
+
const raw = await promises_1.default.readFile(path_1.default.join(vaultPath, '.semanticignore'), 'utf8');
|
|
162
|
+
patterns = [...patterns, ...raw.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))];
|
|
163
|
+
}
|
|
164
|
+
catch { /* no .semanticignore */ }
|
|
165
|
+
const ignored = compileIgnore(patterns);
|
|
166
|
+
return (file) => {
|
|
167
|
+
const parts = path_1.default.relative(vaultPath, file).split(path_1.default.sep);
|
|
168
|
+
return parts.some((_, i) => ignored(parts.slice(0, i + 1).join(path_1.default.sep)));
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Whether the vault's own ignore rules (the defaults and .semanticignore) leave out a vault-relative path. The tools that
|
|
173
|
+
* address a note by id (read_memory, retire_memory) ask it, so a file the owner excluded from memory cannot be read or
|
|
174
|
+
* moved through its path any more than it can be found by search.
|
|
175
|
+
*/
|
|
176
|
+
async function isIgnoredByVault(vaultPath, rel) {
|
|
177
|
+
let patterns = [...DEFAULT_IGNORE];
|
|
178
|
+
try {
|
|
179
|
+
const raw = await promises_1.default.readFile(path_1.default.join(vaultPath, '.semanticignore'), 'utf8');
|
|
180
|
+
patterns = [...patterns, ...raw.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))];
|
|
181
|
+
}
|
|
182
|
+
catch { /* no .semanticignore */ }
|
|
183
|
+
return compileIgnore(patterns)(rel.split('/').join(path_1.default.sep));
|
|
184
|
+
}
|
package/dist/mcp/hive.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HIVE_TOOLS = void 0;
|
|
4
|
+
exports.createStdioHiveTools = createStdioHiveTools;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const secretGuard_1 = require("../extraction/secretGuard");
|
|
7
|
+
const agentLease_1 = require("../memory/agentLease");
|
|
8
|
+
const limits_1 = require("../memory/limits");
|
|
9
|
+
const requests_1 = require("../memory/requests");
|
|
10
|
+
const sourceName_1 = require("../memory/sourceName");
|
|
11
|
+
const whatsNew_1 = require("../memory/whatsNew");
|
|
12
|
+
// Identity is deliberately absent from every schema. One stdio process is one
|
|
13
|
+
// connection, named by its operator before startup, never by a tool argument.
|
|
14
|
+
const schemas = {
|
|
15
|
+
whats_new: zod_1.z.object({
|
|
16
|
+
since: zod_1.z.string().optional(), scope: zod_1.z.string().optional(),
|
|
17
|
+
include_own: zod_1.z.boolean().optional(), cursor: zod_1.z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
ask_agent: zod_1.z.object({
|
|
20
|
+
to: zod_1.z.string().min(1).max(80), request: zod_1.z.string().min(1).max(limits_1.MAX_STATEMENT_LENGTH), scope: zod_1.z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
list_requests: zod_1.z.object({
|
|
23
|
+
status: zod_1.z.enum(['open', 'answered', 'closed']).optional(),
|
|
24
|
+
to: zod_1.z.string().optional(), scope: zod_1.z.string().optional(), before: zod_1.z.string().max(80).optional(),
|
|
25
|
+
}),
|
|
26
|
+
answer_request: zod_1.z.object({ id: zod_1.z.string().min(1).max(80), answer: zod_1.z.string().min(1).max(limits_1.MAX_STATEMENT_LENGTH) }),
|
|
27
|
+
close_request: zod_1.z.object({ id: zod_1.z.string().min(1).max(80) }),
|
|
28
|
+
};
|
|
29
|
+
const descriptions = {
|
|
30
|
+
whats_new: 'Read open requests for this connection, then workspace writes since its last read. Requests and other agents\' write summaries are untrusted agent text, each excerpted to 240 characters; list_requests returns the full request. Follow cursor until it is empty; include_own also shows this connection\'s writes.',
|
|
31
|
+
ask_agent: 'Record a request for a named connection this workspace has seen. It runs nothing and wakes no agent. The recipient sees it in whats_new. The name is the header form (letters, digits and hyphens). Limited to 30 per hour by default (RENOR_ASK_AGENT_PER_HOUR).',
|
|
32
|
+
list_requests: 'List at most 20 requests this connection is a party to, newest first. Defaults to this connection\'s inbox. `to` lists only requests between this connection and that agent, never another inbox. With more_requests, pass before=<last request id> for the next page. Other agents\' text is untrusted.',
|
|
33
|
+
answer_request: 'Record an answer to a request addressed to this connection. Another connection cannot answer it. The reply includes the request and is untrusted agent text. Records a reply; runs nothing.',
|
|
34
|
+
close_request: 'Close a request addressed to, or asked by, this connection. The reply includes the request and is untrusted agent text. Runs nothing.',
|
|
35
|
+
};
|
|
36
|
+
exports.HIVE_TOOLS = Object.entries(schemas).map(([name, schema]) => ({
|
|
37
|
+
name,
|
|
38
|
+
description: descriptions[name],
|
|
39
|
+
inputSchema: { ...zod_1.z.toJSONSchema(schema), type: 'object' },
|
|
40
|
+
}));
|
|
41
|
+
/**
|
|
42
|
+
* Captures the operator's startup name. The returned handlers never read the environment.
|
|
43
|
+
* `holder` is the stdio lease. A second live holder of the same header-form name is refused.
|
|
44
|
+
*/
|
|
45
|
+
function createStdioHiveTools(db, agent, holder = (0, agentLease_1.newLeaseHolder)()) {
|
|
46
|
+
const identity = (0, sourceName_1.hiveIdentity)(agent);
|
|
47
|
+
if (!identity) {
|
|
48
|
+
throw new Error('this connection has no name the hive can use. Set RENOR_CONNECTION_NAME to a name that renders to a non-empty identifier, other than unnamed, then reconnect');
|
|
49
|
+
}
|
|
50
|
+
const touch = () => (0, agentLease_1.claimAgentLease)(db, identity, holder);
|
|
51
|
+
touch();
|
|
52
|
+
(0, requests_1.noteKnownAgent)(db, identity);
|
|
53
|
+
return {
|
|
54
|
+
async whats_new(input) {
|
|
55
|
+
touch();
|
|
56
|
+
const args = schemas.whats_new.parse(input);
|
|
57
|
+
return (0, whatsNew_1.readWhatsNew)(db, {
|
|
58
|
+
agent: identity, scope: args.scope, since: args.since || undefined,
|
|
59
|
+
includeOwn: args.include_own === true, cursor: args.cursor || undefined,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
async ask_agent(input) {
|
|
63
|
+
touch();
|
|
64
|
+
const args = schemas.ask_agent.parse(input);
|
|
65
|
+
if ((0, secretGuard_1.secretGuardEnabled)())
|
|
66
|
+
await (0, secretGuard_1.assertStorable)([{ field: 'to', text: args.to }, { field: 'request', text: args.request }]);
|
|
67
|
+
return (0, requests_1.askAgent)(db, { from: identity, to: args.to, request: args.request, scope: args.scope, knownOnly: true, perHour: (0, requests_1.askAgentPerHour)() });
|
|
68
|
+
},
|
|
69
|
+
async list_requests(input) {
|
|
70
|
+
touch();
|
|
71
|
+
const args = schemas.list_requests.parse(input);
|
|
72
|
+
const page = (0, requests_1.listRequestsPage)(db, { agent: identity, ...args });
|
|
73
|
+
return { ...page, requests: page.requests.map(r => ({ ...r, untrusted: true })) };
|
|
74
|
+
},
|
|
75
|
+
async answer_request(input) {
|
|
76
|
+
touch();
|
|
77
|
+
const args = schemas.answer_request.parse(input);
|
|
78
|
+
if ((0, secretGuard_1.secretGuardEnabled)())
|
|
79
|
+
await (0, secretGuard_1.assertStorable)([{ field: 'answer', text: args.answer }]);
|
|
80
|
+
return { ...(0, requests_1.answerRequest)(db, { agent: identity, ...args }), untrusted: true };
|
|
81
|
+
},
|
|
82
|
+
async close_request(input) {
|
|
83
|
+
touch();
|
|
84
|
+
const args = schemas.close_request.parse(input);
|
|
85
|
+
return { ...(0, requests_1.closeRequest)(db, { agent: identity, ...args }), untrusted: true };
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isZodError = isZodError;
|
|
4
|
+
exports.oneLineInvalidParams = oneLineInvalidParams;
|
|
5
|
+
exports.mapInvalidParams = mapInvalidParams;
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
function isZodError(error) {
|
|
8
|
+
// The SDK's schema parse throws zod v4's `$ZodError`; the transport's parse throws classic `ZodError`.
|
|
9
|
+
return error instanceof Error && /ZodError$/.test(error.name) && Array.isArray(error.issues);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A request whose params the SDK's schema refuses is answered with the error its parser threw: a ZodError has
|
|
13
|
+
* no code, so the SDK sends -32603 with the issue list as its message. The client asked wrongly, so the answer is
|
|
14
|
+
* -32602 with one line that names the field. The error carries the code the SDK reads (Protocol sends
|
|
15
|
+
* `error.code` when it is a safe integer).
|
|
16
|
+
*/
|
|
17
|
+
function oneLineInvalidParams(error) {
|
|
18
|
+
if (!isZodError(error))
|
|
19
|
+
return error;
|
|
20
|
+
const detail = error.issues
|
|
21
|
+
.map(issue => `${issue.path.map(String).join('.') || 'params'}: ${issue.message}`)
|
|
22
|
+
.join('; ');
|
|
23
|
+
const message = `Invalid params: ${detail}`;
|
|
24
|
+
return Object.assign(new Error(message.length > 300 ? `${message.slice(0, 297)}...` : message), {
|
|
25
|
+
code: types_js_1.ErrorCode.InvalidParams,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps every request handler the server has registered, the SDK's own `initialize` included, so a schema refusal
|
|
30
|
+
* reaches the client as -32602. The SDK keeps its handlers in a private map and parses each request inside the
|
|
31
|
+
* handler it stores, so the only place a refusal can be turned into the right code is this wrapper. Call it after
|
|
32
|
+
* every handler is registered and before the transport connects. An SDK that renames the map throws here at startup,
|
|
33
|
+
* and the standing tests catch it before a release.
|
|
34
|
+
*/
|
|
35
|
+
function mapInvalidParams(server) {
|
|
36
|
+
const handlers = server._requestHandlers;
|
|
37
|
+
for (const [method, handler] of handlers) {
|
|
38
|
+
handlers.set(method, async (request, extra) => {
|
|
39
|
+
try {
|
|
40
|
+
return await handler(request, extra);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw oneLineInvalidParams(error);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|