@remnic/core 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abort-error.d.ts +32 -0
- package/dist/abort-error.js +11 -0
- package/dist/access-cli.js +41 -40
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +3 -2
- package/dist/access-http.js +11 -11
- package/dist/access-mcp.d.ts +3 -2
- package/dist/access-mcp.js +7 -7
- package/dist/access-schema.d.ts +16 -16
- package/dist/access-service-HmO1Trrx.d.ts +732 -0
- package/dist/access-service.d.ts +11 -658
- package/dist/access-service.js +9 -9
- package/dist/bootstrap.d.ts +1 -1
- package/dist/briefing.js +6 -6
- package/dist/calibration.js +3 -3
- package/dist/causal-consolidation.js +10 -10
- package/dist/{chunk-PMB3WGDL.js → chunk-37UIFYWO.js} +64 -3
- package/dist/chunk-37UIFYWO.js.map +1 -0
- package/dist/{chunk-X4WESCKA.js → chunk-3QHL5ABG.js} +5 -5
- package/dist/{chunk-2VFW5K5U.js → chunk-3SV6CQHO.js} +10 -8
- package/dist/chunk-3SV6CQHO.js.map +1 -0
- package/dist/{chunk-U2IQTSBY.js → chunk-3WHVNEN7.js} +1 -1
- package/dist/chunk-3WHVNEN7.js.map +1 -0
- package/dist/{chunk-QKAH5B6E.js → chunk-44ICJRF3.js} +97 -9
- package/dist/chunk-44ICJRF3.js.map +1 -0
- package/dist/{chunk-S4LX5EBI.js → chunk-47UU5PU2.js} +48 -9
- package/dist/chunk-47UU5PU2.js.map +1 -0
- package/dist/{chunk-ECKDIK5F.js → chunk-4LACOVZX.js} +2 -2
- package/dist/{chunk-AYPYCLR7.js → chunk-6LX5ORAS.js} +2 -2
- package/dist/{chunk-GJQPH5G3.js → chunk-6UJ47TVX.js} +2 -2
- package/dist/{chunk-KWP7T3DP.js → chunk-7ECD5ATE.js} +2 -2
- package/dist/{chunk-74JR4N5J.js → chunk-7WQ6SLIE.js} +2 -2
- package/dist/{chunk-7PA4OZEU.js → chunk-BLKTA7MM.js} +6 -14
- package/dist/chunk-BLKTA7MM.js.map +1 -0
- package/dist/{chunk-XMGSSBFX.js → chunk-DEPL3635.js} +172 -100
- package/dist/chunk-DEPL3635.js.map +1 -0
- package/dist/{chunk-JROGC36Y.js → chunk-DHHP2Z4X.js} +2 -2
- package/dist/{chunk-BKQJBXXX.js → chunk-GGD5W7TB.js} +2 -2
- package/dist/chunk-GGD5W7TB.js.map +1 -0
- package/dist/{chunk-POMSFKTB.js → chunk-GV6NLQ4X.js} +14 -14
- package/dist/{chunk-AAI7JARD.js → chunk-HMDCOMYU.js} +8 -11
- package/dist/chunk-HMDCOMYU.js.map +1 -0
- package/dist/{chunk-POBPGDWI.js → chunk-ITRLGI2T.js} +2 -2
- package/dist/{chunk-MYQWXITD.js → chunk-JIU55F3X.js} +2 -2
- package/dist/{chunk-UPMD5XND.js → chunk-JL2PU6AI.js} +16 -5
- package/dist/chunk-JL2PU6AI.js.map +1 -0
- package/dist/{chunk-OJFGVJS6.js → chunk-MBJHSA7F.js} +65 -9
- package/dist/chunk-MBJHSA7F.js.map +1 -0
- package/dist/{chunk-V7XCAHIB.js → chunk-MVTHXUBX.js} +46 -23
- package/dist/chunk-MVTHXUBX.js.map +1 -0
- package/dist/{chunk-BTY5RRRF.js → chunk-N42IWANG.js} +5 -5
- package/dist/{chunk-NSB3WSYS.js → chunk-NQEVYWX6.js} +74 -3
- package/dist/chunk-NQEVYWX6.js.map +1 -0
- package/dist/chunk-OIT5QGG4.js +80 -0
- package/dist/chunk-OIT5QGG4.js.map +1 -0
- package/dist/chunk-PVGDJXVK.js +21 -0
- package/dist/chunk-PVGDJXVK.js.map +1 -0
- package/dist/{chunk-RCICHSHL.js → chunk-SYUK3VLY.js} +2 -2
- package/dist/{chunk-YFYL2SIJ.js → chunk-WBSAYXVI.js} +127 -39
- package/dist/chunk-WBSAYXVI.js.map +1 -0
- package/dist/{chunk-KEG4GNGI.js → chunk-XZ2TIKGC.js} +38 -8
- package/dist/chunk-XZ2TIKGC.js.map +1 -0
- package/dist/chunk-Y4FHOFJ2.js +140 -0
- package/dist/chunk-Y4FHOFJ2.js.map +1 -0
- package/dist/chunk-YNB73F22.js +137 -0
- package/dist/chunk-YNB73F22.js.map +1 -0
- package/dist/{chunk-HITJFT7E.js → chunk-ZVBB3T7V.js} +10 -5
- package/dist/chunk-ZVBB3T7V.js.map +1 -0
- package/dist/{cli-DwIBnp2g.d.ts → cli-BneVIEvh.d.ts} +2 -2
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +25 -24
- package/dist/config.js +1 -1
- package/dist/contradiction-review-WIUBAR52.js +21 -0
- package/dist/contradiction-review-WIUBAR52.js.map +1 -0
- package/dist/contradiction-scan-GR33PONM.js +376 -0
- package/dist/contradiction-scan-GR33PONM.js.map +1 -0
- package/dist/direct-answer-wiring.d.ts +77 -0
- package/dist/direct-answer-wiring.js +75 -0
- package/dist/direct-answer-wiring.js.map +1 -0
- package/dist/direct-answer.d.ts +106 -0
- package/dist/direct-answer.js +10 -0
- package/dist/direct-answer.js.map +1 -0
- package/dist/{engine-X7X3AAG3.js → engine-5TIQBYZR.js} +7 -7
- package/dist/engine-5TIQBYZR.js.map +1 -0
- package/dist/entity-retrieval.js +6 -6
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/extraction.js +6 -6
- package/dist/fallback-llm.d.ts +11 -2
- package/dist/fallback-llm.js +3 -3
- package/dist/harmonic-retrieval.js +2 -1
- package/dist/index.d.ts +10 -124
- package/dist/index.js +74 -137
- package/dist/index.js.map +1 -1
- package/dist/intent.js +1 -1
- package/dist/local-llm.d.ts +10 -3
- package/dist/local-llm.js +1 -1
- package/dist/operator-toolkit.js +12 -11
- package/dist/{orchestrator-B9kwlCep.d.ts → orchestrator-DRYA6_lW.d.ts} +21 -2
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +36 -35
- package/dist/qmd.js +2 -1
- package/dist/recall-state.d.ts +28 -1
- package/dist/recall-state.js +1 -1
- package/dist/resolution-QBTDHTG7.js +100 -0
- package/dist/resolution-QBTDHTG7.js.map +1 -0
- package/dist/resolve-provider-secret.d.ts +24 -1
- package/dist/resolve-provider-secret.js +3 -1
- package/dist/resume-bundles.js +3 -3
- package/dist/schemas.d.ts +14 -14
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +6 -6
- package/dist/semantic-rule-verifier.js +6 -6
- package/dist/storage.js +5 -5
- package/dist/summarizer.js +5 -5
- package/dist/types-DJhqDJUV.d.ts +50 -0
- package/dist/types.d.ts +34 -2
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +6 -6
- package/package.json +1 -1
- package/dist/chunk-2VFW5K5U.js.map +0 -1
- package/dist/chunk-7PA4OZEU.js.map +0 -1
- package/dist/chunk-AAI7JARD.js.map +0 -1
- package/dist/chunk-BKQJBXXX.js.map +0 -1
- package/dist/chunk-HITJFT7E.js.map +0 -1
- package/dist/chunk-KEG4GNGI.js.map +0 -1
- package/dist/chunk-NSB3WSYS.js.map +0 -1
- package/dist/chunk-OJFGVJS6.js.map +0 -1
- package/dist/chunk-PMB3WGDL.js.map +0 -1
- package/dist/chunk-QKAH5B6E.js.map +0 -1
- package/dist/chunk-S4LX5EBI.js.map +0 -1
- package/dist/chunk-U2IQTSBY.js.map +0 -1
- package/dist/chunk-UPMD5XND.js.map +0 -1
- package/dist/chunk-V7XCAHIB.js.map +0 -1
- package/dist/chunk-XMGSSBFX.js.map +0 -1
- package/dist/chunk-YFYL2SIJ.js.map +0 -1
- /package/dist/{engine-X7X3AAG3.js.map → abort-error.js.map} +0 -0
- /package/dist/{chunk-X4WESCKA.js.map → chunk-3QHL5ABG.js.map} +0 -0
- /package/dist/{chunk-ECKDIK5F.js.map → chunk-4LACOVZX.js.map} +0 -0
- /package/dist/{chunk-AYPYCLR7.js.map → chunk-6LX5ORAS.js.map} +0 -0
- /package/dist/{chunk-GJQPH5G3.js.map → chunk-6UJ47TVX.js.map} +0 -0
- /package/dist/{chunk-KWP7T3DP.js.map → chunk-7ECD5ATE.js.map} +0 -0
- /package/dist/{chunk-74JR4N5J.js.map → chunk-7WQ6SLIE.js.map} +0 -0
- /package/dist/{chunk-JROGC36Y.js.map → chunk-DHHP2Z4X.js.map} +0 -0
- /package/dist/{chunk-POMSFKTB.js.map → chunk-GV6NLQ4X.js.map} +0 -0
- /package/dist/{chunk-POBPGDWI.js.map → chunk-ITRLGI2T.js.map} +0 -0
- /package/dist/{chunk-MYQWXITD.js.map → chunk-JIU55F3X.js.map} +0 -0
- /package/dist/{chunk-BTY5RRRF.js.map → chunk-N42IWANG.js.map} +0 -0
- /package/dist/{chunk-RCICHSHL.js.map → chunk-SYUK3VLY.js.map} +0 -0
|
@@ -53,13 +53,25 @@ async function getGatewayResolver() {
|
|
|
53
53
|
async function findRuntimeModules() {
|
|
54
54
|
const { readdirSync } = await import("fs");
|
|
55
55
|
const { createRequire } = await import("module");
|
|
56
|
+
const { execFileSync } = await import("child_process");
|
|
56
57
|
const candidates = [];
|
|
57
58
|
const distDirs = [];
|
|
59
|
+
const pushDistDirs = (entryPath) => {
|
|
60
|
+
const resolvedEntryDir = path.dirname(entryPath);
|
|
61
|
+
const packageRoot = path.basename(resolvedEntryDir) === "dist" ? path.resolve(resolvedEntryDir, "..") : resolvedEntryDir;
|
|
62
|
+
const candidateDistDirs = [
|
|
63
|
+
path.join(packageRoot, "dist"),
|
|
64
|
+
path.join(packageRoot, "..", "dist")
|
|
65
|
+
];
|
|
66
|
+
for (const candidate of candidateDistDirs) {
|
|
67
|
+
const resolved = path.resolve(candidate);
|
|
68
|
+
if (!distDirs.includes(resolved)) distDirs.push(resolved);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
58
71
|
try {
|
|
59
72
|
const req = createRequire(import.meta.url);
|
|
60
73
|
const openclawMain = req.resolve("openclaw");
|
|
61
|
-
|
|
62
|
-
if (openclawDist) distDirs.push(path.resolve(openclawDist));
|
|
74
|
+
pushDistDirs(openclawMain);
|
|
63
75
|
} catch {
|
|
64
76
|
}
|
|
65
77
|
try {
|
|
@@ -68,12 +80,22 @@ async function findRuntimeModules() {
|
|
|
68
80
|
if (mainScript) {
|
|
69
81
|
const realScript = realpathSync(mainScript);
|
|
70
82
|
if (realScript.includes("openclaw")) {
|
|
71
|
-
|
|
72
|
-
if (!distDirs.includes(distDir)) distDirs.push(distDir);
|
|
83
|
+
pushDistDirs(realScript);
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
} catch {
|
|
76
87
|
}
|
|
88
|
+
try {
|
|
89
|
+
const openclawBin = execFileSync("which", ["openclaw"], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
92
|
+
}).trim();
|
|
93
|
+
if (openclawBin) {
|
|
94
|
+
const { realpathSync } = await import("fs");
|
|
95
|
+
pushDistDirs(realpathSync(openclawBin));
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
77
99
|
for (const dir of distDirs) {
|
|
78
100
|
try {
|
|
79
101
|
const files = readdirSync(dir);
|
|
@@ -88,7 +110,10 @@ async function findRuntimeModules() {
|
|
|
88
110
|
return candidates;
|
|
89
111
|
}
|
|
90
112
|
async function resolveProviderApiKey(providerId, apiKeyValue, gatewayConfig, agentDir) {
|
|
91
|
-
const
|
|
113
|
+
const resolvedAgentDir = path.resolve(
|
|
114
|
+
agentDir ?? path.join(os.homedir(), ".openclaw", "agents", "main", "agent")
|
|
115
|
+
);
|
|
116
|
+
const cacheKey = `provider:${providerId}:agentDir:${resolvedAgentDir}`;
|
|
92
117
|
if (resolvedCache.has(cacheKey)) {
|
|
93
118
|
return resolvedCache.get(cacheKey);
|
|
94
119
|
}
|
|
@@ -104,7 +129,6 @@ async function resolveProviderApiKey(providerId, apiKeyValue, gatewayConfig, age
|
|
|
104
129
|
const resolver = await getGatewayResolver();
|
|
105
130
|
if (resolver) {
|
|
106
131
|
try {
|
|
107
|
-
const resolvedAgentDir = agentDir ?? path.join(os.homedir(), ".openclaw", "agents", "main", "agent");
|
|
108
132
|
const auth = await resolver({ provider: providerId, cfg: gatewayConfig, agentDir: resolvedAgentDir });
|
|
109
133
|
if (auth?.apiKey) {
|
|
110
134
|
resolved = auth.apiKey;
|
|
@@ -154,10 +178,16 @@ function clearSecretCache() {
|
|
|
154
178
|
_resolverLoaded = false;
|
|
155
179
|
_resolverNextRetryAt = 0;
|
|
156
180
|
}
|
|
181
|
+
function __setGatewayResolverForTest(resolver) {
|
|
182
|
+
_resolveApiKeyForProvider = resolver;
|
|
183
|
+
_resolverLoaded = resolver !== null;
|
|
184
|
+
_resolverNextRetryAt = 0;
|
|
185
|
+
}
|
|
157
186
|
|
|
158
187
|
export {
|
|
159
188
|
resolveProviderApiKey,
|
|
160
189
|
getGatewayRuntimeAuthForModel,
|
|
161
|
-
clearSecretCache
|
|
190
|
+
clearSecretCache,
|
|
191
|
+
__setGatewayResolverForTest
|
|
162
192
|
};
|
|
163
|
-
//# sourceMappingURL=chunk-
|
|
193
|
+
//# sourceMappingURL=chunk-XZ2TIKGC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resolve-provider-secret.ts"],"sourcesContent":["import { log } from \"./logger.js\";\nimport { readEnvVar } from \"./runtime/env.js\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\n/**\n * Resolve a provider API key using OpenClaw's own auth resolution system.\n *\n * This module delegates to the gateway's `resolveApiKeyForProvider()` function,\n * which handles all secret reference formats (SecretRef objects, auth profiles,\n * \"secretref-managed\" markers, environment variables, etc.) using the same\n * codepath the gateway uses for its own agent sessions.\n *\n * For plain-text API keys, a fast path returns them directly without\n * involving the gateway auth system.\n *\n * Results are cached per provider for the gateway process lifetime.\n */\n\ntype ResolveApiKeyFn = (params: {\n provider: string;\n cfg?: unknown;\n agentDir?: string;\n}) => Promise<{ apiKey?: string; source?: string; mode?: string } | null>;\n\n/**\n * Resolve request-ready auth for a model, including provider-owned transforms\n * (e.g., OAuth token exchange, base URL override for openai-codex).\n */\nexport type GetRuntimeAuthForModelFn = (params: {\n model: { provider: string; id: string; api?: string; baseUrl?: string };\n cfg?: unknown;\n workspaceDir?: string;\n}) => Promise<{\n apiKey?: string;\n baseUrl?: string;\n source?: string;\n mode?: string;\n profileId?: string;\n} | null>;\n\nlet _resolveApiKeyForProvider: ResolveApiKeyFn | null = null;\nlet _getRuntimeAuthForModel: GetRuntimeAuthForModelFn | null = null;\nlet _resolverLoaded = false;\nlet _resolverNextRetryAt = 0;\nconst RESOLVER_RETRY_BACKOFF_MS = 60_000; // 1 minute between retries after first failure\nconst resolvedCache = new Map<string, string | undefined>();\n\n/**\n * Lazily load the gateway's resolveApiKeyForProvider function.\n * Returns null if not available (e.g., running outside the gateway process).\n */\nasync function getGatewayResolver(): Promise<ResolveApiKeyFn | null> {\n if (_resolverLoaded) {\n return _resolveApiKeyForProvider;\n }\n // Backoff: don't re-scan filesystem on every call when module wasn't found.\n // After a failure, wait RESOLVER_RETRY_BACKOFF_MS before trying again.\n if (_resolverNextRetryAt > 0 && Date.now() < _resolverNextRetryAt) {\n return null;\n }\n\n try {\n // The gateway bundles this in a runtime chunk — import it dynamically.\n // This import path is stable across gateway versions since it's a named runtime export.\n const candidates = [\n // Try glob-matching the runtime module name (hash varies per build)\n ...await findRuntimeModules(),\n ];\n\n const { pathToFileURL } = await import(\"node:url\");\n for (const candidate of candidates) {\n try {\n // Convert native path to file:// URL for cross-platform ESM import compatibility\n const importUrl = pathToFileURL(candidate).href;\n const mod = await import(importUrl);\n if (typeof mod.resolveApiKeyForProvider === \"function\") {\n _resolveApiKeyForProvider = mod.resolveApiKeyForProvider;\n if (typeof mod.getRuntimeAuthForModel === \"function\") {\n _getRuntimeAuthForModel = mod.getRuntimeAuthForModel;\n log.debug(\"loaded gateway getRuntimeAuthForModel from runtime module\");\n }\n _resolverLoaded = true;\n log.debug(\"loaded gateway resolveApiKeyForProvider from runtime module\");\n return _resolveApiKeyForProvider;\n }\n } catch {\n // Try next candidate\n }\n }\n } catch {\n // Silent\n }\n\n // Backoff before retrying — avoid repeated fs scanning.\n // Retries after RESOLVER_RETRY_BACKOFF_MS so the resolver can\n // recover if the gateway restarts or the module becomes available.\n _resolverNextRetryAt = Date.now() + RESOLVER_RETRY_BACKOFF_MS;\n log.debug(`gateway resolveApiKeyForProvider not available — will retry after ${RESOLVER_RETRY_BACKOFF_MS / 1000}s`);\n return null;\n}\n\n/**\n * Find the gateway's model-auth runtime module by scanning the dist directory.\n * Uses require.resolve to find the openclaw package regardless of install method.\n */\nasync function findRuntimeModules(): Promise<string[]> {\n const { readdirSync } = await import(\"node:fs\");\n const { createRequire } = await import(\"node:module\");\n const { execFileSync } = await import(\"node:child_process\");\n const candidates: string[] = [];\n\n // Discover the openclaw dist directory from the installed package,\n // regardless of how it was installed (Homebrew, npm global, local, etc.)\n const distDirs: string[] = [];\n const pushDistDirs = (entryPath: string): void => {\n const resolvedEntryDir = path.dirname(entryPath);\n const packageRoot = path.basename(resolvedEntryDir) === \"dist\"\n ? path.resolve(resolvedEntryDir, \"..\")\n : resolvedEntryDir;\n const candidateDistDirs = [\n path.join(packageRoot, \"dist\"),\n path.join(packageRoot, \"..\", \"dist\"),\n ];\n for (const candidate of candidateDistDirs) {\n const resolved = path.resolve(candidate);\n if (!distDirs.includes(resolved)) distDirs.push(resolved);\n }\n };\n\n try {\n // require.resolve finds the package from the current process context\n const req = createRequire(import.meta.url);\n const openclawMain = req.resolve(\"openclaw\");\n pushDistDirs(openclawMain);\n } catch {\n // openclaw not resolvable from plugin context — try alternate paths\n }\n\n // Fallback: infer from the running process (gateway runs from its own dist/)\n // Use fs.realpathSync to resolve symlinks (e.g., /usr/local/bin/openclaw → actual path)\n try {\n const { realpathSync } = await import(\"node:fs\");\n const mainScript = process.argv[1];\n if (mainScript) {\n const realScript = realpathSync(mainScript);\n if (realScript.includes(\"openclaw\")) {\n pushDistDirs(realScript);\n }\n }\n } catch {\n // Silent\n }\n\n // Fallback: inspect the installed openclaw binary on PATH (Homebrew/global npm).\n try {\n const openclawBin = execFileSync(\"which\", [\"openclaw\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n if (openclawBin) {\n const { realpathSync } = await import(\"node:fs\");\n pushDistDirs(realpathSync(openclawBin));\n }\n } catch {\n // Silent\n }\n\n for (const dir of distDirs) {\n try {\n const files = readdirSync(dir);\n for (const f of files) {\n if (f.startsWith(\"runtime-model-auth.runtime-\") && f.endsWith(\".js\")) {\n candidates.push(path.join(dir, f));\n }\n }\n } catch {\n // Directory doesn't exist — skip\n }\n }\n\n return candidates;\n}\n\n/**\n * Resolve a provider API key from various OpenClaw formats.\n *\n * Resolution order:\n * 1. Plain-text string → returned immediately\n * 2. Gateway's resolveApiKeyForProvider → handles all secret ref formats\n * 3. Environment variable fallback (PROVIDER_NAME_API_KEY)\n * 4. undefined → provider is skipped in the fallback chain\n */\nexport async function resolveProviderApiKey(\n providerId: string,\n apiKeyValue: unknown,\n gatewayConfig?: unknown,\n agentDir?: string,\n): Promise<string | undefined> {\n const resolvedAgentDir = path.resolve(\n agentDir ?? path.join(os.homedir(), \".openclaw\", \"agents\", \"main\", \"agent\"),\n );\n\n // Check cache first\n const cacheKey = `provider:${providerId}:agentDir:${resolvedAgentDir}`;\n if (resolvedCache.has(cacheKey)) {\n return resolvedCache.get(cacheKey);\n }\n\n let resolved: string | undefined;\n\n // Fast path: plain-text string that looks like an actual API key\n if (typeof apiKeyValue === \"string\" && apiKeyValue.trim().length > 0) {\n // Skip known non-API-key markers used by the gateway for auth modes\n // that don't use bearer tokens (OAuth, local endpoints, GCP credentials)\n if (\n apiKeyValue === \"secretref-managed\" ||\n apiKeyValue.endsWith(\"-oauth\") ||\n apiKeyValue.endsWith(\"-local\") ||\n apiKeyValue === \"lm-studio\" ||\n apiKeyValue.startsWith(\"gcp-\")\n ) {\n // Fall through to gateway resolver / env var fallback\n } else {\n resolved = apiKeyValue;\n resolvedCache.set(cacheKey, resolved);\n return resolved;\n }\n }\n\n // The API key is either a SecretRef object, \"secretref-managed\", or empty.\n // Try the gateway's own auth resolution system first.\n const resolver = await getGatewayResolver();\n if (resolver) {\n try {\n const auth = await resolver({ provider: providerId, cfg: gatewayConfig, agentDir: resolvedAgentDir });\n if (auth?.apiKey) {\n resolved = auth.apiKey;\n log.debug(`resolved API key for provider \"${providerId}\" via gateway auth (source: ${auth.source ?? \"unknown\"}, mode: ${auth.mode ?? \"unknown\"})`);\n resolvedCache.set(cacheKey, resolved);\n return resolved;\n }\n } catch (err) {\n log.debug(\n `gateway auth resolution failed for provider \"${providerId}\": ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Environment variable fallback\n resolved = resolveFromEnv(providerId);\n if (resolved) {\n log.debug(`resolved API key for provider \"${providerId}\" from environment variable`);\n } else {\n log.debug(`could not resolve API key for provider \"${providerId}\" — skipping`);\n }\n\n // Only cache successful resolutions — failures are retried on next call\n // so providers can recover after transient issues (e.g., 1Password agent restart)\n if (resolved) {\n resolvedCache.set(cacheKey, resolved);\n }\n return resolved;\n}\n\n/**\n * Try to resolve an API key from environment variables.\n */\nfunction resolveFromEnv(providerId: string): string | undefined {\n const normalized = providerId.toUpperCase().replace(/[^A-Z0-9]/g, \"_\");\n const candidates = [\n `${normalized}_API_KEY`,\n `${normalized}_TOKEN`,\n ];\n for (const envVar of candidates) {\n const value = readEnvVar(envVar);\n if (value && value.trim().length > 0) {\n return value.trim();\n }\n }\n return undefined;\n}\n\n/**\n * Get the gateway's getRuntimeAuthForModel function, if available.\n * This resolves request-ready auth including provider-owned transforms\n * (OAuth token exchange, base URL override for codex/copilot/etc.).\n * Must be called after at least one resolveProviderApiKey() call to\n * trigger the lazy module load.\n */\nexport async function getGatewayRuntimeAuthForModel(): Promise<GetRuntimeAuthForModelFn | null> {\n // Ensure the runtime module has been loaded\n await getGatewayResolver();\n return _getRuntimeAuthForModel;\n}\n\n/**\n * Clear the resolution cache (useful for testing or key rotation).\n */\nexport function clearSecretCache(): void {\n resolvedCache.clear();\n _resolveApiKeyForProvider = null;\n _getRuntimeAuthForModel = null;\n _resolverLoaded = false;\n _resolverNextRetryAt = 0;\n}\n\nexport function __setGatewayResolverForTest(resolver: ResolveApiKeyFn | null): void {\n _resolveApiKeyForProvider = resolver;\n _resolverLoaded = resolver !== null;\n _resolverNextRetryAt = 0;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
countRecallTokenOverlap,
|
|
3
|
+
normalizeRecallTokens
|
|
4
|
+
} from "./chunk-DT5TVLJE.js";
|
|
5
|
+
|
|
6
|
+
// src/direct-answer.ts
|
|
7
|
+
var FILTER_LABELS = {
|
|
8
|
+
nonActiveStatus: "non-active-status",
|
|
9
|
+
notTrustedZone: "not-trusted-zone",
|
|
10
|
+
ineligibleTaxonomyBucket: "ineligible-taxonomy-bucket",
|
|
11
|
+
belowImportanceFloor: "below-importance-floor",
|
|
12
|
+
entityRefMismatch: "entity-ref-mismatch",
|
|
13
|
+
belowTokenOverlapFloor: "below-token-overlap-floor"
|
|
14
|
+
};
|
|
15
|
+
function isDirectAnswerEligible(input) {
|
|
16
|
+
const { query, candidates, config, queryEntityRefs } = input;
|
|
17
|
+
if (!config.enabled) {
|
|
18
|
+
return {
|
|
19
|
+
eligible: false,
|
|
20
|
+
reason: "disabled",
|
|
21
|
+
narrative: "direct-answer tier is disabled",
|
|
22
|
+
filteredBy: []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const queryTokens = new Set(normalizeRecallTokens(query));
|
|
26
|
+
if (queryTokens.size === 0) {
|
|
27
|
+
return {
|
|
28
|
+
eligible: false,
|
|
29
|
+
reason: "empty-query",
|
|
30
|
+
narrative: "query has no searchable tokens after normalization",
|
|
31
|
+
filteredBy: []
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (candidates.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
eligible: false,
|
|
37
|
+
reason: "no-candidates",
|
|
38
|
+
narrative: "no candidates supplied",
|
|
39
|
+
filteredBy: []
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const filteredBy = [];
|
|
43
|
+
let working = candidates;
|
|
44
|
+
working = applyFilter(working, filteredBy, FILTER_LABELS.nonActiveStatus, (c) => {
|
|
45
|
+
const status = c.memory.frontmatter.status ?? "active";
|
|
46
|
+
return status === "active";
|
|
47
|
+
});
|
|
48
|
+
working = applyFilter(
|
|
49
|
+
working,
|
|
50
|
+
filteredBy,
|
|
51
|
+
FILTER_LABELS.notTrustedZone,
|
|
52
|
+
(c) => c.trustZone === "trusted"
|
|
53
|
+
);
|
|
54
|
+
working = applyFilter(
|
|
55
|
+
working,
|
|
56
|
+
filteredBy,
|
|
57
|
+
FILTER_LABELS.ineligibleTaxonomyBucket,
|
|
58
|
+
(c) => c.taxonomyBucket !== null && config.eligibleTaxonomyBuckets.includes(c.taxonomyBucket)
|
|
59
|
+
);
|
|
60
|
+
working = applyFilter(working, filteredBy, FILTER_LABELS.belowImportanceFloor, (c) => {
|
|
61
|
+
if (c.memory.frontmatter.verificationState === "user_confirmed") return true;
|
|
62
|
+
return c.importanceScore >= config.importanceFloor;
|
|
63
|
+
});
|
|
64
|
+
if (queryEntityRefs && queryEntityRefs.length > 0) {
|
|
65
|
+
const normRefs = new Set(queryEntityRefs.map((r) => r.toLowerCase()));
|
|
66
|
+
working = applyFilter(working, filteredBy, FILTER_LABELS.entityRefMismatch, (c) => {
|
|
67
|
+
const ref = c.memory.frontmatter.entityRef;
|
|
68
|
+
if (!ref) return true;
|
|
69
|
+
return normRefs.has(ref.toLowerCase());
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (working.length === 0) {
|
|
73
|
+
return {
|
|
74
|
+
eligible: false,
|
|
75
|
+
reason: "no-eligible-candidates",
|
|
76
|
+
narrative: "no candidates survived eligibility filters",
|
|
77
|
+
filteredBy
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const scored = working.map((candidate) => {
|
|
81
|
+
const searchable = `${candidate.memory.frontmatter.tags?.join(" ") ?? ""} ${candidate.memory.content}`.trim();
|
|
82
|
+
const matches = countRecallTokenOverlap(queryTokens, searchable);
|
|
83
|
+
return { candidate, tokenOverlap: matches / queryTokens.size };
|
|
84
|
+
});
|
|
85
|
+
const overlapSurvivors = scored.filter((s) => s.tokenOverlap >= config.tokenOverlapFloor);
|
|
86
|
+
if (overlapSurvivors.length < scored.length) {
|
|
87
|
+
filteredBy.push(FILTER_LABELS.belowTokenOverlapFloor);
|
|
88
|
+
}
|
|
89
|
+
if (overlapSurvivors.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
eligible: false,
|
|
92
|
+
reason: "below-token-overlap-floor",
|
|
93
|
+
narrative: `no candidate met token-overlap floor ${config.tokenOverlapFloor}`,
|
|
94
|
+
filteredBy
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
overlapSurvivors.sort(compareScored);
|
|
98
|
+
if (overlapSurvivors.length >= 2) {
|
|
99
|
+
const topScore = scoreFor(overlapSurvivors[0]);
|
|
100
|
+
const secondScore = scoreFor(overlapSurvivors[1]);
|
|
101
|
+
if (topScore - secondScore < config.ambiguityMargin) {
|
|
102
|
+
return {
|
|
103
|
+
eligible: false,
|
|
104
|
+
reason: "ambiguous",
|
|
105
|
+
narrative: `top two candidates within ambiguityMargin ${config.ambiguityMargin}`,
|
|
106
|
+
filteredBy
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const winner = overlapSurvivors[0];
|
|
111
|
+
const bucket = winner.candidate.taxonomyBucket ?? "unknown";
|
|
112
|
+
return {
|
|
113
|
+
eligible: true,
|
|
114
|
+
reason: "eligible",
|
|
115
|
+
winner: winner.candidate,
|
|
116
|
+
tokenOverlap: winner.tokenOverlap,
|
|
117
|
+
narrative: `trusted ${bucket}, unambiguous, token-overlap ${winner.tokenOverlap.toFixed(2)}`,
|
|
118
|
+
filteredBy
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function applyFilter(working, filteredBy, label, keep) {
|
|
122
|
+
const before = working.length;
|
|
123
|
+
const next = working.filter(keep);
|
|
124
|
+
if (next.length < before) filteredBy.push(label);
|
|
125
|
+
return next;
|
|
126
|
+
}
|
|
127
|
+
function scoreFor(s) {
|
|
128
|
+
return s.candidate.matchScore ?? s.tokenOverlap;
|
|
129
|
+
}
|
|
130
|
+
function compareScored(a, b) {
|
|
131
|
+
const diff = scoreFor(b) - scoreFor(a);
|
|
132
|
+
if (diff !== 0) return diff;
|
|
133
|
+
return a.candidate.memory.path.localeCompare(b.candidate.memory.path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
FILTER_LABELS,
|
|
138
|
+
isDirectAnswerEligible
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=chunk-Y4FHOFJ2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/direct-answer.ts"],"sourcesContent":["/**\n * Direct-answer retrieval tier eligibility (issue #518 slice 2).\n *\n * This module is a pure decision layer. It takes a query, a set of\n * caller-resolved candidates (each already decorated with trust-zone,\n * taxonomy-bucket, and importance information), and the direct-answer\n * config, then returns an eligibility verdict.\n *\n * Keeping the module pure means:\n *\n * - Tests do not need a trust-zones store, taxonomy resolver, or importance\n * scorer on disk.\n * - Slice 3 (retrieval.ts wiring) is responsible for resolving those signals\n * before calling in; the wiring layer decides where candidates come from\n * (entity index, token prefilter, etc.). This module only decides\n * whether the surfaced candidates add up to a confident direct answer.\n *\n * Not wired into retrieval yet — see slice 3.\n */\n\nimport type { MemoryFile, MemoryStatus } from \"./types.js\";\nimport type { TrustZoneName } from \"./trust-zones.js\";\nimport {\n countRecallTokenOverlap,\n normalizeRecallTokens,\n} from \"./recall-tokenization.js\";\n\n/**\n * Caller-supplied candidate.\n *\n * `trustZone`, `taxonomyBucket`, and `importanceScore` are resolved outside\n * this module because each comes from a different subsystem. Passing them\n * as inputs keeps the function deterministic and easy to unit-test.\n *\n * `matchScore` is optional; if omitted, candidates are ranked by\n * token-overlap ratio. Callers that already computed a better ranking\n * score (e.g. BM25, vector similarity) can supply it to drive the\n * ambiguity check.\n */\nexport interface DirectAnswerCandidate {\n memory: MemoryFile;\n trustZone: TrustZoneName | null;\n taxonomyBucket: string | null;\n importanceScore: number;\n matchScore?: number;\n}\n\nexport interface DirectAnswerConfig {\n enabled: boolean;\n tokenOverlapFloor: number;\n importanceFloor: number;\n ambiguityMargin: number;\n eligibleTaxonomyBuckets: string[];\n}\n\nexport interface DirectAnswerInput {\n query: string;\n candidates: DirectAnswerCandidate[];\n config: DirectAnswerConfig;\n /**\n * Optional entity-ref hints resolved from the query upstream. When\n * supplied, a candidate with a set `entityRef` must match one of these\n * (case-insensitive) to remain eligible. Candidates without an\n * `entityRef` are allowed through regardless.\n */\n queryEntityRefs?: string[];\n}\n\nexport type DirectAnswerReason =\n | \"disabled\"\n | \"empty-query\"\n | \"no-candidates\"\n | \"no-eligible-candidates\"\n | \"below-token-overlap-floor\"\n | \"ambiguous\"\n | \"eligible\";\n\nexport interface DirectAnswerResult {\n eligible: boolean;\n reason: DirectAnswerReason;\n /** Winning candidate when eligible. */\n winner?: DirectAnswerCandidate;\n /** Computed token-overlap ratio (0..1) of the winner. */\n tokenOverlap?: number;\n /**\n * Human-readable summary suitable for\n * `RecallTierExplain.tierReason`.\n */\n narrative: string;\n /**\n * Filter labels that eliminated at least one candidate along the way.\n * Populated regardless of eligibility so the caller can surface the\n * narrowing steps in `RecallTierExplain.filteredBy`.\n */\n filteredBy: string[];\n}\n\n/** Filter labels — exported so callers and tests can match them structurally. */\nexport const FILTER_LABELS = {\n nonActiveStatus: \"non-active-status\",\n notTrustedZone: \"not-trusted-zone\",\n ineligibleTaxonomyBucket: \"ineligible-taxonomy-bucket\",\n belowImportanceFloor: \"below-importance-floor\",\n entityRefMismatch: \"entity-ref-mismatch\",\n belowTokenOverlapFloor: \"below-token-overlap-floor\",\n} as const;\n\ninterface ScoredCandidate {\n candidate: DirectAnswerCandidate;\n tokenOverlap: number;\n}\n\n/**\n * Determine whether a query can be served by the direct-answer tier.\n *\n * Decision ladder, in order:\n *\n * 1. config.enabled === false → \"disabled\"\n * 2. empty query tokens → \"empty-query\"\n * 3. empty candidates → \"no-candidates\"\n * 4. hard filters drop all candidates → \"no-eligible-candidates\"\n * 5. token-overlap floor drops all → \"below-token-overlap-floor\"\n * 6. top two candidates within ambiguityMargin → \"ambiguous\"\n * 7. otherwise → \"eligible\"\n */\nexport function isDirectAnswerEligible(\n input: DirectAnswerInput,\n): DirectAnswerResult {\n const { query, candidates, config, queryEntityRefs } = input;\n\n if (!config.enabled) {\n return {\n eligible: false,\n reason: \"disabled\",\n narrative: \"direct-answer tier is disabled\",\n filteredBy: [],\n };\n }\n\n const queryTokens = new Set(normalizeRecallTokens(query));\n if (queryTokens.size === 0) {\n return {\n eligible: false,\n reason: \"empty-query\",\n narrative: \"query has no searchable tokens after normalization\",\n filteredBy: [],\n };\n }\n\n if (candidates.length === 0) {\n return {\n eligible: false,\n reason: \"no-candidates\",\n narrative: \"no candidates supplied\",\n filteredBy: [],\n };\n }\n\n const filteredBy: string[] = [];\n let working: DirectAnswerCandidate[] = candidates;\n\n working = applyFilter(working, filteredBy, FILTER_LABELS.nonActiveStatus, (c) => {\n const status: MemoryStatus = c.memory.frontmatter.status ?? \"active\";\n return status === \"active\";\n });\n\n working = applyFilter(working, filteredBy, FILTER_LABELS.notTrustedZone, (c) =>\n c.trustZone === \"trusted\",\n );\n\n working = applyFilter(\n working,\n filteredBy,\n FILTER_LABELS.ineligibleTaxonomyBucket,\n (c) =>\n c.taxonomyBucket !== null &&\n config.eligibleTaxonomyBuckets.includes(c.taxonomyBucket),\n );\n\n working = applyFilter(working, filteredBy, FILTER_LABELS.belowImportanceFloor, (c) => {\n if (c.memory.frontmatter.verificationState === \"user_confirmed\") return true;\n return c.importanceScore >= config.importanceFloor;\n });\n\n if (queryEntityRefs && queryEntityRefs.length > 0) {\n const normRefs = new Set(queryEntityRefs.map((r) => r.toLowerCase()));\n working = applyFilter(working, filteredBy, FILTER_LABELS.entityRefMismatch, (c) => {\n const ref = c.memory.frontmatter.entityRef;\n if (!ref) return true;\n return normRefs.has(ref.toLowerCase());\n });\n }\n\n if (working.length === 0) {\n return {\n eligible: false,\n reason: \"no-eligible-candidates\",\n narrative: \"no candidates survived eligibility filters\",\n filteredBy,\n };\n }\n\n const scored: ScoredCandidate[] = working.map((candidate) => {\n const searchable =\n `${candidate.memory.frontmatter.tags?.join(\" \") ?? \"\"} ${candidate.memory.content}`.trim();\n const matches = countRecallTokenOverlap(queryTokens, searchable);\n return { candidate, tokenOverlap: matches / queryTokens.size };\n });\n\n const overlapSurvivors = scored.filter((s) => s.tokenOverlap >= config.tokenOverlapFloor);\n if (overlapSurvivors.length < scored.length) {\n filteredBy.push(FILTER_LABELS.belowTokenOverlapFloor);\n }\n\n if (overlapSurvivors.length === 0) {\n return {\n eligible: false,\n reason: \"below-token-overlap-floor\",\n narrative: `no candidate met token-overlap floor ${config.tokenOverlapFloor}`,\n filteredBy,\n };\n }\n\n overlapSurvivors.sort(compareScored);\n\n if (overlapSurvivors.length >= 2) {\n const topScore = scoreFor(overlapSurvivors[0]);\n const secondScore = scoreFor(overlapSurvivors[1]);\n if (topScore - secondScore < config.ambiguityMargin) {\n return {\n eligible: false,\n reason: \"ambiguous\",\n narrative: `top two candidates within ambiguityMargin ${config.ambiguityMargin}`,\n filteredBy,\n };\n }\n }\n\n const winner = overlapSurvivors[0];\n const bucket = winner.candidate.taxonomyBucket ?? \"unknown\";\n return {\n eligible: true,\n reason: \"eligible\",\n winner: winner.candidate,\n tokenOverlap: winner.tokenOverlap,\n narrative: `trusted ${bucket}, unambiguous, token-overlap ${winner.tokenOverlap.toFixed(2)}`,\n filteredBy,\n };\n}\n\nfunction applyFilter(\n working: DirectAnswerCandidate[],\n filteredBy: string[],\n label: string,\n keep: (c: DirectAnswerCandidate) => boolean,\n): DirectAnswerCandidate[] {\n const before = working.length;\n const next = working.filter(keep);\n if (next.length < before) filteredBy.push(label);\n return next;\n}\n\nfunction scoreFor(s: ScoredCandidate): number {\n return s.candidate.matchScore ?? s.tokenOverlap;\n}\n\nfunction compareScored(a: ScoredCandidate, b: ScoredCandidate): number {\n const diff = scoreFor(b) - scoreFor(a);\n if (diff !== 0) return diff;\n // Stable secondary key on path so the comparator returns 0 only for equal\n // entries (CLAUDE.md rule 19).\n return a.candidate.memory.path.localeCompare(b.candidate.memory.path);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/contradiction/contradiction-review.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
function computePairId(memoryIdA, memoryIdB) {
|
|
6
|
+
const sorted = [memoryIdA, memoryIdB].sort();
|
|
7
|
+
return createHash("sha256").update(sorted.join("::")).digest("hex").slice(0, 24);
|
|
8
|
+
}
|
|
9
|
+
function reviewDir(memoryDir) {
|
|
10
|
+
return path.join(memoryDir, ".review", "contradictions");
|
|
11
|
+
}
|
|
12
|
+
function pairPath(memoryDir, pairId) {
|
|
13
|
+
if (pairId.includes("/") || pairId.includes("\\") || pairId.includes("..")) {
|
|
14
|
+
throw new Error(`Invalid pairId: ${pairId}`);
|
|
15
|
+
}
|
|
16
|
+
return path.join(reviewDir(memoryDir), `${pairId}.json`);
|
|
17
|
+
}
|
|
18
|
+
function ensureDir(memoryDir) {
|
|
19
|
+
const dir = reviewDir(memoryDir);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writePair(memoryDir, pair) {
|
|
25
|
+
ensureDir(memoryDir);
|
|
26
|
+
const pairId = computePairId(pair.memoryIds[0], pair.memoryIds[1]);
|
|
27
|
+
const existing = readPair(memoryDir, pairId);
|
|
28
|
+
if (existing?.resolution) {
|
|
29
|
+
return existing;
|
|
30
|
+
}
|
|
31
|
+
if (existing && existing.confidence >= pair.confidence) {
|
|
32
|
+
return existing;
|
|
33
|
+
}
|
|
34
|
+
const full = {
|
|
35
|
+
...pair,
|
|
36
|
+
pairId,
|
|
37
|
+
lastReviewedAt: existing?.lastReviewedAt,
|
|
38
|
+
resolution: existing?.resolution
|
|
39
|
+
};
|
|
40
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
41
|
+
const tmpPath = `${filePath}.tmp`;
|
|
42
|
+
fs.writeFileSync(tmpPath, JSON.stringify(full, null, 2), "utf-8");
|
|
43
|
+
fs.renameSync(tmpPath, filePath);
|
|
44
|
+
return full;
|
|
45
|
+
}
|
|
46
|
+
function writePairs(memoryDir, pairs) {
|
|
47
|
+
const seen = /* @__PURE__ */ new Set();
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const pair of pairs) {
|
|
50
|
+
const key = computePairId(pair.memoryIds[0], pair.memoryIds[1]);
|
|
51
|
+
if (seen.has(key)) continue;
|
|
52
|
+
seen.add(key);
|
|
53
|
+
results.push(writePair(memoryDir, pair));
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
function readPair(memoryDir, pairId) {
|
|
58
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
59
|
+
try {
|
|
60
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.memoryIds)) {
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function listPairs(memoryDir, options) {
|
|
71
|
+
const startTime = Date.now();
|
|
72
|
+
const dir = reviewDir(memoryDir);
|
|
73
|
+
const { filter = "all", namespace, limit = 50 } = options ?? {};
|
|
74
|
+
const pairs = [];
|
|
75
|
+
let total = 0;
|
|
76
|
+
if (!fs.existsSync(dir)) {
|
|
77
|
+
return { pairs: [], total: 0, durationMs: Date.now() - startTime };
|
|
78
|
+
}
|
|
79
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
80
|
+
if (!entry.endsWith(".json")) continue;
|
|
81
|
+
try {
|
|
82
|
+
const raw = fs.readFileSync(path.join(dir, entry), "utf-8");
|
|
83
|
+
const pair = JSON.parse(raw);
|
|
84
|
+
if (typeof pair !== "object" || pair === null) continue;
|
|
85
|
+
if (!Array.isArray(pair.memoryIds)) continue;
|
|
86
|
+
if (namespace && pair.namespace !== namespace) continue;
|
|
87
|
+
if (filter === "unresolved") {
|
|
88
|
+
if (pair.resolution) continue;
|
|
89
|
+
if (pair.verdict === "independent") continue;
|
|
90
|
+
} else if (filter !== "all" && pair.verdict !== filter) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
total++;
|
|
94
|
+
if (pairs.length < limit) pairs.push(pair);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { pairs, total, durationMs: Date.now() - startTime };
|
|
100
|
+
}
|
|
101
|
+
function isCoolingDown(pair, cooldownDays) {
|
|
102
|
+
if (cooldownDays <= 0) return false;
|
|
103
|
+
if (!pair.lastReviewedAt) return false;
|
|
104
|
+
const lastReviewed = new Date(pair.lastReviewedAt).getTime();
|
|
105
|
+
if (!Number.isFinite(lastReviewed)) return false;
|
|
106
|
+
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1e3;
|
|
107
|
+
return Date.now() < lastReviewed + cooldownMs;
|
|
108
|
+
}
|
|
109
|
+
function resolvePair(memoryDir, pairId, verb) {
|
|
110
|
+
const existing = readPair(memoryDir, pairId);
|
|
111
|
+
if (!existing) return null;
|
|
112
|
+
const updated = {
|
|
113
|
+
...existing,
|
|
114
|
+
lastReviewedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115
|
+
resolution: verb
|
|
116
|
+
};
|
|
117
|
+
const filePath = pairPath(memoryDir, pairId);
|
|
118
|
+
const tmpPath = `${filePath}.tmp`;
|
|
119
|
+
fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2), "utf-8");
|
|
120
|
+
fs.renameSync(tmpPath, filePath);
|
|
121
|
+
return updated;
|
|
122
|
+
}
|
|
123
|
+
function memoryHashesChanged(_memoryDir, _pair, _getCurrentHash) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export {
|
|
128
|
+
computePairId,
|
|
129
|
+
writePair,
|
|
130
|
+
writePairs,
|
|
131
|
+
readPair,
|
|
132
|
+
listPairs,
|
|
133
|
+
isCoolingDown,
|
|
134
|
+
resolvePair,
|
|
135
|
+
memoryHashesChanged
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=chunk-YNB73F22.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/contradiction/contradiction-review.ts"],"sourcesContent":["/**\n * Contradiction Review Queue — storage for detected contradiction pairs (issue #520).\n *\n * Stores candidate pairs as JSON files under `memoryDir/.review/contradictions/`.\n * Pair IDs are deterministic (sha256 of sorted memory IDs) so reruns are idempotent.\n *\n * Lifecycle:\n * - `contradicts` → awaiting user review\n * - `duplicates` → auto-flagged for dedup (still needs user approval)\n * - `independent` / `both-valid` → dormant with cooldown\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { log } from \"../logger.js\";\nimport type { ContradictionVerdict } from \"./contradiction-judge.js\";\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\nexport type ResolutionVerb = \"keep-a\" | \"keep-b\" | \"merge\" | \"both-valid\" | \"needs-more-context\";\n\nexport interface ContradictionPair {\n /** Deterministic pair ID: sha256(sorted(memoryIdA, memoryIdB)). */\n pairId: string;\n /** Memory IDs (sorted). */\n memoryIds: [string, string];\n /** Judge verdict. */\n verdict: ContradictionVerdict;\n /** Judge rationale. */\n rationale: string;\n /** Judge confidence in [0, 1]. */\n confidence: number;\n /** ISO timestamp when detected. */\n detectedAt: string;\n /** ISO timestamp when last reviewed by user. */\n lastReviewedAt?: string;\n /** Resolution verb applied by user. */\n resolution?: ResolutionVerb;\n /** Namespace scope. */\n namespace?: string;\n}\n\nexport interface ContradictionListResult {\n pairs: ContradictionPair[];\n total: number;\n durationMs: number;\n}\n\nexport type ContradictionFilter = ContradictionVerdict | \"all\" | \"unresolved\";\n\n// ── Helpers ────────────────────────────────────────────────────────────────────\n\nexport function computePairId(memoryIdA: string, memoryIdB: string): string {\n const sorted = [memoryIdA, memoryIdB].sort();\n return createHash(\"sha256\").update(sorted.join(\"::\")).digest(\"hex\").slice(0, 24);\n}\n\nfunction reviewDir(memoryDir: string): string {\n return path.join(memoryDir, \".review\", \"contradictions\");\n}\n\nfunction pairPath(memoryDir: string, pairId: string): string {\n if (pairId.includes(\"/\") || pairId.includes(\"\\\\\") || pairId.includes(\"..\")) {\n throw new Error(`Invalid pairId: ${pairId}`);\n }\n return path.join(reviewDir(memoryDir), `${pairId}.json`);\n}\n\nfunction ensureDir(memoryDir: string): void {\n const dir = reviewDir(memoryDir);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\n// ── Write ──────────────────────────────────────────────────────────────────────\n\n/**\n * Write a contradiction pair to the review queue.\n * Idempotent: if the pair already exists with a higher or equal confidence,\n * the existing entry is preserved.\n */\nexport function writePair(memoryDir: string, pair: Omit<ContradictionPair, \"pairId\"> & { memoryIds: [string, string] }): ContradictionPair {\n ensureDir(memoryDir);\n const pairId = computePairId(pair.memoryIds[0], pair.memoryIds[1]);\n const existing = readPair(memoryDir, pairId);\n\n // Preserve user resolution if already reviewed\n if (existing?.resolution) {\n return existing;\n }\n\n // Preserve cooldown: don't overwrite a cooled-down entry with lower confidence\n if (existing && existing.confidence >= pair.confidence) {\n return existing;\n }\n\n const full: ContradictionPair = {\n ...pair,\n pairId,\n lastReviewedAt: existing?.lastReviewedAt,\n resolution: existing?.resolution,\n };\n\n const filePath = pairPath(memoryDir, pairId);\n const tmpPath = `${filePath}.tmp`;\n\n // Atomic write: temp then rename (rule 54)\n fs.writeFileSync(tmpPath, JSON.stringify(full, null, 2), \"utf-8\");\n fs.renameSync(tmpPath, filePath);\n\n return full;\n}\n\n/**\n * Write multiple pairs, deduplicating inputs first (rule 49).\n */\nexport function writePairs(memoryDir: string, pairs: Array<Omit<ContradictionPair, \"pairId\"> & { memoryIds: [string, string] }>): ContradictionPair[] {\n const seen = new Set<string>();\n const results: ContradictionPair[] = [];\n\n for (const pair of pairs) {\n const key = computePairId(pair.memoryIds[0], pair.memoryIds[1]);\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(writePair(memoryDir, pair));\n }\n\n return results;\n}\n\n// ── Read ───────────────────────────────────────────────────────────────────────\n\n/**\n * Read a single pair by ID. Returns null if not found.\n */\nexport function readPair(memoryDir: string, pairId: string): ContradictionPair | null {\n const filePath = pairPath(memoryDir, pairId);\n try {\n const raw = fs.readFileSync(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (typeof parsed === \"object\" && parsed !== null && Array.isArray(parsed.memoryIds)) {\n return parsed as ContradictionPair;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * List pairs in the review queue, optionally filtered by verdict.\n */\nexport function listPairs(\n memoryDir: string,\n options?: {\n filter?: ContradictionFilter;\n namespace?: string;\n limit?: number;\n },\n): ContradictionListResult {\n const startTime = Date.now();\n const dir = reviewDir(memoryDir);\n const { filter = \"all\", namespace, limit = 50 } = options ?? {};\n const pairs: ContradictionPair[] = [];\n let total = 0;\n\n if (!fs.existsSync(dir)) {\n return { pairs: [], total: 0, durationMs: Date.now() - startTime };\n }\n\n for (const entry of fs.readdirSync(dir)) {\n if (!entry.endsWith(\".json\")) continue;\n\n try {\n const raw = fs.readFileSync(path.join(dir, entry), \"utf-8\");\n const pair = JSON.parse(raw) as ContradictionPair;\n\n if (typeof pair !== \"object\" || pair === null) continue;\n if (!Array.isArray(pair.memoryIds)) continue;\n\n // Namespace filter\n if (namespace && pair.namespace !== namespace) continue;\n\n // Verdict filter\n if (filter === \"unresolved\") {\n if (pair.resolution) continue;\n if (pair.verdict === \"independent\") continue;\n } else if (filter !== \"all\" && pair.verdict !== filter) {\n continue;\n }\n\n total++;\n if (pairs.length < limit) pairs.push(pair);\n } catch {\n continue;\n }\n }\n\n return { pairs, total, durationMs: Date.now() - startTime };\n}\n\n// ── Cooldown ───────────────────────────────────────────────────────────────────\n\n/**\n * Check if a pair is within its cooldown window.\n * Returns true if the pair should be SKIPPED (still cooling down).\n */\nexport function isCoolingDown(pair: ContradictionPair, cooldownDays: number): boolean {\n if (cooldownDays <= 0) return false; // rule 27: guard against 0\n if (!pair.lastReviewedAt) return false;\n\n const lastReviewed = new Date(pair.lastReviewedAt).getTime();\n if (!Number.isFinite(lastReviewed)) return false;\n\n const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;\n return Date.now() < lastReviewed + cooldownMs;\n}\n\n/**\n * Mark a pair as reviewed (sets lastReviewedAt and resolution).\n */\nexport function resolvePair(\n memoryDir: string,\n pairId: string,\n verb: ResolutionVerb,\n): ContradictionPair | null {\n const existing = readPair(memoryDir, pairId);\n if (!existing) return null;\n\n const updated: ContradictionPair = {\n ...existing,\n lastReviewedAt: new Date().toISOString(),\n resolution: verb,\n };\n\n const filePath = pairPath(memoryDir, pairId);\n const tmpPath = `${filePath}.tmp`;\n\n fs.writeFileSync(tmpPath, JSON.stringify(updated, null, 2), \"utf-8\");\n fs.renameSync(tmpPath, filePath);\n\n return updated;\n}\n\n/**\n * Check whether a pair's referenced memories have changed since detection,\n * which should override cooldown.\n */\nexport function memoryHashesChanged(\n _memoryDir: string,\n _pair: ContradictionPair,\n _getCurrentHash: (memoryId: string) => string | null,\n): boolean {\n // Intentionally a stub for now — the full implementation would compare\n // content hashes stored at detection time with current hashes.\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QmdClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BLKTA7MM.js";
|
|
4
4
|
import {
|
|
5
5
|
launchProcess
|
|
6
6
|
} from "./chunk-LK6SGL53.js";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./chunk-QNJMBKFK.js";
|
|
10
10
|
import {
|
|
11
11
|
StorageManager
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GV6NLQ4X.js";
|
|
13
13
|
import {
|
|
14
14
|
log
|
|
15
15
|
} from "./chunk-2ODBA7MQ.js";
|
|
@@ -104,8 +104,13 @@ async function scanDir(dir) {
|
|
|
104
104
|
async function scanMemoryDir(memoryDir) {
|
|
105
105
|
const factsDir = path.join(memoryDir, "facts");
|
|
106
106
|
const correctionsDir = path.join(memoryDir, "corrections");
|
|
107
|
-
const
|
|
108
|
-
|
|
107
|
+
const proceduresDir = path.join(memoryDir, "procedures");
|
|
108
|
+
const [facts, corrections, procedures] = await Promise.all([
|
|
109
|
+
scanDir(factsDir),
|
|
110
|
+
scanDir(correctionsDir),
|
|
111
|
+
scanDir(proceduresDir)
|
|
112
|
+
]);
|
|
113
|
+
return [...facts, ...corrections, ...procedures];
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
// src/search/lancedb-backend.ts
|
|
@@ -1897,4 +1902,4 @@ export {
|
|
|
1897
1902
|
namespaceCollectionName,
|
|
1898
1903
|
NamespaceSearchRouter
|
|
1899
1904
|
};
|
|
1900
|
-
//# sourceMappingURL=chunk-
|
|
1905
|
+
//# sourceMappingURL=chunk-ZVBB3T7V.js.map
|