wyrm-mcp 7.3.2 → 7.3.3
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/README.md +8 -14
- package/dist/activation.js +59 -1
- package/dist/agent-daemon.js +281 -4
- package/dist/agent-loop.js +332 -7
- package/dist/analytics.js +236 -13
- package/dist/attribution.js +49 -1
- package/dist/audit.js +457 -2
- package/dist/auto-capture.js +138 -3
- package/dist/auto-orchestrator.js +325 -1
- package/dist/autoconfig.d.ts +50 -0
- package/dist/autoconfig.d.ts.map +1 -1
- package/dist/autoconfig.js +1115 -39
- package/dist/autoconfig.js.map +1 -1
- package/dist/buddy-runner.js +109 -1
- package/dist/buddy.js +564 -14
- package/dist/build-flags.js +15 -1
- package/dist/capabilities.js +183 -3
- package/dist/capture.js +56 -1
- package/dist/causality.js +148 -8
- package/dist/cli.js +281 -20
- package/dist/cloud/cli.js +541 -5
- package/dist/cloud/client.js +221 -1
- package/dist/cloud/crypto.js +85 -1
- package/dist/cloud/machine-id.js +113 -2
- package/dist/cloud/recovery.js +60 -1
- package/dist/cloud/sync-engine.js +543 -7
- package/dist/cloud-backup.js +579 -5
- package/dist/cloud-profile.js +138 -1
- package/dist/cloud-sync-entrypoint.js +47 -1
- package/dist/cloud-sync.js +309 -2
- package/dist/connectors/bridge-source.d.ts +46 -0
- package/dist/connectors/bridge-source.d.ts.map +1 -0
- package/dist/connectors/bridge-source.js +77 -0
- package/dist/connectors/bridge-source.js.map +1 -0
- package/dist/connectors/index.d.ts +24 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +69 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/ingest.d.ts +16 -0
- package/dist/connectors/ingest.d.ts.map +1 -0
- package/dist/connectors/ingest.js +116 -0
- package/dist/connectors/ingest.js.map +1 -0
- package/dist/connectors/types.d.ts +99 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +17 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/constellation.js +168 -12
- package/dist/content-signature.js +45 -1
- package/dist/context-build-budgeted.js +144 -4
- package/dist/context-ranking.js +69 -1
- package/dist/crypto.js +179 -1
- package/dist/daemon-write-endpoint.js +290 -1
- package/dist/daemon-writer.js +406 -2
- package/dist/database.js +1278 -53
- package/dist/deprecations.js +162 -2
- package/dist/design.js +141 -13
- package/dist/event-replication.js +112 -1
- package/dist/events-sse.js +43 -7
- package/dist/events.js +238 -6
- package/dist/failure-patterns.d.ts +107 -0
- package/dist/failure-patterns.d.ts.map +1 -1
- package/dist/failure-patterns.js +924 -43
- package/dist/failure-patterns.js.map +1 -1
- package/dist/federation.js +236 -12
- package/dist/goals.js +101 -13
- package/dist/golden.js +355 -3
- package/dist/handlers/agent.js +165 -4
- package/dist/handlers/alias-adapters.js +129 -1
- package/dist/handlers/aliases.js +171 -1
- package/dist/handlers/audit.js +87 -1
- package/dist/handlers/boundary.js +221 -1
- package/dist/handlers/capture.js +1114 -73
- package/dist/handlers/causality.js +119 -9
- package/dist/handlers/cloud.js +382 -85
- package/dist/handlers/companion.js +459 -28
- package/dist/handlers/datalake.js +187 -7
- package/dist/handlers/dispatch-context.js +22 -0
- package/dist/handlers/entity.js +256 -25
- package/dist/handlers/events.js +335 -16
- package/dist/handlers/failure.d.ts.map +1 -1
- package/dist/handlers/failure.js +408 -13
- package/dist/handlers/failure.js.map +1 -1
- package/dist/handlers/goals.js +296 -4
- package/dist/handlers/intelligence.js +681 -126
- package/dist/handlers/invoicing.js +70 -1
- package/dist/handlers/mcpclient.js +137 -6
- package/dist/handlers/orchestration.js +125 -40
- package/dist/handlers/output-schemas.js +24 -1
- package/dist/handlers/presence.js +99 -3
- package/dist/handlers/project.js +182 -28
- package/dist/handlers/prompts.js +157 -6
- package/dist/handlers/quest.js +224 -4
- package/dist/handlers/recall.js +237 -13
- package/dist/handlers/registry.js +167 -1
- package/dist/handlers/resources.js +288 -1
- package/dist/handlers/review.js +74 -11
- package/dist/handlers/run.js +498 -16
- package/dist/handlers/search.js +338 -15
- package/dist/handlers/session.js +643 -31
- package/dist/handlers/share.js +184 -8
- package/dist/handlers/shims.js +464 -1
- package/dist/handlers/skill.js +449 -67
- package/dist/handlers/survivors.js +120 -1
- package/dist/handlers/symbols.js +109 -8
- package/dist/handlers/syncops.js +302 -4
- package/dist/handlers/types.js +27 -1
- package/dist/harvest.js +191 -5
- package/dist/hours.js +156 -7
- package/dist/http-auth.js +321 -3
- package/dist/http-fast.js +1302 -22
- package/dist/icons.js +47 -1
- package/dist/importers.js +268 -1
- package/dist/index.js +840 -2
- package/dist/indexer.js +145 -4
- package/dist/intelligence.js +261 -31
- package/dist/internal-dispatch.js +212 -3
- package/dist/keyset.js +110 -1
- package/dist/knowledge-graph.js +176 -12
- package/dist/license.js +441 -2
- package/dist/logger.js +199 -2
- package/dist/maintenance.js +148 -2
- package/dist/mcp-client.js +262 -6
- package/dist/memory-artifacts.js +596 -32
- package/dist/migrate-prompt.js +124 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +799 -42
- package/dist/migrations.js.map +1 -1
- package/dist/performance.js +228 -1
- package/dist/presence.js +140 -11
- package/dist/priority-embed.js +164 -5
- package/dist/providers/embedding-provider.js +196 -1
- package/dist/readonly-gate.js +29 -1
- package/dist/receipt.js +43 -1
- package/dist/rehydration.js +157 -9
- package/dist/reindex.js +88 -1
- package/dist/render-target.js +544 -21
- package/dist/render.js +280 -4
- package/dist/repl-guard.js +173 -1
- package/dist/replication-daemon-entrypoint.js +31 -1
- package/dist/replication-daemon.js +262 -2
- package/dist/rerank.js +142 -1
- package/dist/resilience.js +591 -1
- package/dist/reverse-bridge.js +360 -5
- package/dist/security.js +244 -1
- package/dist/session-seen.js +51 -3
- package/dist/setup.js +260 -1
- package/dist/skill-author.js +168 -5
- package/dist/spec-kit.js +191 -1
- package/dist/sqlite-busy.js +154 -1
- package/dist/statusline.js +315 -11
- package/dist/sub-agent.js +262 -13
- package/dist/summarizer.js +139 -13
- package/dist/symbols.js +283 -7
- package/dist/sync.js +359 -5
- package/dist/tasks-dispatch.js +84 -1
- package/dist/tasks.js +282 -1
- package/dist/token-budget.js +143 -1
- package/dist/tool-analytics.js +129 -7
- package/dist/tool-annotations.js +365 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +75 -1
- package/dist/trace-harvest.js +244 -6
- package/dist/types.js +30 -1
- package/dist/ui-dashboard.js +50 -41
- package/dist/ulid.js +81 -1
- package/dist/usage-tracker.js +66 -1
- package/dist/validate.js +129 -1
- package/dist/vault.js +534 -1
- package/dist/vector-init.js +67 -1
- package/dist/vectors.js +184 -3
- package/dist/version-check.js +136 -4
- package/dist/visibility.js +155 -19
- package/dist/wyrm-cli.js +2845 -101
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.d.ts.map +1 -1
- package/dist/wyrm-guard.js +475 -14
- package/dist/wyrm-guard.js.map +1 -1
- package/dist/wyrm-loop.js +150 -3
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +11 -1
- package/dist/wyrm-statusline.js +56 -4
- package/dist/wyrm-ui.js +77 -9
- package/package.json +1 -1
|
@@ -1,2 +1,262 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Wyrm Live Memory v6.4 (Phase 3b) — always-on replication daemon.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Phase-3 `replicateOnce` primitive in a background loop that syncs a
|
|
5
|
+
* registry of peers on an interval. Mirrors the `CloudSyncDaemon` /
|
|
6
|
+
* `CloudSyncManager` shape (detached child + PID file + liveness + rotating log).
|
|
7
|
+
*
|
|
8
|
+
* Secret hygiene: a peer's bearer token is NEVER stored in the DB. The registry
|
|
9
|
+
* holds the NAME of an env var (`tokenEnv`); the token is resolved from
|
|
10
|
+
* `process.env` at tick time, so the SQLite file never contains a credential.
|
|
11
|
+
*
|
|
12
|
+
* The daemon shares the MCP server's DB (`~/.wyrm/wyrm.db` by default, or
|
|
13
|
+
* `WYRM_DB_PATH`) — better-sqlite3 WAL supports the concurrent connections.
|
|
14
|
+
*
|
|
15
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
16
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'child_process';
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, openSync, closeSync, unlinkSync, renameSync, } from 'fs';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { join } from 'path';
|
|
22
|
+
import { replicateOnce } from './event-replication.js';
|
|
23
|
+
import { checkPeerUrl, isValidTokenEnvName } from './repl-guard.js';
|
|
24
|
+
const WYRM_DIR = join(homedir(), '.wyrm');
|
|
25
|
+
const PID_FILE = join(WYRM_DIR, 'wyrm-replication.pid');
|
|
26
|
+
const LOG_FILE = join(WYRM_DIR, 'wyrm-replication.log');
|
|
27
|
+
const DEFAULT_INTERVAL_MS = 60_000; // 1 min — events are small + incremental
|
|
28
|
+
const MIN_INTERVAL_MS = 5_000; // floor: don't hammer a peer
|
|
29
|
+
const LOG_ROTATE_BYTES = 1_000_000; // 1 MB
|
|
30
|
+
const LIVENESS_SIGNAL = 0;
|
|
31
|
+
const PEERS_META_KEY = 'repl:peers';
|
|
32
|
+
const MAX_PEERS = 50;
|
|
33
|
+
/** Validate a peer entry at registration time (scheme/URL + tokenEnv name). */
|
|
34
|
+
export function validatePeerEntry(entry) {
|
|
35
|
+
if (!entry || typeof entry.baseUrl !== 'string')
|
|
36
|
+
return { ok: false, reason: 'baseUrl required' };
|
|
37
|
+
const url = checkPeerUrl(cleanBase(entry.baseUrl));
|
|
38
|
+
// Registration is lenient on private hosts (the egress guard re-checks at fetch
|
|
39
|
+
// time); only reject an unparseable URL or a bad scheme here.
|
|
40
|
+
if (!url.ok && !/loopback|private|link-local/.test(url.reason))
|
|
41
|
+
return { ok: false, reason: url.reason };
|
|
42
|
+
if (!entry.projectPath || typeof entry.projectPath !== 'string')
|
|
43
|
+
return { ok: false, reason: 'projectPath required' };
|
|
44
|
+
if (entry.tokenEnv && !isValidTokenEnvName(entry.tokenEnv)) {
|
|
45
|
+
return { ok: false, reason: `tokenEnv must be a WYRM_-prefixed env var name (got ${entry.tokenEnv})` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true };
|
|
48
|
+
}
|
|
49
|
+
// ==================== REGISTRY ====================
|
|
50
|
+
const cleanBase = (u) => String(u).replace(/\/+$/, '');
|
|
51
|
+
/** Read the persisted peer registry (tolerant of corruption — returns []). */
|
|
52
|
+
export function listPeers(db) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = db.getMeta(PEERS_META_KEY);
|
|
55
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
56
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Upsert a peer (keyed on baseUrl + projectPath). Throws on an invalid entry. Returns the new registry. */
|
|
63
|
+
export function addPeer(db, entry) {
|
|
64
|
+
const v = validatePeerEntry(entry);
|
|
65
|
+
if (!v.ok)
|
|
66
|
+
throw new Error(v.reason || 'invalid peer');
|
|
67
|
+
const e = {
|
|
68
|
+
baseUrl: cleanBase(entry.baseUrl),
|
|
69
|
+
projectPath: entry.projectPath,
|
|
70
|
+
...(entry.remoteProject ? { remoteProject: entry.remoteProject } : {}),
|
|
71
|
+
...(entry.tokenEnv ? { tokenEnv: entry.tokenEnv } : {}),
|
|
72
|
+
};
|
|
73
|
+
const peers = listPeers(db).filter((p) => !(cleanBase(p.baseUrl) === e.baseUrl && p.projectPath === e.projectPath));
|
|
74
|
+
peers.push(e);
|
|
75
|
+
if (peers.length > MAX_PEERS)
|
|
76
|
+
peers.splice(0, peers.length - MAX_PEERS);
|
|
77
|
+
db.setMeta(PEERS_META_KEY, JSON.stringify(peers));
|
|
78
|
+
return peers;
|
|
79
|
+
}
|
|
80
|
+
/** Remove a peer by baseUrl + projectPath. Returns the new registry. */
|
|
81
|
+
export function removePeer(db, baseUrl, projectPath) {
|
|
82
|
+
const target = cleanBase(baseUrl);
|
|
83
|
+
const peers = listPeers(db).filter((p) => !(cleanBase(p.baseUrl) === target && p.projectPath === projectPath));
|
|
84
|
+
db.setMeta(PEERS_META_KEY, JSON.stringify(peers));
|
|
85
|
+
return peers;
|
|
86
|
+
}
|
|
87
|
+
// ==================== DAEMON ====================
|
|
88
|
+
export class ReplicationDaemon {
|
|
89
|
+
db;
|
|
90
|
+
intervalMs;
|
|
91
|
+
constructor(db, intervalMs = DEFAULT_INTERVAL_MS) {
|
|
92
|
+
this.db = db;
|
|
93
|
+
this.intervalMs = intervalMs;
|
|
94
|
+
ensureWyrmDir();
|
|
95
|
+
this.intervalMs = Math.max(MIN_INTERVAL_MS, intervalMs);
|
|
96
|
+
}
|
|
97
|
+
/** Run one replication round over all registered peers. Never throws. */
|
|
98
|
+
async tick(fetchImpl = globalThis.fetch) {
|
|
99
|
+
if (!this.db.liveMemoryEnabled())
|
|
100
|
+
return [];
|
|
101
|
+
const peers = listPeers(this.db);
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const p of peers) {
|
|
104
|
+
const project = this.db.getProject(p.projectPath);
|
|
105
|
+
if (!project) {
|
|
106
|
+
results.push({ peer: p.baseUrl, errors: ['project not found: ' + p.projectPath] });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Resolve the token only from a validly-named env var (never an arbitrary process secret).
|
|
110
|
+
const token = p.tokenEnv && isValidTokenEnvName(p.tokenEnv) ? process.env[p.tokenEnv] : undefined;
|
|
111
|
+
try {
|
|
112
|
+
const r = await replicateOnce(this.db, { baseUrl: cleanBase(p.baseUrl), token, localProjectId: project.id, remoteProject: p.remoteProject || project.name }, fetchImpl);
|
|
113
|
+
results.push({ peer: p.baseUrl, project: project.name, ...r });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
results.push({ peer: p.baseUrl, project: project.name, errors: [err instanceof Error ? err.message : String(err)] });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
/** Long-running loop. Spawned detached; logs to stdout (redirected to LOG_FILE). */
|
|
122
|
+
async run() {
|
|
123
|
+
log(`replication daemon started · interval=${this.intervalMs}ms · peers=${listPeers(this.db).length}`);
|
|
124
|
+
let inFlight = false;
|
|
125
|
+
const timer = setInterval(async () => {
|
|
126
|
+
if (inFlight)
|
|
127
|
+
return; // never overlap ticks
|
|
128
|
+
inFlight = true;
|
|
129
|
+
try {
|
|
130
|
+
const results = await this.tick();
|
|
131
|
+
for (const r of results) {
|
|
132
|
+
if (r.errors.length || (r.pulled || 0) + (r.pushed || 0) > 0) {
|
|
133
|
+
log(`tick: ${JSON.stringify(r)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
log(`tick error: ${e instanceof Error ? e.message : String(e)}`);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
inFlight = false;
|
|
142
|
+
}
|
|
143
|
+
}, this.intervalMs);
|
|
144
|
+
const shutdown = (signal) => {
|
|
145
|
+
clearInterval(timer);
|
|
146
|
+
log(`${signal} — shutting down`);
|
|
147
|
+
try {
|
|
148
|
+
this.db.close?.();
|
|
149
|
+
}
|
|
150
|
+
catch { /* best-effort WAL checkpoint + close */ }
|
|
151
|
+
process.exit(0);
|
|
152
|
+
};
|
|
153
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
154
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ==================== MANAGER ====================
|
|
158
|
+
export class ReplicationManager {
|
|
159
|
+
daemonEntrypoint;
|
|
160
|
+
constructor(daemonEntrypoint) {
|
|
161
|
+
this.daemonEntrypoint = daemonEntrypoint;
|
|
162
|
+
ensureWyrmDir();
|
|
163
|
+
}
|
|
164
|
+
start(opts = {}) {
|
|
165
|
+
if (this.isAlive())
|
|
166
|
+
return { ok: false, pid: this.readPid(), reason: 'already running' };
|
|
167
|
+
rotateLogIfBig();
|
|
168
|
+
const mins = Number(opts.interval_minutes);
|
|
169
|
+
const intervalMs = Number.isFinite(mins) && mins > 0 ? Math.round(mins * 60_000) : DEFAULT_INTERVAL_MS;
|
|
170
|
+
// One shared fd for both stdout+stderr; try/finally so a spawn throw can't leak it.
|
|
171
|
+
const logFd = openSync(LOG_FILE, 'a');
|
|
172
|
+
try {
|
|
173
|
+
const child = spawn(process.execPath, [this.daemonEntrypoint, String(intervalMs)], {
|
|
174
|
+
detached: true,
|
|
175
|
+
stdio: ['ignore', logFd, logFd],
|
|
176
|
+
env: { ...process.env },
|
|
177
|
+
});
|
|
178
|
+
child.unref();
|
|
179
|
+
if (typeof child.pid !== 'number')
|
|
180
|
+
return { ok: false, reason: 'spawn returned no pid' };
|
|
181
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
182
|
+
return { ok: true, pid: child.pid };
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
closeSync(logFd);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
stop() {
|
|
189
|
+
if (!this.isAlive()) {
|
|
190
|
+
this.cleanPid();
|
|
191
|
+
return { ok: false, reason: 'not running' };
|
|
192
|
+
}
|
|
193
|
+
const pid = this.readPid();
|
|
194
|
+
if (!pid)
|
|
195
|
+
return { ok: false, reason: 'pid file unreadable' };
|
|
196
|
+
try {
|
|
197
|
+
process.kill(pid, 'SIGTERM');
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
status() {
|
|
205
|
+
const running = this.isAlive();
|
|
206
|
+
return { running, pid: running ? this.readPid() ?? undefined : undefined };
|
|
207
|
+
}
|
|
208
|
+
isAlive() {
|
|
209
|
+
if (!existsSync(PID_FILE))
|
|
210
|
+
return false;
|
|
211
|
+
const pid = this.readPid();
|
|
212
|
+
if (!pid)
|
|
213
|
+
return false;
|
|
214
|
+
try {
|
|
215
|
+
process.kill(pid, LIVENESS_SIGNAL);
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
readPid() {
|
|
223
|
+
try {
|
|
224
|
+
const n = Number(readFileSync(PID_FILE, 'utf8').trim());
|
|
225
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
cleanPid() {
|
|
232
|
+
try {
|
|
233
|
+
unlinkSync(PID_FILE);
|
|
234
|
+
}
|
|
235
|
+
catch { /* idempotent */ }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// ==================== HELPERS ====================
|
|
239
|
+
function ensureWyrmDir() {
|
|
240
|
+
if (!existsSync(WYRM_DIR))
|
|
241
|
+
mkdirSync(WYRM_DIR, { recursive: true });
|
|
242
|
+
}
|
|
243
|
+
function log(msg) {
|
|
244
|
+
process.stdout.write(`[${new Date().toISOString()}] ${msg}\n`);
|
|
245
|
+
}
|
|
246
|
+
function rotateLogIfBig() {
|
|
247
|
+
if (!existsSync(LOG_FILE))
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
if (statSync(LOG_FILE).size > LOG_ROTATE_BYTES) {
|
|
251
|
+
const rotated = `${LOG_FILE}.1`;
|
|
252
|
+
try {
|
|
253
|
+
unlinkSync(rotated);
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
renameSync(LOG_FILE, rotated);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch { /* non-fatal */ }
|
|
260
|
+
}
|
|
261
|
+
export { DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS, PEERS_META_KEY };
|
|
262
|
+
//# sourceMappingURL=replication-daemon.js.map
|
package/dist/rerank.js
CHANGED
|
@@ -1 +1,142 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Cross-encoder rerank stage for hybrid recall — Ghost Protocol local
|
|
3
|
+
* multi-model ROUTER, subsystem wyrm-routing-rerank.
|
|
4
|
+
*
|
|
5
|
+
* The optional final leg of `recallHybrid`: FTS ⊕ vector fusion produces a
|
|
6
|
+
* top-K candidate set, this re-scores each candidate AGAINST the query with a
|
|
7
|
+
* true cross-encoder (query+document attend jointly, unlike the bi-encoder
|
|
8
|
+
* cosine the fusion uses), and the caller keeps the top-N. Cross-encoders are
|
|
9
|
+
* the strongest reranking signal in IR; the cost is a forward pass per
|
|
10
|
+
* candidate, which is why this is depth-bounded (top-K, not the whole corpus)
|
|
11
|
+
* and OFF by default.
|
|
12
|
+
*
|
|
13
|
+
* MODEL: bge-reranker-v2-m3 served by llama.cpp's OpenAI-style `/v1/rerank`
|
|
14
|
+
* endpoint (Jina/Cohere reranker wire shape:
|
|
15
|
+
* POST { model, query, documents: string[], top_n } ->
|
|
16
|
+
* { results: [{ index, relevance_score }, ...] }).
|
|
17
|
+
* Configured ENTIRELY by environment so Wyrm carries NO new dependency and the
|
|
18
|
+
* reranker is opt-in:
|
|
19
|
+
* WYRM_RERANK_MODEL — model id passed to /v1/rerank. UNSET ⇒ rerank is a
|
|
20
|
+
* no-op (the whole feature is gated on this).
|
|
21
|
+
* WYRM_RERANK_URL — base URL of the llama.cpp server
|
|
22
|
+
* (default http://localhost:8088).
|
|
23
|
+
* WYRM_RERANK_TIMEOUT — per-request timeout in ms (default 500).
|
|
24
|
+
*
|
|
25
|
+
* RESILIENCE CONTRACT (the recallHybrid `rerank:true` promise): this NEVER
|
|
26
|
+
* throws and NEVER hangs. On ANY error — model unset, endpoint down, timeout,
|
|
27
|
+
* malformed body, partial coverage — it returns null and the caller falls back
|
|
28
|
+
* to the fusion order. A reranker that is slow or absent must degrade recall to
|
|
29
|
+
* "exactly the fusion result", never to an error or a stall.
|
|
30
|
+
*
|
|
31
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
32
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
33
|
+
*/
|
|
34
|
+
/** Max documents sent in one /v1/rerank request (the task's batch<=5 cap). A
|
|
35
|
+
* larger candidate set is split into sequential batches of this size. */
|
|
36
|
+
const RERANK_BATCH = 5;
|
|
37
|
+
/** Default per-request timeout (ms) when WYRM_RERANK_TIMEOUT is unset/invalid. */
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 500;
|
|
39
|
+
/** Default llama.cpp server base URL when WYRM_RERANK_URL is unset. */
|
|
40
|
+
const DEFAULT_RERANK_URL = 'http://localhost:8088';
|
|
41
|
+
/** True when a rerank model is configured (the feature is opt-in via env). */
|
|
42
|
+
export function rerankConfigured() {
|
|
43
|
+
return !!(process.env.WYRM_RERANK_MODEL && process.env.WYRM_RERANK_MODEL.trim());
|
|
44
|
+
}
|
|
45
|
+
function rerankTimeoutMs() {
|
|
46
|
+
const raw = Number(process.env.WYRM_RERANK_TIMEOUT);
|
|
47
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS;
|
|
48
|
+
}
|
|
49
|
+
function rerankBaseUrl() {
|
|
50
|
+
const raw = (process.env.WYRM_RERANK_URL || '').trim();
|
|
51
|
+
return (raw || DEFAULT_RERANK_URL).replace(/\/+$/, '');
|
|
52
|
+
}
|
|
53
|
+
/** One /v1/rerank call over a <=RERANK_BATCH slice. Returns an id→score map,
|
|
54
|
+
* or null on any failure (timeout, non-200, malformed body). Never throws. */
|
|
55
|
+
async function rerankBatch(model, baseUrl, query, batch, timeoutMs) {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(`${baseUrl}/v1/rerank`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
model,
|
|
62
|
+
query,
|
|
63
|
+
documents: batch.map((c) => c.text),
|
|
64
|
+
top_n: batch.length,
|
|
65
|
+
}),
|
|
66
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
return null;
|
|
70
|
+
const data = (await res.json());
|
|
71
|
+
const results = data?.results;
|
|
72
|
+
if (!Array.isArray(results) || results.length === 0)
|
|
73
|
+
return null;
|
|
74
|
+
const out = new Map();
|
|
75
|
+
for (const r of results) {
|
|
76
|
+
// The reranker returns `index` into the documents array we sent; map it
|
|
77
|
+
// back to the candidate id. Tolerate `score` as an alias for some builds.
|
|
78
|
+
if (typeof r.index !== 'number' || r.index < 0 || r.index >= batch.length)
|
|
79
|
+
continue;
|
|
80
|
+
const score = typeof r.relevance_score === 'number' ? r.relevance_score
|
|
81
|
+
: typeof r.score === 'number' ? r.score : null;
|
|
82
|
+
if (score == null || !Number.isFinite(score))
|
|
83
|
+
continue;
|
|
84
|
+
out.set(batch[r.index].id, score);
|
|
85
|
+
}
|
|
86
|
+
return out.size > 0 ? out : null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// timeout / connection refused / abort / JSON error — all degrade to null.
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Rerank `candidates` against `query` with the configured cross-encoder and
|
|
95
|
+
* return them ordered best-first (highest relevance_score). Sends <=batchSize
|
|
96
|
+
* documents per /v1/rerank call (sequential batches). Returns null — the
|
|
97
|
+
* signal for "fall back to fusion order" — when:
|
|
98
|
+
* - no model is configured (WYRM_RERANK_MODEL unset),
|
|
99
|
+
* - there are no candidates,
|
|
100
|
+
* - the endpoint is unreachable/slow/erroring for EVERY batch,
|
|
101
|
+
* - or no candidate received a usable score.
|
|
102
|
+
* A PARTIAL success (some batches scored, some not) still returns an order:
|
|
103
|
+
* scored candidates rank by their cross-encoder score, unscored ones keep
|
|
104
|
+
* their original (fusion) order and trail the scored set — strictly no worse
|
|
105
|
+
* than the fusion order, and never a thrown error.
|
|
106
|
+
*/
|
|
107
|
+
export async function rerankCandidates(query, candidates, opts = {}) {
|
|
108
|
+
const model = process.env.WYRM_RERANK_MODEL?.trim();
|
|
109
|
+
if (!model)
|
|
110
|
+
return null; // feature gated on the model env
|
|
111
|
+
if (!query || candidates.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const baseUrl = rerankBaseUrl();
|
|
114
|
+
const timeoutMs = opts.timeoutMs ?? rerankTimeoutMs();
|
|
115
|
+
const batchSize = Math.max(1, Math.min(RERANK_BATCH, opts.batchSize ?? RERANK_BATCH));
|
|
116
|
+
const scores = new Map();
|
|
117
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
118
|
+
const batch = candidates.slice(i, i + batchSize);
|
|
119
|
+
const batchScores = await rerankBatch(model, baseUrl, query, batch, timeoutMs);
|
|
120
|
+
if (batchScores)
|
|
121
|
+
for (const [id, s] of batchScores)
|
|
122
|
+
scores.set(id, s);
|
|
123
|
+
}
|
|
124
|
+
if (scores.size === 0)
|
|
125
|
+
return null; // nothing scored → fusion order wins
|
|
126
|
+
// Scored candidates first (by cross-encoder score desc); any unscored ones
|
|
127
|
+
// (a failed batch) keep their original relative order and trail — so the
|
|
128
|
+
// result is never worse than the incoming fusion order.
|
|
129
|
+
const origIndex = new Map(candidates.map((c, i) => [c.id, i]));
|
|
130
|
+
return [...candidates].sort((a, b) => {
|
|
131
|
+
const sa = scores.get(a.id);
|
|
132
|
+
const sb = scores.get(b.id);
|
|
133
|
+
if (sa != null && sb != null)
|
|
134
|
+
return sb - sa;
|
|
135
|
+
if (sa != null)
|
|
136
|
+
return -1; // scored before unscored
|
|
137
|
+
if (sb != null)
|
|
138
|
+
return 1;
|
|
139
|
+
return (origIndex.get(a.id) ?? 0) - (origIndex.get(b.id) ?? 0); // stable
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=rerank.js.map
|