@remnic/core 9.3.738 → 9.3.739

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.
@@ -509,14 +509,14 @@ import {
509
509
  } from "./chunk-PVGDJXVK.js";
510
510
 
511
511
  // src/orchestrator.ts
512
- import path4 from "path";
512
+ import path5 from "path";
513
513
  import os2 from "os";
514
514
  import { createHash as createHash2, randomBytes } from "crypto";
515
515
  import { existsSync, readFileSync } from "fs";
516
516
  import {
517
517
  lstat,
518
518
  mkdir as mkdir3,
519
- readdir,
519
+ readdir as readdir2,
520
520
  readFile as readFile2,
521
521
  realpath,
522
522
  stat,
@@ -1942,6 +1942,452 @@ ${lines.join("\n\n")}`;
1942
1942
  }
1943
1943
  };
1944
1944
 
1945
+ // src/orchestration/conversation-index-coordinator.ts
1946
+ import { readdir } from "fs/promises";
1947
+ import path3 from "path";
1948
+ var ConversationIndexCoordinator = class {
1949
+ config;
1950
+ getTranscript;
1951
+ getBackend;
1952
+ indexDir;
1953
+ lastUpdateAtMs = /* @__PURE__ */ new Map();
1954
+ constructor(options) {
1955
+ this.config = options.config;
1956
+ this.getTranscript = options.getTranscript;
1957
+ this.getBackend = options.getBackend;
1958
+ this.indexDir = options.indexDir;
1959
+ }
1960
+ /** Semantic recall over past-conversation chunks (fail-open: empty on miss). */
1961
+ async search(retrievalQuery, topK) {
1962
+ const backend = this.getBackend();
1963
+ if (backend) {
1964
+ return backend.search(retrievalQuery, topK);
1965
+ }
1966
+ return [];
1967
+ }
1968
+ /** Render conversation-recall search hits as a budgeted markdown section. */
1969
+ formatRecallSection(results, maxChars) {
1970
+ if (!Array.isArray(results) || results.length === 0) return null;
1971
+ const lines = ["## Semantic Recall (Past Conversations)", ""];
1972
+ let used = 0;
1973
+ for (const r of results) {
1974
+ if (!r?.snippet) continue;
1975
+ const chunk = `### ${r.path}
1976
+ Score: ${r.score.toFixed(3)}
1977
+
1978
+ ${r.snippet.trim()}
1979
+ `;
1980
+ if (used + chunk.length > maxChars) break;
1981
+ lines.push(chunk);
1982
+ used += chunk.length;
1983
+ }
1984
+ return used > 0 ? lines.join("\n") : null;
1985
+ }
1986
+ /** Recursively count `.md` chunk documents under a directory. */
1987
+ async countChunkDocs(dir) {
1988
+ try {
1989
+ const entries = await readdir(dir, { withFileTypes: true });
1990
+ let total = 0;
1991
+ for (const entry of entries) {
1992
+ const fullPath = path3.join(dir, entry.name);
1993
+ if (entry.isDirectory()) {
1994
+ total += await this.countChunkDocs(fullPath);
1995
+ continue;
1996
+ }
1997
+ if (entry.isFile() && entry.name.endsWith(".md")) {
1998
+ total += 1;
1999
+ }
2000
+ }
2001
+ return total;
2002
+ } catch {
2003
+ return 0;
2004
+ }
2005
+ }
2006
+ /** Read recent transcript entries and chunk them for indexing. */
2007
+ async buildChunks(sessionKey, hours = 24) {
2008
+ const entries = await this.getTranscript().readRecent(hours, sessionKey);
2009
+ const effectiveSessionKey = sessionKey ?? "all-sessions";
2010
+ return chunkTranscriptEntries(effectiveSessionKey, entries, {
2011
+ maxChars: this.config.conversationRecallMaxChars * 2,
2012
+ maxTurns: Math.max(10, this.config.hourlySummariesMaxTurnsPerRun)
2013
+ });
2014
+ }
2015
+ async getHealth() {
2016
+ const chunkDocCount = await this.countChunkDocs(this.indexDir);
2017
+ const lastUpdateAtMs = Math.max(0, ...this.lastUpdateAtMs.values());
2018
+ const lastUpdateAt = lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
2019
+ if (!resolveIndexingCapabilities(this.config).conversationIndex) {
2020
+ return {
2021
+ enabled: false,
2022
+ backend: this.config.conversationIndexBackend,
2023
+ status: "disabled",
2024
+ chunkDocCount,
2025
+ lastUpdateAt
2026
+ };
2027
+ }
2028
+ const backend = this.getBackend();
2029
+ const backendHealth = backend ? await backend.health() : {
2030
+ backend: this.config.conversationIndexBackend,
2031
+ status: "degraded"
2032
+ };
2033
+ return {
2034
+ enabled: true,
2035
+ chunkDocCount,
2036
+ lastUpdateAt,
2037
+ ...backendHealth
2038
+ };
2039
+ }
2040
+ async inspect() {
2041
+ const chunkDocCount = await this.countChunkDocs(this.indexDir);
2042
+ const lastUpdateAtMs = Math.max(0, ...this.lastUpdateAtMs.values());
2043
+ const lastUpdateAt = lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
2044
+ if (!resolveIndexingCapabilities(this.config).conversationIndex) {
2045
+ return {
2046
+ enabled: false,
2047
+ backend: this.config.conversationIndexBackend,
2048
+ status: "disabled",
2049
+ available: false,
2050
+ indexPath: this.indexDir,
2051
+ supportsIncrementalUpdate: true,
2052
+ message: "Conversation index disabled by config",
2053
+ metadata: {
2054
+ chunkCount: chunkDocCount
2055
+ },
2056
+ chunkDocCount,
2057
+ lastUpdateAt
2058
+ };
2059
+ }
2060
+ const backend = this.getBackend();
2061
+ const inspection = backend ? await backend.inspect() : {
2062
+ backend: this.config.conversationIndexBackend,
2063
+ status: "degraded",
2064
+ available: false,
2065
+ indexPath: this.indexDir,
2066
+ supportsIncrementalUpdate: true,
2067
+ message: "Conversation index backend unavailable",
2068
+ metadata: {
2069
+ chunkCount: chunkDocCount
2070
+ }
2071
+ };
2072
+ return {
2073
+ enabled: true,
2074
+ chunkDocCount,
2075
+ lastUpdateAt,
2076
+ ...inspection
2077
+ };
2078
+ }
2079
+ async update(sessionKey, hours = 24, opts) {
2080
+ if (!resolveIndexingCapabilities(this.config).conversationIndex) {
2081
+ return { chunks: 0, skipped: true, reason: "disabled", embedded: false };
2082
+ }
2083
+ const enforceMinInterval = opts?.enforceMinInterval !== false;
2084
+ if (enforceMinInterval) {
2085
+ const minIntervalMs = Math.max(
2086
+ 0,
2087
+ this.config.conversationIndexMinUpdateIntervalMs
2088
+ );
2089
+ const now = Date.now();
2090
+ const last = this.lastUpdateAtMs.get(sessionKey) ?? 0;
2091
+ const elapsed = now - last;
2092
+ if (minIntervalMs > 0 && elapsed < minIntervalMs) {
2093
+ return {
2094
+ chunks: 0,
2095
+ skipped: true,
2096
+ reason: "min_interval",
2097
+ retryAfterMs: minIntervalMs - elapsed,
2098
+ embedded: false
2099
+ };
2100
+ }
2101
+ }
2102
+ const chunks = await this.buildChunks(sessionKey, hours);
2103
+ await writeConversationChunks(this.indexDir, chunks);
2104
+ const retentionCutoffMs = Number.isFinite(this.config.conversationIndexRetentionDays) && this.config.conversationIndexRetentionDays > 0 ? Date.now() - this.config.conversationIndexRetentionDays * 24 * 60 * 60 * 1e3 : void 0;
2105
+ await cleanupConversationChunks(
2106
+ this.indexDir,
2107
+ this.config.conversationIndexRetentionDays
2108
+ );
2109
+ const shouldEmbed = opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
2110
+ let embedded = false;
2111
+ const backend = this.getBackend();
2112
+ if (backend) {
2113
+ const result = await backend.update(chunks, {
2114
+ embed: shouldEmbed,
2115
+ ...retentionCutoffMs !== void 0 ? { retentionCutoffMs } : {}
2116
+ });
2117
+ embedded = result.embedded;
2118
+ }
2119
+ this.lastUpdateAtMs.set(sessionKey, Date.now());
2120
+ return { chunks: chunks.length, skipped: false, embedded };
2121
+ }
2122
+ async rebuild(sessionKey, hours = 24, opts) {
2123
+ if (!resolveIndexingCapabilities(this.config).conversationIndex) {
2124
+ return {
2125
+ chunks: 0,
2126
+ skipped: true,
2127
+ reason: "disabled",
2128
+ embedded: false,
2129
+ rebuilt: false
2130
+ };
2131
+ }
2132
+ const chunks = await this.buildChunks(sessionKey, hours);
2133
+ await writeConversationChunks(this.indexDir, chunks);
2134
+ await cleanupConversationChunks(
2135
+ this.indexDir,
2136
+ this.config.conversationIndexRetentionDays
2137
+ );
2138
+ const shouldEmbed = opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
2139
+ let embedded = false;
2140
+ let rebuilt = false;
2141
+ const backend = this.getBackend();
2142
+ if (backend) {
2143
+ const result = await backend.rebuild(chunks, {
2144
+ embed: shouldEmbed
2145
+ });
2146
+ embedded = result.embedded;
2147
+ rebuilt = result.rebuilt;
2148
+ }
2149
+ const stamp = Date.now();
2150
+ if (sessionKey) {
2151
+ this.lastUpdateAtMs.set(sessionKey, stamp);
2152
+ } else {
2153
+ this.lastUpdateAtMs.set("__rebuild__", stamp);
2154
+ }
2155
+ return { chunks: chunks.length, skipped: false, embedded, rebuilt };
2156
+ }
2157
+ };
2158
+
2159
+ // src/orchestration/recall-rerank-coordinator.ts
2160
+ var RecallRerankCoordinator = class _RecallRerankCoordinator {
2161
+ getConfig;
2162
+ getStorage;
2163
+ readQmdResultMemory;
2164
+ memoryWorthCounterCache = /* @__PURE__ */ new Map();
2165
+ static MEMORY_WORTH_CACHE_TTL_MS = 3e4;
2166
+ trustSignalCache = /* @__PURE__ */ new Map();
2167
+ static TRUST_SIGNAL_CACHE_TTL_MS = 3e4;
2168
+ constructor(options) {
2169
+ this.getConfig = options.getConfig;
2170
+ this.getStorage = options.getStorage;
2171
+ this.readQmdResultMemory = options.readQmdResultMemory;
2172
+ }
2173
+ async applyMemoryWorthRerank(results, namespaces) {
2174
+ const counters = /* @__PURE__ */ new Map();
2175
+ const seenNamespaces = /* @__PURE__ */ new Set();
2176
+ const nowMs = Date.now();
2177
+ for (const [key, entry] of this.memoryWorthCounterCache) {
2178
+ if (nowMs - entry.at >= _RecallRerankCoordinator.MEMORY_WORTH_CACHE_TTL_MS) {
2179
+ this.memoryWorthCounterCache.delete(key);
2180
+ }
2181
+ }
2182
+ for (const ns of namespaces) {
2183
+ if (seenNamespaces.has(ns)) continue;
2184
+ seenNamespaces.add(ns);
2185
+ try {
2186
+ const cached = this.memoryWorthCounterCache.get(ns);
2187
+ let nsMap;
2188
+ if (cached && nowMs - cached.at < _RecallRerankCoordinator.MEMORY_WORTH_CACHE_TTL_MS) {
2189
+ nsMap = cached.counters;
2190
+ } else {
2191
+ const storage = await this.getStorage(ns);
2192
+ const memories = await storage.readAllMemories();
2193
+ nsMap = buildMemoryWorthCounterMap(memories);
2194
+ this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
2195
+ }
2196
+ for (const [path6, c] of nsMap) counters.set(path6, c);
2197
+ } catch (err) {
2198
+ log.debug("memory-worth: failed to read namespace, skipping", {
2199
+ namespace: ns,
2200
+ error: err.message
2201
+ });
2202
+ }
2203
+ }
2204
+ const missing = results.filter((r) => !counters.has(r.path));
2205
+ if (missing.length > 0) {
2206
+ let reader = null;
2207
+ for (const ns of namespaces) {
2208
+ try {
2209
+ reader = await this.getStorage(ns);
2210
+ break;
2211
+ } catch {
2212
+ }
2213
+ }
2214
+ if (reader) {
2215
+ for (const r of missing) {
2216
+ try {
2217
+ const memory = await this.readQmdResultMemory(r.path, reader, namespaces);
2218
+ if (!memory) continue;
2219
+ const fm = memory.frontmatter;
2220
+ if (fm.mw_success === void 0 && fm.mw_fail === void 0) continue;
2221
+ counters.set(r.path, {
2222
+ mw_success: fm.mw_success,
2223
+ mw_fail: fm.mw_fail,
2224
+ lastAccessed: fm.lastAccessed
2225
+ });
2226
+ } catch (err) {
2227
+ log.debug("memory-worth: direct path lookup failed", {
2228
+ path: r.path,
2229
+ error: err.message
2230
+ });
2231
+ }
2232
+ }
2233
+ }
2234
+ }
2235
+ if (counters.size === 0) return results;
2236
+ const rankedInputs = results.map((r, i) => ({
2237
+ path: r.path,
2238
+ // Large positive rank score so multiplier math stays well-scaled and
2239
+ // we never hit zero; descending so earlier items rank higher.
2240
+ score: results.length - i
2241
+ }));
2242
+ const config = this.getConfig();
2243
+ const filtered = applyMemoryWorthFilter(rankedInputs, {
2244
+ counters,
2245
+ now: /* @__PURE__ */ new Date(),
2246
+ halfLifeMs: config.recallMemoryWorthHalfLifeMs > 0 ? config.recallMemoryWorthHalfLifeMs : void 0
2247
+ });
2248
+ const byPath = new Map(results.map((r) => [r.path, r]));
2249
+ const reordered = [];
2250
+ for (const item of filtered) {
2251
+ const original = byPath.get(item.path);
2252
+ if (original) reordered.push(original);
2253
+ }
2254
+ return reordered;
2255
+ }
2256
+ /**
2257
+ * Issue #1577 — unified TrustScore recall stage. Thin wiring over the pure
2258
+ * {@link applyTrustScoreStage} scorer + the {@link buildTrustSignalsForRerank}
2259
+ * signal builder. The stage subsumes the Memory Worth multiplier — the
2260
+ * orchestrator runs exactly one of the two (mutual exclusion, rule 39; the
2261
+ * double-multiplier test in trust-score-stage.test.ts pins it structurally).
2262
+ *
2263
+ * Returns the admitted results AND the per-path trust map (including
2264
+ * quarantined items) so the caller can: (a) render epistemic hedges, (b)
2265
+ * surface quarantined items in X-ray with a reason (rule 34), and (c) filter
2266
+ * quarantined paths from fallback recall branches. The trust map is a
2267
+ * per-recall local — never instance state — so concurrent recalls cannot
2268
+ * race on it (review: shared-trust-map concurrency).
2269
+ */
2270
+ async applyTrustScoreRerank(results, namespaces) {
2271
+ if (results.length === 0) return { results, trustByPath: null };
2272
+ const config = this.getConfig();
2273
+ const now = /* @__PURE__ */ new Date();
2274
+ const halfLifeDays = config.recallMemoryWorthHalfLifeMs > 0 ? config.recallMemoryWorthHalfLifeMs / (24 * 60 * 60 * 1e3) : void 0;
2275
+ let fallbackReader = null;
2276
+ const signals = await buildTrustSignalsForRerank(
2277
+ results.map((r) => r.path),
2278
+ namespaces,
2279
+ {
2280
+ readNamespaceMemories: async (ns) => (await this.getStorage(ns)).readAllMemories(),
2281
+ readMemoryFrontmatter: async (path6) => {
2282
+ if (!fallbackReader) {
2283
+ for (const ns of namespaces) {
2284
+ try {
2285
+ fallbackReader = await this.getStorage(ns);
2286
+ break;
2287
+ } catch {
2288
+ }
2289
+ }
2290
+ }
2291
+ if (!fallbackReader) return null;
2292
+ const memory = await this.readQmdResultMemory(path6, fallbackReader, namespaces);
2293
+ return memory ? memory.frontmatter : null;
2294
+ }
2295
+ },
2296
+ { cache: this.trustSignalCache, ttlMs: _RecallRerankCoordinator.TRUST_SIGNAL_CACHE_TTL_MS },
2297
+ now,
2298
+ {
2299
+ recencyHalfLifeDays: halfLifeDays,
2300
+ logDebug: (message, context) => log.debug(message, context)
2301
+ }
2302
+ );
2303
+ if (signals.size === 0) {
2304
+ return { results, trustByPath: null };
2305
+ }
2306
+ const rankedInputs = results.map((r, i) => ({ path: r.path, score: results.length - i }));
2307
+ const stage = applyTrustScoreStage(rankedInputs, {
2308
+ signals,
2309
+ weights: config.trustScoreWeights,
2310
+ minMultiplier: config.trustScoreMinMultiplier,
2311
+ maxMultiplier: config.trustScoreMaxMultiplier,
2312
+ quarantine: config.trustScoreQuarantine
2313
+ });
2314
+ const trustByPath = new Map(stage.all.map((item) => [item.path, item]));
2315
+ const byPath = new Map(results.map((r) => [r.path, r]));
2316
+ const admitted = stage.admitted.map((item) => byPath.get(item.path)).filter((r) => r !== void 0);
2317
+ return { results: admitted, trustByPath };
2318
+ }
2319
+ /**
2320
+ * Issue #1577 — apply the TrustScore stage (or, when trust is off, the Memory
2321
+ * Worth multiplier fallback) to ONE recall branch's results, returning the
2322
+ * scored results + the per-path trust map. Thin wiring over
2323
+ * {@link applyTrustScoreRerank} so every recall path — hot QMD, embedding
2324
+ * fallback, recent scan — applies the SAME multiplier gate (rule 41: a
2325
+ * feature gate must apply across ALL parallel recall paths). TrustScore
2326
+ * subsumes Memory Worth; exactly one runs (rule 39). Fail-open on lookup
2327
+ * errors so a storage hiccup never breaks a fallback path.
2328
+ */
2329
+ async applyTrustScoreToBranch(results, namespaces, caps, label) {
2330
+ if (caps.recallTrustScore && results.length > 0) {
2331
+ try {
2332
+ return await this.applyTrustScoreRerank(results, namespaces);
2333
+ } catch (err) {
2334
+ log.debug(`trust-score stage (${label}) failed open`, {
2335
+ error: err.message
2336
+ });
2337
+ }
2338
+ } else if (caps.recallMemoryWorthFilter && results.length > 0) {
2339
+ try {
2340
+ const filtered = await this.applyMemoryWorthRerank(results, namespaces);
2341
+ return { results: filtered, trustByPath: null };
2342
+ } catch (err) {
2343
+ log.debug(`memory-worth filter (${label}) failed open`, {
2344
+ error: err.message
2345
+ });
2346
+ }
2347
+ }
2348
+ return { results, trustByPath: null };
2349
+ }
2350
+ diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps = resolveCapabilities(this.getConfig())) {
2351
+ const safeLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
2352
+ if (!Array.isArray(results) || results.length === 0) return [];
2353
+ if (safeLimit === 0) return [];
2354
+ const boosted = caps.recallReasoningTraceBoost && typeof retrievalQuery === "string" ? applyReasoningTraceBoost(results, {
2355
+ enabled: true,
2356
+ query: retrievalQuery
2357
+ }) : results;
2358
+ const diversified = this.applyMmrToQmdResults(sectionId, boosted, caps);
2359
+ return diversified.slice(0, safeLimit);
2360
+ }
2361
+ /**
2362
+ * Apply Maximal Marginal Relevance to a section's ordered candidate list.
2363
+ *
2364
+ * Operates per-section so one redundant cluster cannot dominate a section,
2365
+ * and so one section's MMR pass cannot starve other sections. Returns the
2366
+ * input unchanged when disabled, when there are fewer than 2 candidates, or
2367
+ * when no budget information is available.
2368
+ */
2369
+ applyMmrToQmdResults(sectionId, results, caps = resolveCapabilities(this.getConfig())) {
2370
+ if (!caps.recallMmr) return results;
2371
+ if (!Array.isArray(results) || results.length < 2) return results;
2372
+ const config = this.getConfig();
2373
+ const configuredTopN = config.recallMmrTopN;
2374
+ const topN = typeof configuredTopN === "number" && Number.isFinite(configuredTopN) ? Math.max(0, Math.floor(configuredTopN)) : 40;
2375
+ if (topN === 0) return results;
2376
+ const lambda = config.recallMmrLambda ?? 0.7;
2377
+ const { reordered, diversity } = reorderRecallResultsWithMmr(results, {
2378
+ lambda,
2379
+ topN
2380
+ });
2381
+ try {
2382
+ log.info(
2383
+ `recall_mmr: section=${sectionId} kept=${diversity.kept}/${diversity.considered} headReorderCount=${diversity.headReorderCount} avgSimBefore=${diversity.avgPairwiseSimBefore.toFixed(3)} avgSimAfter=${diversity.avgPairwiseSimAfter.toFixed(3)} lambda=${lambda.toFixed(2)}`
2384
+ );
2385
+ } catch {
2386
+ }
2387
+ return reordered;
2388
+ }
2389
+ };
2390
+
1945
2391
  // src/maintenance/pattern-reinforcement.ts
1946
2392
  function patternReinforcementKey(content) {
1947
2393
  return content.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 200);
@@ -2275,7 +2721,7 @@ function generateResolverDocument(taxonomy) {
2275
2721
 
2276
2722
  // src/taxonomy/taxonomy-loader.ts
2277
2723
  import { readFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2278
- import path3 from "path";
2724
+ import path4 from "path";
2279
2725
  var TAXONOMY_DIR = ".taxonomy";
2280
2726
  var TAXONOMY_FILE = "taxonomy.json";
2281
2727
  var MAX_SLUG_LENGTH = 32;
@@ -2373,7 +2819,7 @@ function validateTaxonomy(taxonomy) {
2373
2819
  }
2374
2820
  }
2375
2821
  async function loadTaxonomy(memoryDir) {
2376
- const taxonomyPath = path3.join(memoryDir, TAXONOMY_DIR, TAXONOMY_FILE);
2822
+ const taxonomyPath = path4.join(memoryDir, TAXONOMY_DIR, TAXONOMY_FILE);
2377
2823
  let raw;
2378
2824
  try {
2379
2825
  raw = await readFile(taxonomyPath, "utf-8");
@@ -2423,16 +2869,16 @@ async function loadTaxonomy(memoryDir) {
2423
2869
  }
2424
2870
  async function saveTaxonomy(memoryDir, taxonomy) {
2425
2871
  validateTaxonomy(taxonomy);
2426
- const dir = path3.join(memoryDir, TAXONOMY_DIR);
2872
+ const dir = path4.join(memoryDir, TAXONOMY_DIR);
2427
2873
  await mkdir2(dir, { recursive: true });
2428
- const filePath = path3.join(dir, TAXONOMY_FILE);
2874
+ const filePath = path4.join(dir, TAXONOMY_FILE);
2429
2875
  await writeFile2(filePath, JSON.stringify(taxonomy, null, 2) + "\n", "utf-8");
2430
2876
  }
2431
2877
  function getTaxonomyDir(memoryDir) {
2432
- return path3.join(memoryDir, TAXONOMY_DIR);
2878
+ return path4.join(memoryDir, TAXONOMY_DIR);
2433
2879
  }
2434
2880
  function getTaxonomyFilePath(memoryDir) {
2435
- return path3.join(memoryDir, TAXONOMY_DIR, TAXONOMY_FILE);
2881
+ return path4.join(memoryDir, TAXONOMY_DIR, TAXONOMY_FILE);
2436
2882
  }
2437
2883
 
2438
2884
  // src/wearables/registry.ts
@@ -3333,7 +3779,7 @@ async function raceRecallAbort(promise, signal, message = "recall aborted") {
3333
3779
  }
3334
3780
  }
3335
3781
  function qmdCollectionPathParts(resultPath) {
3336
- if (!resultPath || path4.isAbsolute(resultPath)) return null;
3782
+ if (!resultPath || path5.isAbsolute(resultPath)) return null;
3337
3783
  const normalized = resultPath.replace(/\\/g, "/").replace(/^\/+/, "");
3338
3784
  const slashIndex = normalized.indexOf("/");
3339
3785
  if (slashIndex <= 0 || slashIndex >= normalized.length - 1) return null;
@@ -3346,9 +3792,9 @@ function qmdCollectionPathParts(resultPath) {
3346
3792
  }
3347
3793
  function qmdResultPathCandidates(storageDir, resultPath) {
3348
3794
  const candidates = /* @__PURE__ */ new Set();
3349
- const storageRoot = path4.resolve(storageDir);
3795
+ const storageRoot = path5.resolve(storageDir);
3350
3796
  const addCandidate = (candidate) => {
3351
- const resolved = path4.resolve(candidate);
3797
+ const resolved = path5.resolve(candidate);
3352
3798
  if (isPathInsideStorageRoot(storageRoot, resolved)) {
3353
3799
  candidates.add(resolved);
3354
3800
  }
@@ -3356,12 +3802,12 @@ function qmdResultPathCandidates(storageDir, resultPath) {
3356
3802
  const addRelativeCandidates = (relativePath) => {
3357
3803
  const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
3358
3804
  if (!normalized) return;
3359
- addCandidate(path4.join(storageRoot, normalized));
3805
+ addCandidate(path5.join(storageRoot, normalized));
3360
3806
  if (/^\d{4}-\d{2}-\d{2}\//.test(normalized)) {
3361
- addCandidate(path4.join(storageRoot, "facts", normalized));
3807
+ addCandidate(path5.join(storageRoot, "facts", normalized));
3362
3808
  }
3363
3809
  };
3364
- if (path4.isAbsolute(resultPath)) {
3810
+ if (path5.isAbsolute(resultPath)) {
3365
3811
  addCandidate(resultPath);
3366
3812
  } else {
3367
3813
  addRelativeCandidates(resultPath);
@@ -3478,7 +3924,7 @@ async function qmdStartupCollectionCheckWithTimeout(promise, controller, label)
3478
3924
  return await Promise.race([checkedPromise, timeoutPromise]);
3479
3925
  }
3480
3926
  function defaultWorkspaceDir() {
3481
- return path4.join(os2.homedir(), ".openclaw", "workspace");
3927
+ return path5.join(os2.homedir(), ".openclaw", "workspace");
3482
3928
  }
3483
3929
  function sanitizeSessionKeyForFilename(sessionKey) {
3484
3930
  const readable = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
@@ -3755,11 +4201,11 @@ function mergeGraphExpandedResults(primary, expanded) {
3755
4201
  return Array.from(mergedByPath.values());
3756
4202
  }
3757
4203
  function graphPathRelativeToStorage(storageDir, candidatePath) {
3758
- const absolutePath = path4.isAbsolute(candidatePath) ? candidatePath : path4.resolve(storageDir, candidatePath);
3759
- const rel = path4.relative(storageDir, absolutePath);
4204
+ const absolutePath = path5.isAbsolute(candidatePath) ? candidatePath : path5.resolve(storageDir, candidatePath);
4205
+ const rel = path5.relative(storageDir, absolutePath);
3760
4206
  if (!rel || rel === ".") return null;
3761
4207
  if (rel.startsWith("..")) return null;
3762
- return rel.split(path4.sep).join("/");
4208
+ return rel.split(path5.sep).join("/");
3763
4209
  }
3764
4210
  function normalizeGraphActivationScore(score) {
3765
4211
  const bounded = Number.isFinite(score) && score > 0 ? score : 0;
@@ -3904,7 +4350,7 @@ function buildMemoryPathById(allMemsForGraph, storageDir) {
3904
4350
  for (const mem of allMemsForGraph ?? []) {
3905
4351
  const id = mem.frontmatter.id;
3906
4352
  if (!id) continue;
3907
- pathById.set(id, path4.relative(storageDir, mem.path));
4353
+ pathById.set(id, path5.relative(storageDir, mem.path));
3908
4354
  }
3909
4355
  return pathById;
3910
4356
  }
@@ -3912,7 +4358,7 @@ function appendMemoryToGraphContext(options) {
3912
4358
  if (!Array.isArray(options.allMemsForGraph)) return;
3913
4359
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
3914
4360
  options.allMemsForGraph.push({
3915
- path: path4.join(options.storageDir, options.memoryRelPath),
4361
+ path: path5.join(options.storageDir, options.memoryRelPath),
3916
4362
  content: options.content,
3917
4363
  frontmatter: {
3918
4364
  id: options.memoryId,
@@ -3932,16 +4378,16 @@ function resolvePersistedMemoryRelativePath(options) {
3932
4378
  const persisted = options.pathById.get(options.memoryId);
3933
4379
  if (persisted) return persisted;
3934
4380
  if (options.category === "correction") {
3935
- return path4.join("corrections", `${options.memoryId}.md`);
4381
+ return path5.join("corrections", `${options.memoryId}.md`);
3936
4382
  }
3937
4383
  const subtree = categoryDirName(options.category);
3938
4384
  const idParts = options.memoryId.split("-");
3939
4385
  const maybeTimestamp = Number(idParts[1]);
3940
4386
  if (Number.isFinite(maybeTimestamp) && maybeTimestamp > 0) {
3941
4387
  const day = new Date(maybeTimestamp).toISOString().slice(0, 10);
3942
- return path4.join(subtree, day, `${options.memoryId}.md`);
4388
+ return path5.join(subtree, day, `${options.memoryId}.md`);
3943
4389
  }
3944
- return path4.join(subtree, `${options.memoryId}.md`);
4390
+ return path5.join(subtree, `${options.memoryId}.md`);
3945
4391
  }
3946
4392
  var Orchestrator = class _Orchestrator {
3947
4393
  storage;
@@ -4048,25 +4494,6 @@ var Orchestrator = class _Orchestrator {
4048
4494
  /** Lossless Context Management engine — proactive session archive + DAG summarization. */
4049
4495
  lcmEngine = null;
4050
4496
  rerankCache = new RerankCache();
4051
- /**
4052
- * Short-TTL cache for Memory Worth counter lookups so interactive recall
4053
- * doesn't trigger a full `readAllMemories` scan per query. Keyed by
4054
- * namespace; the filter unions across namespaces at query time. The TTL
4055
- * is intentionally short (seconds, not minutes) because counters are
4056
- * mutated by `recordMemoryOutcome` asynchronously and we'd rather serve
4057
- * a 30-second-stale worth score than a stable-but-wrong one.
4058
- */
4059
- memoryWorthCounterCache = /* @__PURE__ */ new Map();
4060
- static MEMORY_WORTH_CACHE_TTL_MS = 3e4;
4061
- /**
4062
- * Issue #1577 — per-namespace TrustScore signal map cache. Same TTL/shape
4063
- * discipline as {@link memoryWorthCounterCache}: seconds-scale, evicted on
4064
- * every call, keyed by namespace. Holds the frontmatter-derived signals
4065
- * (worth, provenance, faithfulness, corroboration, recency) so the trust
4066
- * stage doesn't trigger a fresh `readAllMemories` scan per query.
4067
- */
4068
- trustSignalCache = /* @__PURE__ */ new Map();
4069
- static TRUST_SIGNAL_CACHE_TTL_MS = 3e4;
4070
4497
  /**
4071
4498
  * Per-session workspace selections keyed by sessionKey.
4072
4499
  * Set by the before_agent_start hook so recall() uses the correct
@@ -4116,13 +4543,18 @@ var Orchestrator = class _Orchestrator {
4116
4543
  * to RecallResultFormatter.
4117
4544
  */
4118
4545
  recallResultFormatter;
4546
+ /**
4547
+ * Issue #1526: conversation-index subsystem moved to
4548
+ * ConversationIndexCoordinator.
4549
+ */
4550
+ conversationIndexCoordinator;
4551
+ recallRerankCoordinator;
4119
4552
  heartbeatObserverChains = /* @__PURE__ */ new Map();
4120
4553
  recentExtractionFingerprints = /* @__PURE__ */ new Map();
4121
4554
  consolidationObservers = /* @__PURE__ */ new Set();
4122
4555
  wearablesServiceInstance = null;
4123
4556
  wearablesAutoSyncHandle = null;
4124
4557
  lastQmdReprobeAtMs = 0;
4125
- conversationIndexLastUpdateAtMs = /* @__PURE__ */ new Map();
4126
4558
  lastFileHygieneRunAtMs = 0;
4127
4559
  // Pattern-reinforcement cadence gate (issue #687 PR 2/4). Tracks the
4128
4560
  // last successful run so `runPatternReinforcement` can short-circuit
@@ -4409,7 +4841,7 @@ var Orchestrator = class _Orchestrator {
4409
4841
  const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
4410
4842
  if (ns !== defaultNs && !isSafeRouteNamespace(ns)) return;
4411
4843
  if (!this.storageDirMatchesNamespaceHint(ns, storageDir)) return;
4412
- const resolvedStorageDir = path4.resolve(storageDir);
4844
+ const resolvedStorageDir = path5.resolve(storageDir);
4413
4845
  let hints = this.namespaceStorageDirHints.get(resolvedStorageDir);
4414
4846
  if (!hints) {
4415
4847
  hints = /* @__PURE__ */ new Set();
@@ -4420,21 +4852,21 @@ var Orchestrator = class _Orchestrator {
4420
4852
  storageDirMatchesNamespaceHint(namespace, storageDir) {
4421
4853
  const ns = normalizeNamespaceIdentity(namespace);
4422
4854
  if (!ns) return false;
4423
- const resolvedStorageDir = path4.resolve(storageDir);
4424
- const resolvedMemoryDir = path4.resolve(this.config.memoryDir);
4855
+ const resolvedStorageDir = path5.resolve(storageDir);
4856
+ const resolvedMemoryDir = path5.resolve(this.config.memoryDir);
4425
4857
  const defaultNs = normalizeNamespaceIdentity(this.config.defaultNamespace);
4426
4858
  if (resolvedStorageDir === resolvedMemoryDir) return ns === defaultNs;
4427
- const resolvedNamespacesDir = path4.join(resolvedMemoryDir, "namespaces");
4859
+ const resolvedNamespacesDir = path5.join(resolvedMemoryDir, "namespaces");
4428
4860
  if (!isPathInsideStorageRoot(resolvedNamespacesDir, resolvedStorageDir)) return false;
4429
- const rawRoot = path4.resolve(resolvedNamespacesDir, ns);
4430
- const tokenRoot = path4.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
4861
+ const rawRoot = path5.resolve(resolvedNamespacesDir, ns);
4862
+ const tokenRoot = path5.resolve(resolvedNamespacesDir, namespaceIdentityToken(ns));
4431
4863
  return resolvedStorageDir === rawRoot || resolvedStorageDir === tokenRoot;
4432
4864
  }
4433
4865
  namespaceStorageDirHintOwnershipRank(record, resolvedStorageDir, configured) {
4434
- if (resolvedStorageDir === path4.resolve(this.config.memoryDir)) {
4866
+ if (resolvedStorageDir === path5.resolve(this.config.memoryDir)) {
4435
4867
  return record.namespace === normalizeNamespaceIdentity(this.config.defaultNamespace) ? 0 : 3;
4436
4868
  }
4437
- const leaf = path4.basename(resolvedStorageDir);
4869
+ const leaf = path5.basename(resolvedStorageDir);
4438
4870
  const tokenOwnsRoot = namespaceIdentityToken(record.namespace) === leaf;
4439
4871
  if (tokenOwnsRoot && configured.has(record.namespace)) return 0;
4440
4872
  if (record.namespace === leaf) return 1;
@@ -4462,7 +4894,7 @@ var Orchestrator = class _Orchestrator {
4462
4894
  loadNamespaceStorageDirHintsFromCatalog() {
4463
4895
  if (this.namespaceStorageDirHintsLoaded || !this.namespaceCatalog.enabled) return;
4464
4896
  this.namespaceStorageDirHintsLoaded = true;
4465
- const catalogPath = path4.join(this.config.memoryDir, "state", "namespaces.jsonl");
4897
+ const catalogPath = path5.join(this.config.memoryDir, "state", "namespaces.jsonl");
4466
4898
  if (!existsSync(catalogPath)) return;
4467
4899
  let body;
4468
4900
  try {
@@ -4499,7 +4931,7 @@ var Orchestrator = class _Orchestrator {
4499
4931
  if (!this.storageDirMatchesNamespaceHint(record.namespace, record.storageDir)) {
4500
4932
  continue;
4501
4933
  }
4502
- const resolvedStorageDir = path4.resolve(record.storageDir);
4934
+ const resolvedStorageDir = path5.resolve(record.storageDir);
4503
4935
  const current = preferredByStorageDir.get(resolvedStorageDir);
4504
4936
  preferredByStorageDir.set(
4505
4937
  resolvedStorageDir,
@@ -4763,7 +5195,7 @@ var Orchestrator = class _Orchestrator {
4763
5195
  this.config = config;
4764
5196
  this.profiler = new ProfilingCollector({
4765
5197
  enabled: config.profilingEnabled,
4766
- storageDir: config.profilingStorageDir || path4.join(config.memoryDir, "profiling"),
5198
+ storageDir: config.profilingStorageDir || path5.join(config.memoryDir, "profiling"),
4767
5199
  maxTraces: config.profilingMaxTraces
4768
5200
  });
4769
5201
  this.namespaceCatalog = new NamespaceCatalog(config);
@@ -4833,11 +5265,22 @@ var Orchestrator = class _Orchestrator {
4833
5265
  this.compounding = resolveConsolidationCapabilities(config).compounding ? new CompoundingEngine(config, this.storage) : void 0;
4834
5266
  this.buffer = new SmartBuffer(config, this.storage);
4835
5267
  this.transcript = new TranscriptManager(config);
4836
- this.conversationIndexDir = path4.join(
5268
+ this.conversationIndexDir = path5.join(
4837
5269
  config.memoryDir,
4838
5270
  "conversation-index",
4839
5271
  "chunks"
4840
5272
  );
5273
+ this.conversationIndexCoordinator = new ConversationIndexCoordinator({
5274
+ config,
5275
+ getTranscript: () => this.transcript,
5276
+ getBackend: () => this.conversationIndexBackend,
5277
+ indexDir: this.conversationIndexDir
5278
+ });
5279
+ this.recallRerankCoordinator = new RecallRerankCoordinator({
5280
+ getConfig: () => this.config,
5281
+ getStorage: (namespace) => this.getStorage(namespace),
5282
+ readQmdResultMemory: (resultPath, fallbackStorage, recallNamespaces) => this.readQmdResultMemory(resultPath, fallbackStorage, recallNamespaces)
5283
+ });
4841
5284
  this.modelRegistry = new ModelRegistry(config.memoryDir);
4842
5285
  this.relevance = new RelevanceStore(config.memoryDir);
4843
5286
  this.negatives = new NegativeExampleStore(config.memoryDir);
@@ -4928,7 +5371,7 @@ var Orchestrator = class _Orchestrator {
4928
5371
  saveContentHashIndexes: () => this.saveContentHashIndexes()
4929
5372
  });
4930
5373
  this.threading = new ThreadingManager(
4931
- path4.join(config.memoryDir, "threads"),
5374
+ path5.join(config.memoryDir, "threads"),
4932
5375
  config.threadingGapMinutes
4933
5376
  );
4934
5377
  const lifecycleCaps = resolveMemoryLifecycleCapabilities(config);
@@ -5248,10 +5691,10 @@ var Orchestrator = class _Orchestrator {
5248
5691
  if (resolveRecallAuxiliaryCapabilities(this.config).compactionReset) {
5249
5692
  try {
5250
5693
  const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
5251
- const files = await readdir(wsDir).catch(() => []);
5694
+ const files = await readdir2(wsDir).catch(() => []);
5252
5695
  for (const f of files) {
5253
5696
  if (!f.startsWith(".compaction-reset-signal-")) continue;
5254
- const fp = path4.join(wsDir, f);
5697
+ const fp = path5.join(wsDir, f);
5255
5698
  const s = await stat(fp).catch(() => null);
5256
5699
  if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
5257
5700
  await unlink(fp).catch(() => {
@@ -5760,15 +6203,15 @@ ${doc.content}` : doc.content,
5760
6203
  this.lastFileHygieneRunAtMs = now;
5761
6204
  if (hygiene.rotateEnabled) {
5762
6205
  for (const rel of hygiene.rotatePaths) {
5763
- const abs = path4.isAbsolute(rel) ? rel : path4.join(this.config.workspaceDir, rel);
6206
+ const abs = path5.isAbsolute(rel) ? rel : path5.join(this.config.workspaceDir, rel);
5764
6207
  try {
5765
6208
  const raw = await readFile2(abs, "utf-8");
5766
6209
  if (raw.length > hygiene.rotateMaxBytes) {
5767
- const archiveDir = path4.join(
6210
+ const archiveDir = path5.join(
5768
6211
  this.config.workspaceDir,
5769
6212
  hygiene.archiveDir
5770
6213
  );
5771
- const base = path4.basename(abs);
6214
+ const base = path5.basename(abs);
5772
6215
  const prefix = base.toUpperCase().replace(/\.MD$/i, "").replace(/[^A-Z0-9]+/g, "-") || "FILE";
5773
6216
  const { newContent } = await rotateMarkdownFileToArchive({
5774
6217
  filePath: abs,
@@ -5793,8 +6236,8 @@ ${doc.content}` : doc.content,
5793
6236
  log.warn(w.message);
5794
6237
  }
5795
6238
  if (hygiene.warningsLogEnabled && warnings.length > 0) {
5796
- const fp = path4.join(this.config.memoryDir, hygiene.warningsLogPath);
5797
- await mkdir3(path4.dirname(fp), { recursive: true });
6239
+ const fp = path5.join(this.config.memoryDir, hygiene.warningsLogPath);
6240
+ await mkdir3(path5.dirname(fp), { recursive: true });
5798
6241
  const stamp = (/* @__PURE__ */ new Date()).toISOString();
5799
6242
  const block = `
5800
6243
 
@@ -5933,16 +6376,16 @@ ${doc.content}` : doc.content,
5933
6376
  for (const categoryDir of RECALL_FALLBACK_DIRS) {
5934
6377
  if (memoryRootReal === null) break;
5935
6378
  for (const date of datesToScan) {
5936
- const dateDir = path4.join(storage.dir, categoryDir, date);
6379
+ const dateDir = path5.join(storage.dir, categoryDir, date);
5937
6380
  try {
5938
6381
  const dirStat = await lstat(dateDir);
5939
6382
  if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) continue;
5940
6383
  assertPathInsideRoot(memoryRootReal, await realpath(dateDir), dateDir);
5941
- const entries = await readdir(dateDir, { withFileTypes: true });
6384
+ const entries = await readdir2(dateDir, { withFileTypes: true });
5942
6385
  for (const entry of entries) {
5943
6386
  if (entry.isSymbolicLink()) continue;
5944
6387
  if (!entry.name.endsWith(".md")) continue;
5945
- const fullPath = path4.join(dateDir, entry.name);
6388
+ const fullPath = path5.join(dateDir, entry.name);
5946
6389
  try {
5947
6390
  assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);
5948
6391
  const raw = await readFile2(fullPath, "utf-8");
@@ -5964,7 +6407,7 @@ ${doc.content}` : doc.content,
5964
6407
  facts.push({
5965
6408
  path: fullPath,
5966
6409
  frontmatter: {
5967
- id: fm.id || path4.basename(entry.name, ".md"),
6410
+ id: fm.id || path5.basename(entry.name, ".md"),
5968
6411
  category: fm.category || "fact",
5969
6412
  created,
5970
6413
  updated: fm.updated || created,
@@ -5987,13 +6430,13 @@ ${doc.content}` : doc.content,
5987
6430
  return a.frontmatter.created < b.frontmatter.created ? -1 : 1;
5988
6431
  });
5989
6432
  const hourlySummaries = [];
5990
- const hourlyBaseDir = path4.join(storage.dir, "summaries", "hourly");
6433
+ const hourlyBaseDir = path5.join(storage.dir, "summaries", "hourly");
5991
6434
  try {
5992
- const sessionKeys = await readdir(hourlyBaseDir, { withFileTypes: true });
6435
+ const sessionKeys = await readdir2(hourlyBaseDir, { withFileTypes: true });
5993
6436
  for (const sk of sessionKeys) {
5994
6437
  if (!sk.isDirectory()) continue;
5995
6438
  for (const date of datesToScan) {
5996
- const summaryFile = path4.join(hourlyBaseDir, sk.name, `${date}.md`);
6439
+ const summaryFile = path5.join(hourlyBaseDir, sk.name, `${date}.md`);
5997
6440
  try {
5998
6441
  const raw = await readFile2(summaryFile, "utf-8");
5999
6442
  const filtered = filterHourlySummaryMarkdownForLocalDay(
@@ -6097,7 +6540,7 @@ ${doc.content}` : doc.content,
6097
6540
  }
6098
6541
  async getLastGraphRecallSnapshot(namespace) {
6099
6542
  const storage = await this.getStorage(namespace);
6100
- const snapshotPath = path4.join(
6543
+ const snapshotPath = path5.join(
6101
6544
  storage.dir,
6102
6545
  "state",
6103
6546
  "last_graph_recall.json"
@@ -6136,7 +6579,7 @@ ${doc.content}` : doc.content,
6136
6579
  }
6137
6580
  async getLastIntentSnapshot(namespace) {
6138
6581
  const storage = await this.getStorage(namespace);
6139
- const snapshotPath = path4.join(storage.dir, "state", "last_intent.json");
6582
+ const snapshotPath = path5.join(storage.dir, "state", "last_intent.json");
6140
6583
  try {
6141
6584
  const raw = await readFile2(snapshotPath, "utf-8");
6142
6585
  const parsed = JSON.parse(raw);
@@ -6169,7 +6612,7 @@ ${doc.content}` : doc.content,
6169
6612
  }
6170
6613
  async getLastQmdRecallSnapshot(namespace) {
6171
6614
  const storage = await this.getStorage(namespace);
6172
- const snapshotPath = path4.join(
6615
+ const snapshotPath = path5.join(
6173
6616
  storage.dir,
6174
6617
  "state",
6175
6618
  "last_qmd_recall.json"
@@ -6294,206 +6737,31 @@ ${doc.content}` : doc.content,
6294
6737
  })
6295
6738
  ].join("\n");
6296
6739
  }
6297
- async searchConversationRecallResults(retrievalQuery, topK) {
6298
- if (this.conversationIndexBackend) {
6299
- return this.conversationIndexBackend.search(retrievalQuery, topK);
6300
- }
6301
- return [];
6302
- }
6303
- formatConversationRecallSection(results, maxChars) {
6304
- if (!Array.isArray(results) || results.length === 0) return null;
6305
- const lines = ["## Semantic Recall (Past Conversations)", ""];
6306
- let used = 0;
6307
- for (const r of results) {
6308
- if (!r?.snippet) continue;
6309
- const chunk = `### ${r.path}
6310
- Score: ${r.score.toFixed(3)}
6311
-
6312
- ${r.snippet.trim()}
6313
- `;
6314
- if (used + chunk.length > maxChars) break;
6315
- lines.push(chunk);
6316
- used += chunk.length;
6317
- }
6318
- return used > 0 ? lines.join("\n") : null;
6319
- }
6320
- async countConversationChunkDocs(dir) {
6321
- try {
6322
- const entries = await readdir(dir, { withFileTypes: true });
6323
- let total = 0;
6324
- for (const entry of entries) {
6325
- const fullPath = path4.join(dir, entry.name);
6326
- if (entry.isDirectory()) {
6327
- total += await this.countConversationChunkDocs(fullPath);
6328
- continue;
6329
- }
6330
- if (entry.isFile() && entry.name.endsWith(".md")) {
6331
- total += 1;
6332
- }
6333
- }
6334
- return total;
6335
- } catch {
6336
- return 0;
6337
- }
6338
- }
6339
- async buildConversationIndexChunks(sessionKey, hours = 24) {
6340
- const entries = await this.transcript.readRecent(hours, sessionKey);
6341
- const effectiveSessionKey = sessionKey ?? "all-sessions";
6342
- return chunkTranscriptEntries(effectiveSessionKey, entries, {
6343
- maxChars: this.config.conversationRecallMaxChars * 2,
6344
- maxTurns: Math.max(10, this.config.hourlySummariesMaxTurnsPerRun)
6345
- });
6346
- }
6347
- async getConversationIndexHealth() {
6348
- const chunkDocCount = await this.countConversationChunkDocs(
6349
- this.conversationIndexDir
6350
- );
6351
- const lastUpdateAtMs = Math.max(
6352
- 0,
6353
- ...this.conversationIndexLastUpdateAtMs.values()
6354
- );
6355
- const lastUpdateAt = lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
6356
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
6357
- return {
6358
- enabled: false,
6359
- backend: this.config.conversationIndexBackend,
6360
- status: "disabled",
6361
- chunkDocCount,
6362
- lastUpdateAt
6363
- };
6364
- }
6365
- const backendHealth = this.conversationIndexBackend ? await this.conversationIndexBackend.health() : {
6366
- backend: this.config.conversationIndexBackend,
6367
- status: "degraded"
6368
- };
6369
- return {
6370
- enabled: true,
6371
- chunkDocCount,
6372
- lastUpdateAt,
6373
- ...backendHealth
6374
- };
6375
- }
6376
- async inspectConversationIndex() {
6377
- const chunkDocCount = await this.countConversationChunkDocs(
6378
- this.conversationIndexDir
6379
- );
6380
- const lastUpdateAtMs = Math.max(
6381
- 0,
6382
- ...this.conversationIndexLastUpdateAtMs.values()
6383
- );
6384
- const lastUpdateAt = lastUpdateAtMs > 0 ? new Date(lastUpdateAtMs).toISOString() : null;
6385
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
6386
- return {
6387
- enabled: false,
6388
- backend: this.config.conversationIndexBackend,
6389
- status: "disabled",
6390
- available: false,
6391
- indexPath: this.conversationIndexDir,
6392
- supportsIncrementalUpdate: true,
6393
- message: "Conversation index disabled by config",
6394
- metadata: {
6395
- chunkCount: chunkDocCount
6396
- },
6397
- chunkDocCount,
6398
- lastUpdateAt
6399
- };
6400
- }
6401
- const inspection = this.conversationIndexBackend ? await this.conversationIndexBackend.inspect() : {
6402
- backend: this.config.conversationIndexBackend,
6403
- status: "degraded",
6404
- available: false,
6405
- indexPath: this.conversationIndexDir,
6406
- supportsIncrementalUpdate: true,
6407
- message: "Conversation index backend unavailable",
6408
- metadata: {
6409
- chunkCount: chunkDocCount
6410
- }
6411
- };
6412
- return {
6413
- enabled: true,
6414
- chunkDocCount,
6415
- lastUpdateAt,
6416
- ...inspection
6417
- };
6740
+ async searchConversationRecallResults(retrievalQuery, topK) {
6741
+ return this.conversationIndexCoordinator.search(retrievalQuery, topK);
6742
+ }
6743
+ formatConversationRecallSection(results, maxChars) {
6744
+ return this.conversationIndexCoordinator.formatRecallSection(
6745
+ results,
6746
+ maxChars
6747
+ );
6748
+ }
6749
+ // Issue #1526: countConversationChunkDocs / buildConversationIndexChunks moved
6750
+ // to ConversationIndexCoordinator (internal helpers, no orchestrator callers).
6751
+ async getConversationIndexHealth() {
6752
+ return this.conversationIndexCoordinator.getHealth();
6753
+ }
6754
+ async inspectConversationIndex() {
6755
+ return this.conversationIndexCoordinator.inspect();
6418
6756
  }
6419
6757
  async getRecoverySummary(sessionKey) {
6420
6758
  return this.transcript.getRecoverySummary(sessionKey);
6421
6759
  }
6422
6760
  async updateConversationIndex(sessionKey, hours = 24, opts) {
6423
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
6424
- return { chunks: 0, skipped: true, reason: "disabled", embedded: false };
6425
- }
6426
- const enforceMinInterval = opts?.enforceMinInterval !== false;
6427
- if (enforceMinInterval) {
6428
- const minIntervalMs = Math.max(
6429
- 0,
6430
- this.config.conversationIndexMinUpdateIntervalMs
6431
- );
6432
- const now = Date.now();
6433
- const last = this.conversationIndexLastUpdateAtMs.get(sessionKey) ?? 0;
6434
- const elapsed = now - last;
6435
- if (minIntervalMs > 0 && elapsed < minIntervalMs) {
6436
- return {
6437
- chunks: 0,
6438
- skipped: true,
6439
- reason: "min_interval",
6440
- retryAfterMs: minIntervalMs - elapsed,
6441
- embedded: false
6442
- };
6443
- }
6444
- }
6445
- const chunks = await this.buildConversationIndexChunks(sessionKey, hours);
6446
- await writeConversationChunks(this.conversationIndexDir, chunks);
6447
- const retentionCutoffMs = Number.isFinite(this.config.conversationIndexRetentionDays) && this.config.conversationIndexRetentionDays > 0 ? Date.now() - this.config.conversationIndexRetentionDays * 24 * 60 * 60 * 1e3 : void 0;
6448
- await cleanupConversationChunks(
6449
- this.conversationIndexDir,
6450
- this.config.conversationIndexRetentionDays
6451
- );
6452
- const shouldEmbed = opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
6453
- let embedded = false;
6454
- if (this.conversationIndexBackend) {
6455
- const result = await this.conversationIndexBackend.update(chunks, {
6456
- embed: shouldEmbed,
6457
- ...retentionCutoffMs !== void 0 ? { retentionCutoffMs } : {}
6458
- });
6459
- embedded = result.embedded;
6460
- }
6461
- this.conversationIndexLastUpdateAtMs.set(sessionKey, Date.now());
6462
- return { chunks: chunks.length, skipped: false, embedded };
6761
+ return this.conversationIndexCoordinator.update(sessionKey, hours, opts);
6463
6762
  }
6464
6763
  async rebuildConversationIndex(sessionKey, hours = 24, opts) {
6465
- if (!resolveIndexingCapabilities(this.config).conversationIndex) {
6466
- return {
6467
- chunks: 0,
6468
- skipped: true,
6469
- reason: "disabled",
6470
- embedded: false,
6471
- rebuilt: false
6472
- };
6473
- }
6474
- const chunks = await this.buildConversationIndexChunks(sessionKey, hours);
6475
- await writeConversationChunks(this.conversationIndexDir, chunks);
6476
- await cleanupConversationChunks(
6477
- this.conversationIndexDir,
6478
- this.config.conversationIndexRetentionDays
6479
- );
6480
- const shouldEmbed = opts?.embed ?? this.config.conversationIndexEmbedOnUpdate;
6481
- let embedded = false;
6482
- let rebuilt = false;
6483
- if (this.conversationIndexBackend) {
6484
- const result = await this.conversationIndexBackend.rebuild(chunks, {
6485
- embed: shouldEmbed
6486
- });
6487
- embedded = result.embedded;
6488
- rebuilt = result.rebuilt;
6489
- }
6490
- const stamp = Date.now();
6491
- if (sessionKey) {
6492
- this.conversationIndexLastUpdateAtMs.set(sessionKey, stamp);
6493
- } else {
6494
- this.conversationIndexLastUpdateAtMs.set("__rebuild__", stamp);
6495
- }
6496
- return { chunks: chunks.length, skipped: false, embedded, rebuilt };
6764
+ return this.conversationIndexCoordinator.rebuild(sessionKey, hours, opts);
6497
6765
  }
6498
6766
  /**
6499
6767
  * Validate local LLM model availability and context window compatibility.
@@ -7228,7 +7496,7 @@ ${r.snippet.trim()}
7228
7496
  resolvedPath = resolvedCold.result.path;
7229
7497
  resolvedResult = resolvedCold.result;
7230
7498
  }
7231
- if (!path4.isAbsolute(resolvedPath)) {
7499
+ if (!path5.isAbsolute(resolvedPath)) {
7232
7500
  resolvedAmbiguousSeeds.set(result.path, null);
7233
7501
  return null;
7234
7502
  }
@@ -7253,7 +7521,7 @@ ${r.snippet.trim()}
7253
7521
  }
7254
7522
  continue;
7255
7523
  }
7256
- if (path4.isAbsolute(result.path)) {
7524
+ if (path5.isAbsolute(result.path)) {
7257
7525
  const resolved = await resolveAmbiguousSeedOwner(result, null);
7258
7526
  if (resolved) {
7259
7527
  addResultForNamespace(resolved.namespace, resolved.result);
@@ -7296,7 +7564,7 @@ ${r.snippet.trim()}
7296
7564
  0
7297
7565
  );
7298
7566
  seedPaths.push(
7299
- ...seedRelativePaths.map((rel) => path4.join(storage.dir, rel))
7567
+ ...seedRelativePaths.map((rel) => path5.join(storage.dir, rel))
7300
7568
  );
7301
7569
  const seedSet = new Set(seedRelativePaths);
7302
7570
  const expanded = await this.graphIndexFor(storage).spreadingActivation(
@@ -7312,7 +7580,7 @@ ${r.snippet.trim()}
7312
7580
  for (const candidate of expanded.slice(0, perNamespaceExpandedCap)) {
7313
7581
  if (deadlineExpired()) break;
7314
7582
  if (seedSet.has(candidate.path)) continue;
7315
- const memoryPath = path4.resolve(storage.dir, candidate.path);
7583
+ const memoryPath = path5.resolve(storage.dir, candidate.path);
7316
7584
  const memory = await storage.readMemoryByPath(memoryPath);
7317
7585
  if (deadlineExpired()) break;
7318
7586
  if (!memory) continue;
@@ -7337,7 +7605,7 @@ ${r.snippet.trim()}
7337
7605
  path: memory.path,
7338
7606
  score,
7339
7607
  namespace,
7340
- seed: path4.resolve(storage.dir, candidate.seed),
7608
+ seed: path5.resolve(storage.dir, candidate.seed),
7341
7609
  hopDepth: candidate.hopDepth,
7342
7610
  decayedWeight: candidate.decayedWeight,
7343
7611
  graphType: candidate.graphType,
@@ -7358,12 +7626,12 @@ ${r.snippet.trim()}
7358
7626
  }
7359
7627
  async recordLastGraphRecallSnapshot(options) {
7360
7628
  try {
7361
- const snapshotPath = path4.join(
7629
+ const snapshotPath = path5.join(
7362
7630
  options.storage.dir,
7363
7631
  "state",
7364
7632
  "last_graph_recall.json"
7365
7633
  );
7366
- await mkdir3(path4.dirname(snapshotPath), { recursive: true });
7634
+ await mkdir3(path5.dirname(snapshotPath), { recursive: true });
7367
7635
  const now = (/* @__PURE__ */ new Date()).toISOString();
7368
7636
  const totalSeedCount = options.seedPaths.length;
7369
7637
  const totalExpandedCount = options.expandedPaths.length;
@@ -7397,12 +7665,12 @@ ${r.snippet.trim()}
7397
7665
  }
7398
7666
  async recordLastIntentSnapshot(options) {
7399
7667
  try {
7400
- const snapshotPath = path4.join(
7668
+ const snapshotPath = path5.join(
7401
7669
  options.storage.dir,
7402
7670
  "state",
7403
7671
  "last_intent.json"
7404
7672
  );
7405
- await mkdir3(path4.dirname(snapshotPath), { recursive: true });
7673
+ await mkdir3(path5.dirname(snapshotPath), { recursive: true });
7406
7674
  await writeFile3(
7407
7675
  snapshotPath,
7408
7676
  JSON.stringify(options.snapshot, null, 2),
@@ -7414,12 +7682,12 @@ ${r.snippet.trim()}
7414
7682
  }
7415
7683
  async recordLastQmdRecallSnapshot(options) {
7416
7684
  try {
7417
- const snapshotPath = path4.join(
7685
+ const snapshotPath = path5.join(
7418
7686
  options.storage.dir,
7419
7687
  "state",
7420
7688
  "last_qmd_recall.json"
7421
7689
  );
7422
- await mkdir3(path4.dirname(snapshotPath), { recursive: true });
7690
+ await mkdir3(path5.dirname(snapshotPath), { recursive: true });
7423
7691
  await writeFile3(
7424
7692
  snapshotPath,
7425
7693
  JSON.stringify(options.snapshot, null, 2),
@@ -7434,8 +7702,8 @@ ${r.snippet.trim()}
7434
7702
  const stateDir = await this.resolveStateDirForNamespace(
7435
7703
  options.namespace
7436
7704
  );
7437
- const snapshotPath = path4.join(stateDir, "last_intent.json");
7438
- await mkdir3(path4.dirname(snapshotPath), { recursive: true });
7705
+ const snapshotPath = path5.join(stateDir, "last_intent.json");
7706
+ await mkdir3(path5.dirname(snapshotPath), { recursive: true });
7439
7707
  await writeFile3(
7440
7708
  snapshotPath,
7441
7709
  JSON.stringify(options.snapshot, null, 2),
@@ -7447,12 +7715,12 @@ ${r.snippet.trim()}
7447
7715
  }
7448
7716
  async resolveStateDirForNamespace(namespace) {
7449
7717
  if (!resolveNamespaceCapabilities(this.config).namespaces) {
7450
- return path4.join(this.config.memoryDir, "state");
7718
+ return path5.join(this.config.memoryDir, "state");
7451
7719
  }
7452
7720
  if (namespace !== this.config.defaultNamespace) {
7453
- return path4.join(this.config.memoryDir, "namespaces", namespace, "state");
7721
+ return path5.join(this.config.memoryDir, "namespaces", namespace, "state");
7454
7722
  }
7455
- const candidate = path4.join(
7723
+ const candidate = path5.join(
7456
7724
  this.config.memoryDir,
7457
7725
  "namespaces",
7458
7726
  this.config.defaultNamespace
@@ -7460,11 +7728,11 @@ ${r.snippet.trim()}
7460
7728
  try {
7461
7729
  const candidateStat = await stat(candidate);
7462
7730
  if (candidateStat.isDirectory()) {
7463
- return path4.join(candidate, "state");
7731
+ return path5.join(candidate, "state");
7464
7732
  }
7465
7733
  } catch {
7466
7734
  }
7467
- return path4.join(this.config.memoryDir, "state");
7735
+ return path5.join(this.config.memoryDir, "state");
7468
7736
  }
7469
7737
  buildGraphRecallRankedResults(results, sourceLabelResolver, limit = 64) {
7470
7738
  return results.slice(0, limit).map((result) => ({
@@ -7866,7 +8134,7 @@ ${r.snippet.trim()}
7866
8134
  const graphExpandedResultPaths = /* @__PURE__ */ new Set();
7867
8135
  const graphSourceLabelsForPath = (resultPath) => {
7868
8136
  const labels = [];
7869
- const normalizedPath = resultPath.split(path4.sep).join("/");
8137
+ const normalizedPath = resultPath.split(path5.sep).join("/");
7870
8138
  const isEntityPath = normalizedPath.startsWith("entities/") || normalizedPath.includes("/entities/");
7871
8139
  if (graphBaselinePaths.has(resultPath)) labels.push("baseline");
7872
8140
  if (graphExpandedResultPaths.has(resultPath))
@@ -8016,7 +8284,7 @@ ${r.snippet.trim()}
8016
8284
  profileStorageNamespaces.map((namespace) => this.storageRouter.storageFor(namespace))
8017
8285
  );
8018
8286
  const emptyProfileStorage = new Proxy(
8019
- { dir: path4.join(this.config.memoryDir, ".empty-scope-profile") },
8287
+ { dir: path5.join(this.config.memoryDir, ".empty-scope-profile") },
8020
8288
  {
8021
8289
  get(target, prop) {
8022
8290
  if (prop in target) return target[prop];
@@ -9492,11 +9760,11 @@ ${formatted}`;
9492
9760
  if (!resolveRecallAuxiliaryCapabilities(this.config).compactionReset) return null;
9493
9761
  const workspaceDir = compactionWorkspaceDir || this.config.workspaceDir || defaultWorkspaceDir();
9494
9762
  const safeSessionKey = sanitizeSessionKeyForFilename(effectiveSessionKey);
9495
- const signalPath = path4.join(
9763
+ const signalPath = path5.join(
9496
9764
  workspaceDir,
9497
9765
  `.compaction-reset-signal-${safeSessionKey}`
9498
9766
  );
9499
- const bootPath = path4.join(workspaceDir, "BOOT.md");
9767
+ const bootPath = path5.join(workspaceDir, "BOOT.md");
9500
9768
  try {
9501
9769
  const signalStat = await stat(signalPath).catch(() => null);
9502
9770
  if (!signalStat) return null;
@@ -14098,7 +14366,7 @@ ${normalized}`).digest("hex");
14098
14366
  const allMems = allMemsForGraph ?? [];
14099
14367
  for (const m of allMems) {
14100
14368
  if (m.frontmatter.entityRef === entityRef) {
14101
- const rel = path4.relative(storage.dir, m.path);
14369
+ const rel = path5.relative(storage.dir, m.path);
14102
14370
  if (rel !== memoryRelPath) entitySiblings.push(rel);
14103
14371
  }
14104
14372
  }
@@ -14422,7 +14690,7 @@ ${normalized}`).digest("hex");
14422
14690
  }
14423
14691
  if (resolveConsolidationCapabilities(this.config).semanticConsolidation) {
14424
14692
  try {
14425
- const stateFilePath = path4.join(
14693
+ const stateFilePath = path5.join(
14426
14694
  this.config.memoryDir,
14427
14695
  "state",
14428
14696
  "semantic-consolidation-last-run.json"
@@ -14470,7 +14738,7 @@ ${normalized}`).digest("hex");
14470
14738
  );
14471
14739
  }
14472
14740
  if (semResult.errors === 0 || semResult.memoriesArchived > 0) {
14473
- const stateDir = path4.join(this.config.memoryDir, "state");
14741
+ const stateDir = path5.join(this.config.memoryDir, "state");
14474
14742
  await mkdir3(stateDir, { recursive: true });
14475
14743
  await writeFile3(
14476
14744
  stateFilePath,
@@ -14791,7 +15059,7 @@ ${reflectionsContent.trim()}
14791
15059
  const seenStorageDirs = /* @__PURE__ */ new Set();
14792
15060
  const addStorage = (storage) => {
14793
15061
  const storageDir = storageDirFor(storage);
14794
- const storageKey = storageDir ? path4.resolve(storageDir) : `storage-without-dir-${storages.length}`;
15062
+ const storageKey = storageDir ? path5.resolve(storageDir) : `storage-without-dir-${storages.length}`;
14795
15063
  if (seenStorageDirs.has(storageKey)) return;
14796
15064
  seenStorageDirs.add(storageKey);
14797
15065
  storages.push(storage);
@@ -14821,7 +15089,7 @@ ${reflectionsContent.trim()}
14821
15089
  continue;
14822
15090
  }
14823
15091
  try {
14824
- const coldRoot = path4.join(storageDir, "cold");
15092
+ const coldRoot = path5.join(storageDir, "cold");
14825
15093
  for (const candidate of qmdResultPathCandidates(
14826
15094
  coldRoot,
14827
15095
  parts.relativePath
@@ -14862,7 +15130,7 @@ ${reflectionsContent.trim()}
14862
15130
  return null;
14863
15131
  }
14864
15132
  }
14865
- if (path4.isAbsolute(resultPath)) {
15133
+ if (path5.isAbsolute(resultPath)) {
14866
15134
  if (!fallbackStorageDir) {
14867
15135
  return await fallbackStorage.readMemoryByPath(resultPath);
14868
15136
  }
@@ -14901,7 +15169,7 @@ ${reflectionsContent.trim()}
14901
15169
  );
14902
15170
  if (!memory) return null;
14903
15171
  let ownerNamespace = null;
14904
- if (path4.isAbsolute(memory.path)) {
15172
+ if (path5.isAbsolute(memory.path)) {
14905
15173
  const ownerStorage = await this.storageForAbsoluteQmdResultPath(
14906
15174
  memory.path,
14907
15175
  fallbackStorage,
@@ -14925,16 +15193,16 @@ ${reflectionsContent.trim()}
14925
15193
  };
14926
15194
  }
14927
15195
  async storageForAbsoluteQmdResultPath(resultPath, fallbackStorage, recallNamespaces = []) {
14928
- const resolvedPath = path4.resolve(resultPath);
14929
- const memoryRoot = path4.resolve(this.config.memoryDir);
14930
- const namespacesRoot = path4.join(memoryRoot, "namespaces");
15196
+ const resolvedPath = path5.resolve(resultPath);
15197
+ const memoryRoot = path5.resolve(this.config.memoryDir);
15198
+ const namespacesRoot = path5.join(memoryRoot, "namespaces");
14931
15199
  const fallbackStorageDir = typeof fallbackStorage.dir === "string" && fallbackStorage.dir ? fallbackStorage.dir : null;
14932
15200
  const matches = [];
14933
15201
  const seenDirs = /* @__PURE__ */ new Set();
14934
15202
  const maybeAddStorage = (storage, namespace) => {
14935
15203
  const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
14936
15204
  if (!storageDir) return;
14937
- const candidateRoot = path4.resolve(storageDir);
15205
+ const candidateRoot = path5.resolve(storageDir);
14938
15206
  if (seenDirs.has(candidateRoot)) return;
14939
15207
  if (!isPathInsideStorageRoot(candidateRoot, resolvedPath)) return;
14940
15208
  if (candidateRoot === memoryRoot && isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
@@ -14952,7 +15220,7 @@ ${reflectionsContent.trim()}
14952
15220
  candidateNamespaces.add(ns);
14953
15221
  }
14954
15222
  if (isPathInsideStorageRoot(namespacesRoot, resolvedPath)) {
14955
- const relativeToNamespaces = path4.relative(namespacesRoot, resolvedPath);
15223
+ const relativeToNamespaces = path5.relative(namespacesRoot, resolvedPath);
14956
15224
  const [namespaceSegment] = relativeToNamespaces.split(/[\\/]/);
14957
15225
  if (namespaceSegment) {
14958
15226
  candidateNamespaces.add(
@@ -14974,218 +15242,22 @@ ${reflectionsContent.trim()}
14974
15242
  matches.sort((a, b) => b.dir.length - a.dir.length);
14975
15243
  return matches[0] ?? null;
14976
15244
  }
15245
+ // Issue #1526: recall-rerank methods moved to RecallRerankCoordinator.
15246
+ // Thin delegation keeps the private API stable for callers + tests.
14977
15247
  async applyMemoryWorthRerank(results, namespaces) {
14978
- const counters = /* @__PURE__ */ new Map();
14979
- const seenNamespaces = /* @__PURE__ */ new Set();
14980
- const nowMs = Date.now();
14981
- for (const [key, entry] of this.memoryWorthCounterCache) {
14982
- if (nowMs - entry.at >= _Orchestrator.MEMORY_WORTH_CACHE_TTL_MS) {
14983
- this.memoryWorthCounterCache.delete(key);
14984
- }
14985
- }
14986
- for (const ns of namespaces) {
14987
- if (seenNamespaces.has(ns)) continue;
14988
- seenNamespaces.add(ns);
14989
- try {
14990
- const cached = this.memoryWorthCounterCache.get(ns);
14991
- let nsMap;
14992
- if (cached && nowMs - cached.at < _Orchestrator.MEMORY_WORTH_CACHE_TTL_MS) {
14993
- nsMap = cached.counters;
14994
- } else {
14995
- const storage = await this.getStorage(ns);
14996
- const memories = await storage.readAllMemories();
14997
- nsMap = buildMemoryWorthCounterMap(memories);
14998
- this.memoryWorthCounterCache.set(ns, { at: nowMs, counters: nsMap });
14999
- }
15000
- for (const [path5, c] of nsMap) counters.set(path5, c);
15001
- } catch (err) {
15002
- log.debug("memory-worth: failed to read namespace, skipping", {
15003
- namespace: ns,
15004
- error: err.message
15005
- });
15006
- }
15007
- }
15008
- const missing = results.filter((r) => !counters.has(r.path));
15009
- if (missing.length > 0) {
15010
- let reader = null;
15011
- for (const ns of namespaces) {
15012
- try {
15013
- reader = await this.getStorage(ns);
15014
- break;
15015
- } catch {
15016
- }
15017
- }
15018
- if (reader) {
15019
- for (const r of missing) {
15020
- try {
15021
- const memory = await this.readQmdResultMemory(r.path, reader, namespaces);
15022
- if (!memory) continue;
15023
- const fm = memory.frontmatter;
15024
- if (fm.mw_success === void 0 && fm.mw_fail === void 0) continue;
15025
- counters.set(r.path, {
15026
- mw_success: fm.mw_success,
15027
- mw_fail: fm.mw_fail,
15028
- lastAccessed: fm.lastAccessed
15029
- });
15030
- } catch (err) {
15031
- log.debug("memory-worth: direct path lookup failed", {
15032
- path: r.path,
15033
- error: err.message
15034
- });
15035
- }
15036
- }
15037
- }
15038
- }
15039
- if (counters.size === 0) return results;
15040
- const rankedInputs = results.map((r, i) => ({
15041
- path: r.path,
15042
- // Large positive rank score so multiplier math stays well-scaled and
15043
- // we never hit zero; descending so earlier items rank higher.
15044
- score: results.length - i
15045
- }));
15046
- const filtered = applyMemoryWorthFilter(rankedInputs, {
15047
- counters,
15048
- now: /* @__PURE__ */ new Date(),
15049
- halfLifeMs: this.config.recallMemoryWorthHalfLifeMs > 0 ? this.config.recallMemoryWorthHalfLifeMs : void 0
15050
- });
15051
- const byPath = new Map(results.map((r) => [r.path, r]));
15052
- const reordered = [];
15053
- for (const item of filtered) {
15054
- const original = byPath.get(item.path);
15055
- if (original) reordered.push(original);
15056
- }
15057
- return reordered;
15248
+ return this.recallRerankCoordinator.applyMemoryWorthRerank(results, namespaces);
15058
15249
  }
15059
- /**
15060
- * Issue #1577 — unified TrustScore recall stage. Thin wiring over the pure
15061
- * {@link applyTrustScoreStage} scorer + the {@link buildTrustSignalsForRerank}
15062
- * signal builder. The stage subsumes the Memory Worth multiplier — the
15063
- * orchestrator runs exactly one of the two (mutual exclusion, rule 39; the
15064
- * double-multiplier test in trust-score-stage.test.ts pins it structurally).
15065
- *
15066
- * Returns the admitted results AND the per-path trust map (including
15067
- * quarantined items) so the caller can: (a) render epistemic hedges, (b)
15068
- * surface quarantined items in X-ray with a reason (rule 34), and (c) filter
15069
- * quarantined paths from fallback recall branches. The trust map is a
15070
- * per-recall local — never instance state — so concurrent recalls cannot
15071
- * race on it (review: shared-trust-map concurrency).
15072
- */
15073
15250
  async applyTrustScoreRerank(results, namespaces) {
15074
- if (results.length === 0) return { results, trustByPath: null };
15075
- const now = /* @__PURE__ */ new Date();
15076
- const halfLifeDays = this.config.recallMemoryWorthHalfLifeMs > 0 ? this.config.recallMemoryWorthHalfLifeMs / (24 * 60 * 60 * 1e3) : void 0;
15077
- let fallbackReader = null;
15078
- const signals = await buildTrustSignalsForRerank(
15079
- results.map((r) => r.path),
15080
- namespaces,
15081
- {
15082
- readNamespaceMemories: async (ns) => (await this.getStorage(ns)).readAllMemories(),
15083
- readMemoryFrontmatter: async (path5) => {
15084
- if (!fallbackReader) {
15085
- for (const ns of namespaces) {
15086
- try {
15087
- fallbackReader = await this.getStorage(ns);
15088
- break;
15089
- } catch {
15090
- }
15091
- }
15092
- }
15093
- if (!fallbackReader) return null;
15094
- const memory = await this.readQmdResultMemory(path5, fallbackReader, namespaces);
15095
- return memory ? memory.frontmatter : null;
15096
- }
15097
- },
15098
- { cache: this.trustSignalCache, ttlMs: _Orchestrator.TRUST_SIGNAL_CACHE_TTL_MS },
15099
- now,
15100
- {
15101
- recencyHalfLifeDays: halfLifeDays,
15102
- logDebug: (message, context) => log.debug(message, context)
15103
- }
15104
- );
15105
- if (signals.size === 0) {
15106
- return { results, trustByPath: null };
15107
- }
15108
- const rankedInputs = results.map((r, i) => ({ path: r.path, score: results.length - i }));
15109
- const stage = applyTrustScoreStage(rankedInputs, {
15110
- signals,
15111
- weights: this.config.trustScoreWeights,
15112
- minMultiplier: this.config.trustScoreMinMultiplier,
15113
- maxMultiplier: this.config.trustScoreMaxMultiplier,
15114
- quarantine: this.config.trustScoreQuarantine
15115
- });
15116
- const trustByPath = new Map(stage.all.map((item) => [item.path, item]));
15117
- const byPath = new Map(results.map((r) => [r.path, r]));
15118
- const admitted = stage.admitted.map((item) => byPath.get(item.path)).filter((r) => r !== void 0);
15119
- return { results: admitted, trustByPath };
15251
+ return this.recallRerankCoordinator.applyTrustScoreRerank(results, namespaces);
15120
15252
  }
15121
- /**
15122
- * Issue #1577 — apply the TrustScore stage (or, when trust is off, the Memory
15123
- * Worth multiplier fallback) to ONE recall branch's results, returning the
15124
- * scored results + the per-path trust map. Thin wiring over
15125
- * {@link applyTrustScoreRerank} so every recall path — hot QMD, embedding
15126
- * fallback, recent scan — applies the SAME multiplier gate (rule 41: a
15127
- * feature gate must apply across ALL parallel recall paths). TrustScore
15128
- * subsumes Memory Worth; exactly one runs (rule 39). Fail-open on lookup
15129
- * errors so a storage hiccup never breaks a fallback path.
15130
- */
15131
15253
  async applyTrustScoreToBranch(results, namespaces, caps, label) {
15132
- if (caps.recallTrustScore && results.length > 0) {
15133
- try {
15134
- return await this.applyTrustScoreRerank(results, namespaces);
15135
- } catch (err) {
15136
- log.debug(`trust-score stage (${label}) failed open`, {
15137
- error: err.message
15138
- });
15139
- }
15140
- } else if (caps.recallMemoryWorthFilter && results.length > 0) {
15141
- try {
15142
- const filtered = await this.applyMemoryWorthRerank(results, namespaces);
15143
- return { results: filtered, trustByPath: null };
15144
- } catch (err) {
15145
- log.debug(`memory-worth filter (${label}) failed open`, {
15146
- error: err.message
15147
- });
15148
- }
15149
- }
15150
- return { results, trustByPath: null };
15254
+ return this.recallRerankCoordinator.applyTrustScoreToBranch(results, namespaces, caps, label);
15151
15255
  }
15152
15256
  diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps = resolveCapabilities(this.config)) {
15153
- const safeLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 0;
15154
- if (!Array.isArray(results) || results.length === 0) return [];
15155
- if (safeLimit === 0) return [];
15156
- const boosted = caps.recallReasoningTraceBoost && typeof retrievalQuery === "string" ? applyReasoningTraceBoost(results, {
15157
- enabled: true,
15158
- query: retrievalQuery
15159
- }) : results;
15160
- const diversified = this.applyMmrToQmdResults(sectionId, boosted, caps);
15161
- return diversified.slice(0, safeLimit);
15257
+ return this.recallRerankCoordinator.diversifyAndLimitRecallResults(sectionId, results, limit, retrievalQuery, caps);
15162
15258
  }
15163
- /**
15164
- * Apply Maximal Marginal Relevance to a section's ordered candidate list.
15165
- *
15166
- * Operates per-section so one redundant cluster cannot dominate a section,
15167
- * and so one section's MMR pass cannot starve other sections. Returns the
15168
- * input unchanged when disabled, when there are fewer than 2 candidates, or
15169
- * when no budget information is available.
15170
- */
15171
15259
  applyMmrToQmdResults(sectionId, results, caps = resolveCapabilities(this.config)) {
15172
- if (!caps.recallMmr) return results;
15173
- if (!Array.isArray(results) || results.length < 2) return results;
15174
- const configuredTopN = this.config.recallMmrTopN;
15175
- const topN = typeof configuredTopN === "number" && Number.isFinite(configuredTopN) ? Math.max(0, Math.floor(configuredTopN)) : 40;
15176
- if (topN === 0) return results;
15177
- const lambda = this.config.recallMmrLambda ?? 0.7;
15178
- const { reordered, diversity } = reorderRecallResultsWithMmr(results, {
15179
- lambda,
15180
- topN
15181
- });
15182
- try {
15183
- log.info(
15184
- `recall_mmr: section=${sectionId} kept=${diversity.kept}/${diversity.considered} headReorderCount=${diversity.headReorderCount} avgSimBefore=${diversity.avgPairwiseSimBefore.toFixed(3)} avgSimAfter=${diversity.avgPairwiseSimAfter.toFixed(3)} lambda=${lambda.toFixed(2)}`
15185
- );
15186
- } catch {
15187
- }
15188
- return reordered;
15260
+ return this.recallRerankCoordinator.applyMmrToQmdResults(sectionId, results, caps);
15189
15261
  }
15190
15262
  buildLastRecallBudgetSummary(options) {
15191
15263
  return {
@@ -15261,12 +15333,12 @@ ${reflectionsContent.trim()}
15261
15333
  */
15262
15334
  semanticDedupScopeFor(targetStorage) {
15263
15335
  if (!resolveNamespaceCapabilities(this.config).namespaces) return {};
15264
- const memoryDir = path4.resolve(this.config.memoryDir);
15265
- const storageDir = path4.resolve(targetStorage.dir);
15336
+ const memoryDir = path5.resolve(this.config.memoryDir);
15337
+ const storageDir = path5.resolve(targetStorage.dir);
15266
15338
  if (storageDir === memoryDir) {
15267
15339
  return { pathExcludePrefixes: ["namespaces/"] };
15268
15340
  }
15269
- let rel = path4.relative(memoryDir, storageDir);
15341
+ let rel = path5.relative(memoryDir, storageDir);
15270
15342
  if (!rel || rel.startsWith("..")) {
15271
15343
  log.debug(
15272
15344
  `semantic dedup: target storage dir ${storageDir} is outside memoryDir ${memoryDir}; scoping lookup to absolute path prefix`
@@ -15285,7 +15357,7 @@ ${reflectionsContent.trim()}
15285
15357
  if (hits.length === 0) return [];
15286
15358
  const results = [];
15287
15359
  for (const hit of hits) {
15288
- const fullPath = path4.isAbsolute(hit.path) ? hit.path : path4.join(this.config.memoryDir, hit.path);
15360
+ const fullPath = path5.isAbsolute(hit.path) ? hit.path : path5.join(this.config.memoryDir, hit.path);
15289
15361
  const memory = await this.storage.readMemoryByPath(fullPath);
15290
15362
  if (!memory) continue;
15291
15363
  results.push({
@@ -15482,7 +15554,7 @@ ${reflectionsContent.trim()}
15482
15554
  const storage = await this.storageRouter.storageFor(namespace);
15483
15555
  const storageDir = typeof storage.dir === "string" && storage.dir ? storage.dir : null;
15484
15556
  if (!storageDir) continue;
15485
- const recallRoot = path4.resolve(storageDir);
15557
+ const recallRoot = path5.resolve(storageDir);
15486
15558
  if (seenRecallRoots.has(recallRoot)) continue;
15487
15559
  seenRecallRoots.add(recallRoot);
15488
15560
  recallRoots.push(recallRoot);
@@ -15506,8 +15578,8 @@ ${reflectionsContent.trim()}
15506
15578
  if (resolvedCold) scopedResults.push(resolvedCold.result);
15507
15579
  continue;
15508
15580
  }
15509
- if (path4.isAbsolute(result.path)) {
15510
- const resolvedPath = path4.resolve(result.path);
15581
+ if (path5.isAbsolute(result.path)) {
15582
+ const resolvedPath = path5.resolve(result.path);
15511
15583
  if (recallRoots.some(
15512
15584
  (recallRoot) => isPathInsideStorageRoot(recallRoot, resolvedPath)
15513
15585
  )) {
@@ -16441,4 +16513,4 @@ export {
16441
16513
  resolvePersistedMemoryRelativePath,
16442
16514
  Orchestrator
16443
16515
  };
16444
- //# sourceMappingURL=chunk-EKQ3LWBM.js.map
16516
+ //# sourceMappingURL=chunk-IEWUPZ75.js.map