@remnic/plugin-openclaw 1.0.6 → 1.0.7

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/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SharedContextManager,
4
4
  defaultTierMigrationCycleBudget,
5
5
  external_exports
6
- } from "./chunk-GUKYM4XZ.js";
6
+ } from "./chunk-SVGN3ACY.js";
7
7
  import {
8
8
  filterTrajectoriesByLookbackDays,
9
9
  getCausalTrajectoryStoreStatus,
@@ -85,12 +85,14 @@ import {
85
85
  import {
86
86
  FallbackLlmClient,
87
87
  buildChatCompletionTokenLimit,
88
- extractJsonCandidates,
89
88
  mergeEnv,
90
89
  readEnvVar,
91
90
  resolveHomeDir,
92
91
  shouldAssumeOpenAiChatCompletions
93
- } from "./chunk-3SA5F4WT.js";
92
+ } from "./chunk-NXLHSCLU.js";
93
+ import {
94
+ extractJsonCandidates
95
+ } from "./chunk-3A5ELHTT.js";
94
96
  import {
95
97
  listJsonFiles,
96
98
  listNamedFiles,
@@ -306,6 +308,31 @@ function normalizeMemoryRelativeDir(raw, fallback) {
306
308
  const normalized = trimmed.replace(/\\/g, "/").split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..").join("/");
307
309
  return normalized.length > 0 ? normalized : fallback;
308
310
  }
311
+ function parseContradictionScanConfig(raw) {
312
+ if (!raw || typeof raw !== "object") {
313
+ return {
314
+ enabled: false,
315
+ similarityFloor: 0.82,
316
+ topicOverlapFloor: 0.4,
317
+ maxPairsPerRun: 500,
318
+ cooldownDays: 14,
319
+ autoMergeDuplicates: false
320
+ };
321
+ }
322
+ const src = raw;
323
+ const simFloor = coerceNumber(src.similarityFloor) ?? 0.82;
324
+ const topicFloor = coerceNumber(src.topicOverlapFloor) ?? 0.4;
325
+ const maxPairs = coerceNumber(src.maxPairsPerRun) ?? 500;
326
+ const cooldown = coerceNumber(src.cooldownDays) ?? 14;
327
+ return {
328
+ enabled: coerceBool(src.enabled) === true,
329
+ similarityFloor: Math.min(1, Math.max(0, simFloor)),
330
+ topicOverlapFloor: Math.min(1, Math.max(0, topicFloor)),
331
+ maxPairsPerRun: Math.max(1, maxPairs),
332
+ cooldownDays: Math.max(0, cooldown),
333
+ autoMergeDuplicates: coerceBool(src.autoMergeDuplicates) === true
334
+ };
335
+ }
309
336
  function parseSemanticChunkingConfig(raw) {
310
337
  if (!raw || typeof raw !== "object") return {};
311
338
  const src = raw;
@@ -510,17 +537,26 @@ function parseConfig(raw) {
510
537
  fingerprintDedup: rawCodexCompat.fingerprintDedup !== false
511
538
  };
512
539
  const rawProcedural = cfg.procedural && typeof cfg.procedural === "object" && !Array.isArray(cfg.procedural) ? cfg.procedural : {};
513
- const proceduralMinRaw = typeof rawProcedural.minOccurrences === "number" && Number.isFinite(rawProcedural.minOccurrences) ? Math.floor(rawProcedural.minOccurrences) : 3;
540
+ const proceduralMinCoerced = coerceNumber(rawProcedural.minOccurrences);
541
+ const proceduralMinRaw = proceduralMinCoerced !== void 0 ? Math.floor(proceduralMinCoerced) : 3;
542
+ const successFloorRaw = coerceNumber(rawProcedural.successFloor);
543
+ const successFloor = successFloorRaw !== void 0 && successFloorRaw >= 0 && successFloorRaw <= 1 ? successFloorRaw : 0.7;
544
+ const autoPromoteOccRaw = coerceNumber(rawProcedural.autoPromoteOccurrences);
545
+ const autoPromoteOccurrences = autoPromoteOccRaw !== void 0 && Number.isFinite(autoPromoteOccRaw) ? autoPromoteOccRaw <= 0 ? 0 : Math.min(1e4, Math.max(1, Math.floor(autoPromoteOccRaw))) : 8;
546
+ const lookbackCoerced = coerceNumber(rawProcedural.lookbackDays);
547
+ const lookbackDays = lookbackCoerced !== void 0 && Number.isFinite(lookbackCoerced) ? Math.min(3650, Math.max(1, Math.floor(lookbackCoerced))) : 30;
548
+ const recallMaxCoerced = coerceNumber(rawProcedural.recallMaxProcedures);
549
+ const recallMaxProcedures = recallMaxCoerced !== void 0 && Number.isFinite(recallMaxCoerced) ? Math.min(10, Math.max(1, Math.floor(recallMaxCoerced))) : 3;
514
550
  const procedural = {
515
551
  enabled: coerceBool(rawProcedural.enabled) === true,
516
- /** 0 disables miner emission threshold (no clusters qualify). */
552
+ /** `0` skips all mining (`minOccurrences_zero`); otherwise clusters need at least this many members. */
517
553
  minOccurrences: Math.min(1e3, Math.max(0, proceduralMinRaw)),
518
- successFloor: typeof rawProcedural.successFloor === "number" && Number.isFinite(rawProcedural.successFloor) && rawProcedural.successFloor >= 0 && rawProcedural.successFloor <= 1 ? rawProcedural.successFloor : 0.7,
519
- autoPromoteOccurrences: typeof rawProcedural.autoPromoteOccurrences === "number" && Number.isFinite(rawProcedural.autoPromoteOccurrences) ? rawProcedural.autoPromoteOccurrences <= 0 ? 0 : Math.min(1e4, Math.max(1, Math.floor(rawProcedural.autoPromoteOccurrences))) : 8,
554
+ successFloor,
555
+ autoPromoteOccurrences,
520
556
  autoPromoteEnabled: coerceBool(rawProcedural.autoPromoteEnabled) === true,
521
- lookbackDays: typeof rawProcedural.lookbackDays === "number" && Number.isFinite(rawProcedural.lookbackDays) ? Math.min(3650, Math.max(1, Math.floor(rawProcedural.lookbackDays))) : 30,
557
+ lookbackDays,
522
558
  proceduralMiningCronAutoRegister: coerceBool(rawProcedural.proceduralMiningCronAutoRegister) === true,
523
- recallMaxProcedures: typeof rawProcedural.recallMaxProcedures === "number" && Number.isFinite(rawProcedural.recallMaxProcedures) ? Math.min(10, Math.max(1, Math.floor(rawProcedural.recallMaxProcedures))) : 3
559
+ recallMaxProcedures
524
560
  };
525
561
  const memoryDir = typeof cfg.memoryDir === "string" && cfg.memoryDir.length > 0 ? cfg.memoryDir : DEFAULT_MEMORY_DIR;
526
562
  const rawIdentityInjectionMode = cfg.identityInjectionMode;
@@ -704,13 +740,19 @@ function parseConfig(raw) {
704
740
  contradictionSimilarityThreshold: typeof cfg.contradictionSimilarityThreshold === "number" ? cfg.contradictionSimilarityThreshold : 0.7,
705
741
  contradictionMinConfidence: typeof cfg.contradictionMinConfidence === "number" ? cfg.contradictionMinConfidence : 0.9,
706
742
  contradictionAutoResolve: cfg.contradictionAutoResolve !== false,
743
+ // Contradiction Scan cron (issue #520)
744
+ contradictionScan: parseContradictionScanConfig(cfg.contradictionScan),
707
745
  // Temporal Supersession (issue #375)
708
746
  temporalSupersessionEnabled: cfg.temporalSupersessionEnabled !== false,
709
747
  // On by default
710
748
  temporalSupersessionIncludeInRecall: cfg.temporalSupersessionIncludeInRecall === true,
711
749
  // Off by default
712
- // Direct-answer retrieval tier (issue #518)
713
- recallDirectAnswerEnabled: coerceBool(cfg.recallDirectAnswerEnabled) ?? false,
750
+ // Direct-answer retrieval tier (issue #518). Default on — the
751
+ // tier runs in observation mode: it annotates
752
+ // LastRecallSnapshot.tierExplain but never short-circuits the
753
+ // QMD path. Operators can opt out with
754
+ // recallDirectAnswerEnabled=false.
755
+ recallDirectAnswerEnabled: coerceBool(cfg.recallDirectAnswerEnabled) ?? true,
714
756
  recallDirectAnswerTokenOverlapFloor: (() => {
715
757
  const n = coerceNumber(cfg.recallDirectAnswerTokenOverlapFloor);
716
758
  return n !== void 0 && n >= 0 && n <= 1 ? n : 0.55;
@@ -972,6 +1014,22 @@ function parseConfig(raw) {
972
1014
  localLlmFastModel: typeof cfg.localLlmFastModel === "string" && cfg.localLlmFastModel.length > 0 ? cfg.localLlmFastModel : "",
973
1015
  localLlmFastUrl: typeof cfg.localLlmFastUrl === "string" && cfg.localLlmFastUrl.length > 0 ? cfg.localLlmFastUrl : typeof cfg.localLlmUrl === "string" && cfg.localLlmUrl.length > 0 ? cfg.localLlmUrl : "http://localhost:1234/v1",
974
1016
  localLlmFastTimeoutMs: typeof cfg.localLlmFastTimeoutMs === "number" ? cfg.localLlmFastTimeoutMs : 15e3,
1017
+ // Thinking-mode suppression on the main local LLM (issue #548).
1018
+ // Default true — extraction / consolidation produce structured
1019
+ // JSON and gain nothing from chain-of-thought; thinking-capable
1020
+ // models burn their token budget on reasoning and blow the
1021
+ // default 60s timeout. Operators who need thinking on the main
1022
+ // client (e.g. for narrative tasks) can set this to false via
1023
+ // config or --config CLI flag. The fast-tier `fastLlm` always
1024
+ // disables thinking and is unaffected by this flag.
1025
+ //
1026
+ // Injection is backend-gated inside LocalLlmClient: the
1027
+ // `chat_template_kwargs` field is only sent when the detected
1028
+ // backend is in `THINKING_COMPATIBLE_BACKENDS` (LM Studio, vLLM).
1029
+ // Strict OpenAI-compatible backends reject unknown request
1030
+ // fields with 400, so the client fails open on unknown backends
1031
+ // rather than tripping the 400 cooldown (Codex P1 on PR #550).
1032
+ localLlmDisableThinking: coerceBool(cfg.localLlmDisableThinking) ?? true,
975
1033
  // Gateway config (passed from index.ts for fallback AI)
976
1034
  gatewayConfig: cfg.gatewayConfig,
977
1035
  // Gateway model source (v9.2) — route LLM calls through gateway agent model chain
@@ -2844,6 +2902,10 @@ function trimTrailingSlashes(s) {
2844
2902
  while (end > 0 && s[end - 1] === "/") end--;
2845
2903
  return s.substring(0, end);
2846
2904
  }
2905
+ var THINKING_COMPATIBLE_BACKENDS = /* @__PURE__ */ new Set([
2906
+ "lmstudio",
2907
+ "vllm"
2908
+ ]);
2847
2909
  var LOCAL_SERVERS = [
2848
2910
  {
2849
2911
  type: "ollama",
@@ -2902,9 +2964,16 @@ var LocalLlmClient = class _LocalLlmClient {
2902
2964
  this.modelRegistry = modelRegistry;
2903
2965
  }
2904
2966
  /**
2905
- * Disable thinking/reasoning mode for models that support it (e.g. Qwen 3.5).
2906
- * When enabled, adds chat_template_kwargs to suppress chain-of-thought,
2907
- * reducing latency for fast-tier operations.
2967
+ * Request thinking/reasoning suppression on the next chat completion.
2968
+ *
2969
+ * When `true`, the client will inject
2970
+ * `chat_template_kwargs: { enable_thinking: false }` into the request
2971
+ * body — **but only when the detected backend is known to support it**
2972
+ * (LM Studio, vLLM; see `THINKING_COMPATIBLE_BACKENDS`). Strict
2973
+ * OpenAI-compat backends reject unknown fields with 400; on those the
2974
+ * client fails open (thinking runs normally). This is the safe
2975
+ * default for Remnic extraction / consolidation: measurable latency
2976
+ * win on thinking-capable backends, zero risk on others. Issue #548.
2908
2977
  */
2909
2978
  set disableThinking(value) {
2910
2979
  this._disableThinking = value;
@@ -3402,7 +3471,7 @@ var LocalLlmClient = class _LocalLlmClient {
3402
3471
  if (options.responseFormat?.type === "json_schema") {
3403
3472
  requestBody.response_format = options.responseFormat;
3404
3473
  }
3405
- if (this._disableThinking) {
3474
+ if (this._disableThinking && this.detectedType !== null && THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)) {
3406
3475
  requestBody.chat_template_kwargs = { enable_thinking: false };
3407
3476
  }
3408
3477
  const baseUrl = trimTrailingSlashes(
@@ -5395,6 +5464,8 @@ Memory categories \u2014 use the MOST SPECIFIC category that fits:
5395
5464
  - commitment: Promises, obligations, deadlines
5396
5465
  - moment: Emotionally significant events
5397
5466
  - skill: Demonstrated capabilities
5467
+ - rule: Explicit operational rules or constraints
5468
+ - procedure: Repeatable workflows \u2014 use when the user describes a multi-step play (\u22652 ordered steps). Put the human-readable trigger/context in "content" (e.g. "When you deploy\u2026") and list steps in "procedureSteps" as [{"order":1,"intent":"\u2026"}, \u2026] mirroring the gateway extraction schema.
5398
5469
 
5399
5470
  IMPORTANT: Do NOT label everything as "fact". Use "decision" for architectural choices, "commitment" for deadlines/promises, "principle" for reusable rules, "correction" for when the user rejects a suggestion, etc.
5400
5471
 
@@ -5456,7 +5527,7 @@ Also generate:
5456
5527
 
5457
5528
  Output JSON:
5458
5529
  {
5459
- "facts": [{"category": "decision", "content": "Chose PostgreSQL over MongoDB for the user service", "importance": 8, "confidence": 0.9, "structuredAttributes": {"chosen": "PostgreSQL", "rejected": "MongoDB"}}, {"category": "commitment", "content": "Must ship v2.0 API by end of March", "importance": 10, "confidence": 1.0, "structuredAttributes": {"deadline": "end of March", "deliverable": "v2.0 API"}}, {"category": "fact", "content": "The store backend uses Redis for session caching", "importance": 6, "confidence": 0.95, "entityRef": "project-acme-store"}, {"category": "principle", "content": "Always run migrations in a transaction to avoid partial schema updates", "importance": 8, "confidence": 0.9}],
5530
+ "facts": [{"category": "decision", "content": "Chose PostgreSQL over MongoDB for the user service", "importance": 8, "confidence": 0.9, "structuredAttributes": {"chosen": "PostgreSQL", "rejected": "MongoDB"}}, {"category": "procedure", "content": "When you cut a hotfix release, follow the checklist", "importance": 8, "confidence": 0.9, "procedureSteps": [{"order": 1, "intent": "Branch from main and cherry-pick the fix"}, {"order": 2, "intent": "Run CI and tag the release"}]}, {"category": "commitment", "content": "Must ship v2.0 API by end of March", "importance": 10, "confidence": 1.0, "structuredAttributes": {"deadline": "end of March", "deliverable": "v2.0 API"}}, {"category": "fact", "content": "The store backend uses Redis for session caching", "importance": 6, "confidence": 0.95, "entityRef": "project-acme-store"}, {"category": "principle", "content": "Always run migrations in a transaction to avoid partial schema updates", "importance": 8, "confidence": 0.9}],
5460
5531
  "entities": [{"name": "person-jane-doe", "type": "person", "facts": ["Works at Acme Corp", "Prefers Python over JavaScript"], "structuredSections": [{"key": "beliefs", "title": "Beliefs", "facts": ["Python is a better fit than JavaScript for backend work."]}]}, {"name": "project-acme-store", "type": "project", "facts": ["Built with Next.js", "Deployed on Vercel"]}],
5461
5532
  "profileUpdates": ["User prefers dark mode in all editors"],
5462
5533
  "questions": [{"question": "Which cloud provider hosts the staging environment?", "context": "Came up during deployment discussion", "priority": 0.5}],
@@ -7367,7 +7438,7 @@ var ENTITY_PATTERNS = [
7367
7438
  { re: /\b(model|llm|qmd|embedding|retrieval|memory)\b/i, entityType: "ai" },
7368
7439
  { re: /\b(doc|readme|docs|changelog)\b/i, entityType: "docs" }
7369
7440
  ];
7370
- var TASK_INITIATION_RE = /\b(ship(?:ping|ped)?|deploy(?:ing|ed)?|release|publish|open(?:ing)?\s+(?:a\s+)?(?:pr|pull\s+request)|merge(?:ing)?\s+(?:the\s+)?(?:pr|pull\s+request)|run\s+(?:the\s+)?tests?|start(?:ing)?\s+(?:work|on|the)|kick\s+off|implement(?:ing|ed)?|let's\s+|going\s+to\s+(?:ship|deploy|release|open|run|merge)|need\s+to\s+(?:ship|deploy|run|open|merge|test)|fix(?:ing|ed)?\s+(?:the\s+)?(?:bug|build)|patch(?:ing|ed)?|build(?:ing)?\s+(?:and\s+)?(?:ship|deploy))\b/i;
7441
+ var TASK_INITIATION_RE = /\b(ship(?:ping|ped)?|deploy(?:ing|ed)?|release|publish|open(?:ing)?\s+(?:a\s+)?(?:pr|pull\s+request)|merge(?:ing)?\s+(?:the\s+)?(?:pr|pull\s+request)|run\s+(?:the\s+)?tests?|start(?:ing)?\s+(?:work|on|the)|kick\s+off|implement(?:ing|ed)?|let's\s+(?:ship|deploy|release|publish|open|run|merge|implement|fix|patch|build|start|do|get|put|wire|hook|land|roll)\b|going\s+to\s+(?:ship|deploy|release|open|run|merge)|need\s+to\s+(?:ship|deploy|run|open|merge|test)|fix(?:ing|ed)?\s+(?:(?:the|a)\s+)?(?:\w+\s+){0,4}(?:bug|build)\b|patch(?:ing|ed)?|build(?:ing)?\s+(?:and\s+)?(?:ship|deploy))\b/i;
7371
7442
  function normalizeTextInput(input) {
7372
7443
  return typeof input === "string" ? input : "";
7373
7444
  }
@@ -7698,8 +7769,13 @@ async function scanDir(dir) {
7698
7769
  async function scanMemoryDir(memoryDir) {
7699
7770
  const factsDir = path4.join(memoryDir, "facts");
7700
7771
  const correctionsDir = path4.join(memoryDir, "corrections");
7701
- const [facts, corrections] = await Promise.all([scanDir(factsDir), scanDir(correctionsDir)]);
7702
- return [...facts, ...corrections];
7772
+ const proceduresDir = path4.join(memoryDir, "procedures");
7773
+ const [facts, corrections, procedures] = await Promise.all([
7774
+ scanDir(factsDir),
7775
+ scanDir(correctionsDir),
7776
+ scanDir(proceduresDir)
7777
+ ]);
7778
+ return [...facts, ...corrections, ...procedures];
7703
7779
  }
7704
7780
 
7705
7781
  // ../remnic-core/src/search/lancedb-backend.ts
@@ -8534,6 +8610,23 @@ var EmbedHelper = class {
8534
8610
  import { createHash as createHash3 } from "crypto";
8535
8611
  import os2 from "os";
8536
8612
  import path6 from "path";
8613
+
8614
+ // ../remnic-core/src/abort-error.ts
8615
+ function abortError(message) {
8616
+ const err = new Error(message);
8617
+ Object.defineProperty(err, "name", { value: "AbortError" });
8618
+ return err;
8619
+ }
8620
+ function isAbortError(err) {
8621
+ return err instanceof Error && err.name === "AbortError";
8622
+ }
8623
+ function throwIfAborted(signal, message = "operation aborted") {
8624
+ if (signal?.aborted) {
8625
+ throw abortError(message);
8626
+ }
8627
+ }
8628
+
8629
+ // ../remnic-core/src/qmd.ts
8537
8630
  var QMD_TIMEOUT_MS = 3e4;
8538
8631
  var QMD_DAEMON_TIMEOUT_MS = 8e3;
8539
8632
  var QMD_PROBE_TIMEOUT_MS = 8e3;
@@ -8564,14 +8657,6 @@ function getGlobalQmdState() {
8564
8657
  }
8565
8658
  return g[QMD_GLOBAL_STATE_KEY];
8566
8659
  }
8567
- function abortError(message) {
8568
- const err = new Error(message);
8569
- Object.defineProperty(err, "name", { value: "AbortError" });
8570
- return err;
8571
- }
8572
- function isAbortError(err) {
8573
- return err instanceof Error && err.name === "AbortError";
8574
- }
8575
8660
  function errorMessage(err) {
8576
8661
  if (typeof err === "string") return err;
8577
8662
  if (err instanceof Error) return err.message;
@@ -8592,11 +8677,6 @@ function isCallerCancellation(err, signal) {
8592
8677
  function isDaemonTimeoutError(err) {
8593
8678
  return /timed out/i.test(errorMessage(err));
8594
8679
  }
8595
- function throwIfAborted(signal, message = "operation aborted") {
8596
- if (signal?.aborted) {
8597
- throw abortError(message);
8598
- }
8599
- }
8600
8680
  function sleepWithSignal(ms, signal) {
8601
8681
  return new Promise((resolve, reject) => {
8602
8682
  throwIfAborted(signal);
@@ -12983,6 +13063,7 @@ import path15 from "path";
12983
13063
  var DAY_SUMMARY_CRON_ID = "engram-day-summary";
12984
13064
  var GOVERNANCE_CRON_ID = "engram-nightly-governance";
12985
13065
  var PROCEDURAL_MINING_CRON_ID = "engram-procedural-mining";
13066
+ var CONTRADICTION_SCAN_CRON_ID = "engram-contradiction-scan";
12986
13067
  async function acquireCronJobsLock(jobsPath) {
12987
13068
  const lockPath2 = `${jobsPath}.lock`;
12988
13069
  const start = Date.now();
@@ -13119,6 +13200,30 @@ async function ensureProceduralMiningCron(jobsPath, options) {
13119
13200
  delivery: { mode: "none" }
13120
13201
  }));
13121
13202
  }
13203
+ async function ensureContradictionScanCron(jobsPath, options) {
13204
+ const scheduleExpr = typeof options.scheduleExpr === "string" && options.scheduleExpr.trim().length > 0 ? options.scheduleExpr.trim() : "37 3 * * *";
13205
+ const agentId = typeof options.agentId === "string" && options.agentId.trim().length > 0 ? options.agentId.trim() : "main";
13206
+ return ensureCronJob(jobsPath, CONTRADICTION_SCAN_CRON_ID, () => ({
13207
+ id: CONTRADICTION_SCAN_CRON_ID,
13208
+ agentId,
13209
+ name: "Remnic Contradiction Scan (nightly)",
13210
+ enabled: true,
13211
+ schedule: {
13212
+ kind: "cron",
13213
+ expr: scheduleExpr,
13214
+ tz: options.timezone
13215
+ },
13216
+ sessionTarget: "isolated",
13217
+ wakeMode: "now",
13218
+ payload: {
13219
+ kind: "agentTurn",
13220
+ timeoutSeconds: 900,
13221
+ thinking: "off",
13222
+ message: "You are OpenClaw automation. Call tool `engram.contradiction_scan_run` with empty params. If successful output exactly NO_REPLY. On error output one concise line. Do NOT use message tool."
13223
+ },
13224
+ delivery: { mode: "none" }
13225
+ }));
13226
+ }
13122
13227
 
13123
13228
  // ../remnic-core/src/lifecycle.ts
13124
13229
  var DEFAULT_POLICY = {
@@ -14882,6 +14987,14 @@ function clampGraphRecallExpandedEntries(entries, maxEntries = 64) {
14882
14987
  };
14883
14988
  }).filter((item) => item.path.length > 0 && item.namespace.length > 0).slice(0, limit);
14884
14989
  }
14990
+ function cloneTierExplain(tierExplain) {
14991
+ if (!tierExplain) return void 0;
14992
+ return structuredClone(tierExplain);
14993
+ }
14994
+ function cloneLastRecallSnapshot(snapshot) {
14995
+ if (!snapshot) return null;
14996
+ return structuredClone(snapshot);
14997
+ }
14885
14998
  var DEFAULT_TIER_MIGRATION_STATUS = {
14886
14999
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
14887
15000
  lastCycle: null,
@@ -14912,13 +15025,17 @@ var LastRecallStore = class {
14912
15025
  }
14913
15026
  }
14914
15027
  get(sessionKey) {
14915
- return this.state[sessionKey] ?? null;
15028
+ return cloneLastRecallSnapshot(this.state[sessionKey] ?? null);
14916
15029
  }
14917
15030
  getMostRecent() {
14918
15031
  const snapshots = Object.values(this.state);
14919
15032
  if (snapshots.length === 0) return null;
14920
- snapshots.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
14921
- return snapshots[0] ?? null;
15033
+ snapshots.sort((a, b) => {
15034
+ const byTime = b.recordedAt.localeCompare(a.recordedAt);
15035
+ if (byTime !== 0) return byTime;
15036
+ return a.sessionKey.localeCompare(b.sessionKey);
15037
+ });
15038
+ return cloneLastRecallSnapshot(snapshots[0] ?? null);
14922
15039
  }
14923
15040
  /**
14924
15041
  * Persist last-recall snapshot and append an impression log entry.
@@ -14927,7 +15044,7 @@ var LastRecallStore = class {
14927
15044
  async record(opts) {
14928
15045
  const now = (/* @__PURE__ */ new Date()).toISOString();
14929
15046
  const queryHash = createHash4("sha256").update(opts.query).digest("hex");
14930
- const snapshot = {
15047
+ const liveSnapshot = {
14931
15048
  sessionKey: opts.sessionKey,
14932
15049
  recordedAt: now,
14933
15050
  queryHash,
@@ -14939,15 +15056,17 @@ var LastRecallStore = class {
14939
15056
  requestedMode: opts.requestedMode,
14940
15057
  source: opts.source,
14941
15058
  fallbackUsed: opts.fallbackUsed,
14942
- sourcesUsed: opts.sourcesUsed ? [...opts.sourcesUsed] : void 0,
14943
- budgetsApplied: opts.budgetsApplied ? { ...opts.budgetsApplied } : void 0,
15059
+ sourcesUsed: opts.sourcesUsed,
15060
+ budgetsApplied: opts.budgetsApplied,
14944
15061
  latencyMs: opts.latencyMs,
14945
- resultPaths: opts.resultPaths ? [...opts.resultPaths] : void 0,
15062
+ resultPaths: opts.resultPaths,
14946
15063
  policyVersion: opts.policyVersion,
14947
15064
  identityInjectionMode: opts.identityInjection?.mode,
14948
15065
  identityInjectedChars: opts.identityInjection?.injectedChars,
14949
- identityInjectionTruncated: opts.identityInjection?.truncated
15066
+ identityInjectionTruncated: opts.identityInjection?.truncated,
15067
+ tierExplain: opts.tierExplain
14950
15068
  };
15069
+ const snapshot = cloneLastRecallSnapshot(liveSnapshot);
14951
15070
  this.state[opts.sessionKey] = snapshot;
14952
15071
  const keys = Object.keys(this.state);
14953
15072
  if (keys.length > 50) {
@@ -14971,6 +15090,31 @@ var LastRecallStore = class {
14971
15090
  }
14972
15091
  }
14973
15092
  }
15093
+ /**
15094
+ * Attach a RecallTierExplain block to the existing snapshot for a
15095
+ * session without rewriting the entire snapshot. Used by the
15096
+ * post-recall direct-answer annotation path (issue #518 slice 3c):
15097
+ * recallInternal records the snapshot first, then the orchestrator
15098
+ * fires the direct-answer tier in observation mode and annotates
15099
+ * the stored snapshot with whichever tier served the query.
15100
+ *
15101
+ * No-op when no snapshot exists for the given session; callers do
15102
+ * not need to guard on existence.
15103
+ */
15104
+ async annotateTierExplain(sessionKey, tierExplain) {
15105
+ const current = this.state[sessionKey];
15106
+ if (!current) return;
15107
+ this.state[sessionKey] = {
15108
+ ...current,
15109
+ tierExplain: cloneTierExplain(tierExplain)
15110
+ };
15111
+ try {
15112
+ await mkdir11(path20.dirname(this.statePath), { recursive: true });
15113
+ await writeFile12(this.statePath, JSON.stringify(this.state, null, 2), "utf-8");
15114
+ } catch (err) {
15115
+ log.debug(`last recall tier-explain annotate failed: ${err}`);
15116
+ }
15117
+ }
14974
15118
  };
14975
15119
  var TierMigrationStatusStore = class {
14976
15120
  statePath;
@@ -20555,13 +20699,13 @@ async function readCueAnchors(options) {
20555
20699
  return anchors;
20556
20700
  }
20557
20701
  async function searchHarmonicRetrieval(options) {
20558
- throwIfAborted2(options.abortSignal);
20702
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20559
20703
  const queryTokens = new Set(normalizeRecallTokens(options.query, ["what", "which"]));
20560
20704
  if (queryTokens.size === 0 || options.maxResults <= 0) return [];
20561
20705
  const nodes = await readAbstractionNodes(options);
20562
20706
  const candidates = /* @__PURE__ */ new Map();
20563
20707
  for (const node of nodes) {
20564
- throwIfAborted2(options.abortSignal);
20708
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20565
20709
  const { score, matchedFields } = scoreNode(node, queryTokens);
20566
20710
  if (score <= 0) continue;
20567
20711
  candidates.set(node.nodeId, {
@@ -20573,11 +20717,11 @@ async function searchHarmonicRetrieval(options) {
20573
20717
  });
20574
20718
  }
20575
20719
  if (options.anchorsEnabled) {
20576
- throwIfAborted2(options.abortSignal);
20720
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20577
20721
  const anchors = await readCueAnchors(options);
20578
20722
  const nodeIndex = new Map(nodes.map((node) => [node.nodeId, node]));
20579
20723
  for (const anchor of anchors) {
20580
- throwIfAborted2(options.abortSignal);
20724
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20581
20725
  const { score, matchedFields } = scoreAnchor(anchor, queryTokens);
20582
20726
  if (score <= 0) continue;
20583
20727
  for (const nodeRef of anchor.nodeRefs) {
@@ -20619,12 +20763,6 @@ async function searchHarmonicRetrieval(options) {
20619
20763
  (left, right) => right.score - left.score || right.anchorScore - left.anchorScore || right.node.recordedAt.localeCompare(left.node.recordedAt)
20620
20764
  ).slice(0, options.maxResults);
20621
20765
  }
20622
- function throwIfAborted2(signal) {
20623
- if (!signal?.aborted) return;
20624
- const err = new Error("harmonic retrieval aborted");
20625
- Object.defineProperty(err, "name", { value: "AbortError" });
20626
- throw err;
20627
- }
20628
20766
 
20629
20767
  // ../remnic-core/src/verified-recall.ts
20630
20768
  function createReadOnlyBoxBuilder(memoryDir) {
@@ -27211,15 +27349,9 @@ function fingerprintEntitySynthesisEvidence(entity) {
27211
27349
  fingerprint.update(fingerprintEntityStructuredFacts(entity) ?? "");
27212
27350
  return fingerprint.digest("hex");
27213
27351
  }
27214
- function abortRecallError(message) {
27215
- const err = new Error(message);
27216
- Object.defineProperty(err, "name", { value: "AbortError" });
27217
- return err;
27218
- }
27352
+ var abortRecallError = abortError;
27219
27353
  function throwIfRecallAborted(signal, message = "recall aborted") {
27220
- if (signal?.aborted) {
27221
- throw abortRecallError(message);
27222
- }
27354
+ throwIfAborted(signal, message);
27223
27355
  }
27224
27356
  async function raceRecallAbort(promise, signal, message = "recall aborted") {
27225
27357
  throwIfRecallAborted(signal, message);
@@ -27853,6 +27985,7 @@ var Orchestrator = class _Orchestrator {
27853
27985
  );
27854
27986
  this.judgeVerdictCache = createVerdictCache();
27855
27987
  this.localLlm = new LocalLlmClient(config, this.modelRegistry);
27988
+ this.localLlm.disableThinking = config.localLlmDisableThinking;
27856
27989
  this.fastLlm = config.localLlmFastEnabled ? (() => {
27857
27990
  const client = new LocalLlmClient(
27858
27991
  {
@@ -28353,6 +28486,13 @@ var Orchestrator = class _Orchestrator {
28353
28486
  log.debug(`procedural mining cron auto-register failed (non-fatal): ${err}`);
28354
28487
  }
28355
28488
  }
28489
+ if (this.config.contradictionScan?.enabled) {
28490
+ try {
28491
+ await this.autoRegisterContradictionScanCron();
28492
+ } catch (err) {
28493
+ log.debug(`contradiction scan cron auto-register failed (non-fatal): ${err}`);
28494
+ }
28495
+ }
28356
28496
  log.info("orchestrator initialized (full \u2014 deferred steps complete)");
28357
28497
  }
28358
28498
  /**
@@ -28520,6 +28660,26 @@ var Orchestrator = class _Orchestrator {
28520
28660
  log.debug(`procedural mining cron auto-register error: ${err}`);
28521
28661
  }
28522
28662
  }
28663
+ async autoRegisterContradictionScanCron() {
28664
+ const home = resolveHomeDir();
28665
+ const jobsPath = path43.join(home, ".openclaw", "cron", "jobs.json");
28666
+ try {
28667
+ if (!existsSync8(jobsPath)) {
28668
+ log.debug("contradiction scan cron: jobs.json not found, skipping auto-register");
28669
+ return;
28670
+ }
28671
+ const created = await ensureContradictionScanCron(jobsPath, {
28672
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
28673
+ });
28674
+ if (created.created) {
28675
+ log.info(`contradiction scan cron auto-registered (${created.jobId})`);
28676
+ } else {
28677
+ log.debug("contradiction scan cron already exists, skipping auto-register");
28678
+ }
28679
+ } catch (err) {
28680
+ log.debug(`contradiction scan cron auto-register error: ${err}`);
28681
+ }
28682
+ }
28523
28683
  async applyBehaviorRuntimePolicy(state) {
28524
28684
  const result = await this.policyRuntime.applyFromBehaviorState(state);
28525
28685
  this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
@@ -28647,7 +28807,7 @@ var Orchestrator = class _Orchestrator {
28647
28807
  );
28648
28808
  return result;
28649
28809
  }
28650
- const { FallbackLlmClient: FallbackLlmClient2 } = await import("./fallback-llm-HJRCHKSA.js");
28810
+ const { FallbackLlmClient: FallbackLlmClient2 } = await import("./fallback-llm-QEAPMDW7.js");
28651
28811
  const useGateway = this.config.modelSource === "gateway";
28652
28812
  const modelSetting = this.config.semanticConsolidationModel;
28653
28813
  if (modelSetting === "fast" && this.fastLlm && !useGateway) {
@@ -31036,7 +31196,7 @@ ${trimmedBody}`;
31036
31196
  return null;
31037
31197
  }
31038
31198
  try {
31039
- const { getCalibrationRulesForRecall, buildCalibrationRecallSection } = await import("./calibration-3JHF25QT.js");
31199
+ const { getCalibrationRulesForRecall, buildCalibrationRecallSection } = await import("./calibration-BAC7KNKR.js");
31040
31200
  const rules = await getCalibrationRulesForRecall(this.config.memoryDir);
31041
31201
  if (rules.length === 0) {
31042
31202
  recordRecallSectionMetric({
@@ -31924,7 +32084,7 @@ ${formatted}`;
31924
32084
  if (!this.isRecallSectionEnabled("procedure-recall", true)) return null;
31925
32085
  try {
31926
32086
  return await buildProcedureRecallSection(
31927
- this.storage,
32087
+ profileStorage,
31928
32088
  retrievalQuery,
31929
32089
  this.config
31930
32090
  );
@@ -33337,7 +33497,7 @@ _Context: ${topQuestion.context}_`
33337
33497
  if (!this.queueProcessing) {
33338
33498
  this.queueProcessing = true;
33339
33499
  this.processQueue().catch((err) => {
33340
- log.error("background extraction queue processor failed", err);
33500
+ this.logExtractionQueueFailure(err, "processor");
33341
33501
  this.queueProcessing = false;
33342
33502
  });
33343
33503
  }
@@ -33395,12 +33555,38 @@ ${normalized}`).digest("hex");
33395
33555
  try {
33396
33556
  await task();
33397
33557
  } catch (err) {
33398
- log.error("background extraction task failed", err);
33558
+ this.logExtractionQueueFailure(err, "task");
33399
33559
  }
33400
33560
  }
33401
33561
  }
33402
33562
  this.queueProcessing = false;
33403
33563
  }
33564
+ /**
33565
+ * Classify + log a failure from either the per-task catch inside
33566
+ * `processQueue()` or the outer `processQueue().catch(...)` in
33567
+ * `queueBufferedExtraction()`. Issue #549: `throwIfRecallAborted`
33568
+ * (used throughout `runExtraction`) raises an Error whose `name` is
33569
+ * `"AbortError"`. That path fires when `before_reset` aborts a
33570
+ * queued task to avoid duplicate extraction — it is intentional
33571
+ * cancellation, not a failure. Downgrading the log to debug
33572
+ * prevents spurious `error`-level lines that routinely appear
33573
+ * right next to a successful `persisted: N facts, M entities` log
33574
+ * and that confuse operators into thinking extraction is broken.
33575
+ * Genuine extraction failures (network, parse, I/O) still log at
33576
+ * `error`.
33577
+ *
33578
+ * Source differentiates the two call sites so the log message
33579
+ * names the right layer (`task` vs `processor`).
33580
+ */
33581
+ logExtractionQueueFailure(err, source) {
33582
+ const aborted = source === "task" ? "background extraction task aborted (session transition)" : "background extraction queue processor aborted (session transition)";
33583
+ const failed = source === "task" ? "background extraction task failed" : "background extraction queue processor failed";
33584
+ if (isAbortError(err)) {
33585
+ log.debug(aborted);
33586
+ } else {
33587
+ log.error(failed, err);
33588
+ }
33589
+ }
33404
33590
  async runExtraction(turns, options = {}) {
33405
33591
  log.debug(`running extraction on ${turns.length} turns`);
33406
33592
  const clearBufferAfterExtraction = options.clearBufferAfterExtraction ?? true;
@@ -33412,12 +33598,12 @@ ${normalized}`).digest("hex");
33412
33598
  throw new Error(`replay extraction deadline exceeded (${stage})`);
33413
33599
  }
33414
33600
  };
33415
- const throwIfAborted3 = (stage) => {
33601
+ const throwIfAborted2 = (stage) => {
33416
33602
  throwIfRecallAborted(options.abortSignal, `extraction aborted (${stage})`);
33417
33603
  };
33418
33604
  const clearBuffer = async (options2) => {
33419
33605
  if (options2?.ignoreAbort !== true) {
33420
- throwIfAborted3("before_clear_buffer");
33606
+ throwIfAborted2("before_clear_buffer");
33421
33607
  }
33422
33608
  if (clearBufferAfterExtraction) {
33423
33609
  await this.buffer.clearAfterExtraction(bufferKey);
@@ -33436,7 +33622,7 @@ ${normalized}`).digest("hex");
33436
33622
  content: t.content.trim().slice(0, this.config.extractionMaxTurnChars)
33437
33623
  })).filter((t) => t.content.length > 0);
33438
33624
  throwIfDeadlineExceeded("before_extract");
33439
- throwIfAborted3("before_extract");
33625
+ throwIfAborted2("before_extract");
33440
33626
  const userTurns = normalizedTurns.filter((t) => t.role === "user");
33441
33627
  const totalChars = normalizedTurns.reduce(
33442
33628
  (sum, t) => sum + t.content.length,
@@ -33481,7 +33667,7 @@ ${normalized}`).digest("hex");
33481
33667
  "extraction aborted (during_extract)"
33482
33668
  );
33483
33669
  throwIfDeadlineExceeded("before_persist");
33484
- throwIfAborted3("before_persist");
33670
+ throwIfAborted2("before_persist");
33485
33671
  if (!result) {
33486
33672
  log.warn("runExtraction: extraction returned null/undefined");
33487
33673
  await clearBuffer();
@@ -34194,16 +34380,6 @@ ${normalized}`).digest("hex");
34194
34380
  }
34195
34381
  fact.tags = Array.isArray(fact.tags) ? fact.tags.filter((t) => typeof t === "string") : [];
34196
34382
  fact.confidence = typeof fact.confidence === "number" ? fact.confidence : 0.7;
34197
- if (this.contentHashIndex) {
34198
- const canonicalContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
34199
- if (this.contentHashIndex.has(canonicalContent)) {
34200
- log.debug(
34201
- `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}\u2026"`
34202
- );
34203
- dedupedCount++;
34204
- continue;
34205
- }
34206
- }
34207
34383
  let writeCategory = fact.category;
34208
34384
  let targetStorage = storage;
34209
34385
  let routedRuleId;
@@ -34228,6 +34404,15 @@ ${normalized}`).digest("hex");
34228
34404
  );
34229
34405
  }
34230
34406
  }
34407
+ const canonicalContentForHash = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
34408
+ const contentHashDedupKey = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : canonicalContentForHash;
34409
+ if (this.contentHashIndex && this.contentHashIndex.has(contentHashDedupKey)) {
34410
+ log.debug(
34411
+ `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}\u2026"`
34412
+ );
34413
+ dedupedCount++;
34414
+ continue;
34415
+ }
34231
34416
  const importance = scoreImportance(
34232
34417
  fact.content,
34233
34418
  writeCategory,
@@ -34271,16 +34456,10 @@ ${normalized}`).digest("hex");
34271
34456
  procedureSteps: fact.procedureSteps
34272
34457
  });
34273
34458
  if (!procGate.durable) {
34274
- if (this.config.extractionJudgeShadow) {
34275
- log.info(
34276
- `extraction-procedure-gate[shadow]: would reject "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34277
- );
34278
- } else {
34279
- log.debug(
34280
- `extraction-procedure-gate: rejected "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34281
- );
34282
- continue;
34283
- }
34459
+ log.debug(
34460
+ `extraction-procedure-gate: rejected "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34461
+ );
34462
+ continue;
34284
34463
  }
34285
34464
  }
34286
34465
  let pendingSemanticSkip = null;
@@ -34677,7 +34856,8 @@ ${normalized}`).digest("hex");
34677
34856
  }
34678
34857
  if (this.contentHashIndex) {
34679
34858
  const canonicalFactContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
34680
- this.contentHashIndex.add(canonicalFactContent);
34859
+ const hashRegisterKey = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : canonicalFactContent;
34860
+ this.contentHashIndex.add(hashRegisterKey);
34681
34861
  }
34682
34862
  }
34683
34863
  for (const entity of entities) {
@@ -45363,6 +45543,7 @@ function sleep2(ms) {
45363
45543
  }
45364
45544
 
45365
45545
  // ../remnic-core/src/procedural/procedure-miner.ts
45546
+ var PROCEDURE_CLUSTER_ATTR_MAX = 500;
45366
45547
  function clusterKey(record) {
45367
45548
  const goal = record.goal.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 120);
45368
45549
  const refs = [...record.entityRefs ?? []].map((r) => r.trim().toLowerCase()).sort();
@@ -45395,11 +45576,12 @@ function pseudoStepsFromCluster(group) {
45395
45576
  }));
45396
45577
  }
45397
45578
  async function hasExistingClusterWrite(storage, cluster) {
45579
+ const clusterKey2 = cluster.slice(0, PROCEDURE_CLUSTER_ATTR_MAX);
45398
45580
  const memories = await storage.readAllMemories();
45399
45581
  for (const m of memories) {
45400
45582
  if (m.frontmatter.category !== "procedure") continue;
45401
45583
  const c = m.frontmatter.structuredAttributes?.procedure_cluster;
45402
- if (c === cluster) return true;
45584
+ if (c === clusterKey2) return true;
45403
45585
  }
45404
45586
  return false;
45405
45587
  }
@@ -45411,8 +45593,10 @@ async function runProcedureMining(options) {
45411
45593
  if (cfg.minOccurrences <= 0) {
45412
45594
  return { clustersProcessed: 0, proceduresWritten: 0, skippedReason: "minOccurrences_zero" };
45413
45595
  }
45596
+ const trajectoryDir = typeof options.config.causalTrajectoryStoreDir === "string" && options.config.causalTrajectoryStoreDir.trim().length > 0 ? options.config.causalTrajectoryStoreDir.trim() : void 0;
45414
45597
  const { trajectories } = await readCausalTrajectoryRecords({
45415
- memoryDir: options.memoryDir
45598
+ memoryDir: options.memoryDir,
45599
+ causalTrajectoryStoreDir: trajectoryDir
45416
45600
  });
45417
45601
  const recent = filterTrajectoriesByLookbackDays(trajectories, cfg.lookbackDays);
45418
45602
  const clusters = /* @__PURE__ */ new Map();
@@ -45443,7 +45627,7 @@ async function runProcedureMining(options) {
45443
45627
  status: promote ? "active" : "pending_review",
45444
45628
  tags: ["procedure-miner", "causal-trajectory"],
45445
45629
  structuredAttributes: {
45446
- procedure_cluster: key.slice(0, 500),
45630
+ procedure_cluster: key.slice(0, PROCEDURE_CLUSTER_ATTR_MAX),
45447
45631
  trajectory_ids: group.map((g) => g.trajectoryId).join(",").slice(0, 1900),
45448
45632
  trajectory_count: String(group.length),
45449
45633
  success_rate: rate.toFixed(4)
@@ -48064,6 +48248,25 @@ ${next}`);
48064
48248
  }
48065
48249
  return { submitted: memoryIds.length, matched: matchedIds.length };
48066
48250
  }
48251
+ // ── Contradiction Review (issue #520) ──────────────────────────────────────
48252
+ get memoryDir() {
48253
+ return this.orchestrator.config.memoryDir;
48254
+ }
48255
+ get storageRef() {
48256
+ return this.orchestrator.storage;
48257
+ }
48258
+ get configRef() {
48259
+ return this.orchestrator.config;
48260
+ }
48261
+ get localLlmRef() {
48262
+ return this.orchestrator.localLlm ?? null;
48263
+ }
48264
+ get fallbackLlmRef() {
48265
+ return null;
48266
+ }
48267
+ get embeddingLookupRef() {
48268
+ return void 0;
48269
+ }
48067
48270
  };
48068
48271
 
48069
48272
  // ../remnic-core/src/access-http.ts
@@ -48812,7 +49015,45 @@ var EngramMcpServer = class {
48812
49015
  },
48813
49016
  additionalProperties: false
48814
49017
  }
48815
- }] : []
49018
+ }] : [],
49019
+ // ── Contradiction Review (issue #520) ────────────────────────────────
49020
+ {
49021
+ name: "engram.review_list",
49022
+ description: "List contradiction review items pending user resolution.",
49023
+ inputSchema: {
49024
+ type: "object",
49025
+ properties: {
49026
+ filter: { type: "string", enum: ["all", "unresolved", "contradicts", "independent", "duplicates", "needs-user"], description: "Filter by verdict type. Default: unresolved." },
49027
+ namespace: { type: "string" },
49028
+ limit: { type: "number", description: "Max items to return (default 50)." }
49029
+ },
49030
+ additionalProperties: false
49031
+ }
49032
+ },
49033
+ {
49034
+ name: "engram.review_resolve",
49035
+ description: "Resolve a contradiction pair with a chosen verb.",
49036
+ inputSchema: {
49037
+ type: "object",
49038
+ properties: {
49039
+ pairId: { type: "string", description: "The contradiction pair ID to resolve." },
49040
+ verb: { type: "string", enum: ["keep-a", "keep-b", "merge", "both-valid", "needs-more-context"], description: "Resolution action." }
49041
+ },
49042
+ required: ["pairId", "verb"],
49043
+ additionalProperties: false
49044
+ }
49045
+ },
49046
+ {
49047
+ name: "engram.contradiction_scan_run",
49048
+ description: "Run an on-demand contradiction scan over the memory corpus.",
49049
+ inputSchema: {
49050
+ type: "object",
49051
+ properties: {
49052
+ namespace: { type: "string" }
49053
+ },
49054
+ additionalProperties: false
49055
+ }
49056
+ }
48816
49057
  ].flatMap((tool) => withToolAliases(tool));
48817
49058
  }
48818
49059
  service;
@@ -49419,6 +49660,39 @@ ${body}`;
49419
49660
  principal: effectivePrincipal
49420
49661
  });
49421
49662
  }
49663
+ // ── Contradiction Review (issue #520) ──────────────────────────────────
49664
+ case "engram.review_list":
49665
+ case "remnic.review_list": {
49666
+ const { listPairs } = await import("./contradiction-review-SVGBS3V5.js");
49667
+ const filter = typeof args.filter === "string" ? args.filter : "unresolved";
49668
+ const ns = typeof args.namespace === "string" ? args.namespace : void 0;
49669
+ const limit = typeof args.limit === "number" ? args.limit : 50;
49670
+ return listPairs(this.service.memoryDir, { filter, namespace: ns, limit });
49671
+ }
49672
+ case "engram.review_resolve":
49673
+ case "remnic.review_resolve": {
49674
+ const pairId = typeof args.pairId === "string" ? args.pairId : "";
49675
+ const verb = typeof args.verb === "string" ? args.verb : "";
49676
+ if (!pairId) throw new Error("pairId is required");
49677
+ if (!verb) throw new Error("verb is required");
49678
+ const { isValidResolutionVerb } = await import("./resolution-YITUVUTH.js");
49679
+ if (!isValidResolutionVerb(verb)) throw new Error(`Invalid verb: ${verb}. Must be one of: keep-a, keep-b, merge, both-valid, needs-more-context`);
49680
+ const { executeResolution } = await import("./resolution-YITUVUTH.js");
49681
+ return executeResolution(this.service.memoryDir, this.service.storageRef, pairId, verb);
49682
+ }
49683
+ case "engram.contradiction_scan_run":
49684
+ case "remnic.contradiction_scan_run": {
49685
+ const { runContradictionScan } = await import("./contradiction-scan-LRRLWUOS.js");
49686
+ return runContradictionScan({
49687
+ storage: this.service.storageRef,
49688
+ config: this.service.configRef,
49689
+ memoryDir: this.service.memoryDir,
49690
+ embeddingLookup: this.service.embeddingLookupRef,
49691
+ localLlm: this.service.localLlmRef,
49692
+ fallbackLlm: this.service.fallbackLlmRef,
49693
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0
49694
+ });
49695
+ }
49422
49696
  default:
49423
49697
  throw new Error(`unknown tool: ${name}`);
49424
49698
  }
@@ -50266,6 +50540,67 @@ var EngramAccessHttpServer = class {
50266
50540
  });
50267
50541
  return;
50268
50542
  }
50543
+ if (req.method === "GET" && pathname === "/engram/v1/review/contradictions") {
50544
+ const VALID_FILTERS = /* @__PURE__ */ new Set(["all", "unresolved", "contradicts", "independent", "duplicates", "needs-user"]);
50545
+ const rawFilter = parsed.searchParams.get("filter") ?? "unresolved";
50546
+ if (!VALID_FILTERS.has(rawFilter)) {
50547
+ this.respondJson(res, 400, { error: `Invalid filter '${rawFilter}'. Valid: ${[...VALID_FILTERS].join(", ")}` });
50548
+ return;
50549
+ }
50550
+ const namespace = parsed.searchParams.get("namespace") ?? void 0;
50551
+ const limitRaw = parseInt(parsed.searchParams.get("limit") ?? "50", 10);
50552
+ const { listPairs } = await import("./contradiction-review-SVGBS3V5.js");
50553
+ const result = listPairs(this.service.memoryDir, {
50554
+ filter: rawFilter,
50555
+ namespace,
50556
+ limit: Number.isFinite(limitRaw) ? limitRaw : 50
50557
+ });
50558
+ this.respondJson(res, 200, result);
50559
+ return;
50560
+ }
50561
+ if (req.method === "GET" && pathname.startsWith("/engram/v1/review/contradictions/")) {
50562
+ const pairId = pathname.split("/").pop() ?? "";
50563
+ const { readPair } = await import("./contradiction-review-SVGBS3V5.js");
50564
+ const pair = readPair(this.service.memoryDir, pairId);
50565
+ if (!pair) {
50566
+ this.respondJson(res, 404, { error: "pair_not_found" });
50567
+ return;
50568
+ }
50569
+ this.respondJson(res, 200, pair);
50570
+ return;
50571
+ }
50572
+ if (req.method === "POST" && pathname === "/engram/v1/review/resolve") {
50573
+ const body = await this.readJsonBody(req);
50574
+ const pairId = typeof body.pairId === "string" ? body.pairId : "";
50575
+ const verb = typeof body.verb === "string" ? body.verb : "";
50576
+ if (!pairId || !verb) {
50577
+ this.respondJson(res, 400, { error: "pairId and verb are required" });
50578
+ return;
50579
+ }
50580
+ const { isValidResolutionVerb, executeResolution } = await import("./resolution-YITUVUTH.js");
50581
+ if (!isValidResolutionVerb(verb)) {
50582
+ this.respondJson(res, 400, { error: `Invalid verb: ${verb}. Must be one of: keep-a, keep-b, merge, both-valid, needs-more-context` });
50583
+ return;
50584
+ }
50585
+ const result = await executeResolution(this.service.memoryDir, this.service.storageRef, pairId, verb);
50586
+ this.respondJson(res, 200, result);
50587
+ return;
50588
+ }
50589
+ if (req.method === "POST" && pathname === "/engram/v1/contradiction-scan") {
50590
+ const body = await this.readJsonBody(req);
50591
+ const { runContradictionScan } = await import("./contradiction-scan-LRRLWUOS.js");
50592
+ const result = await runContradictionScan({
50593
+ storage: this.service.storageRef,
50594
+ config: this.service.configRef,
50595
+ memoryDir: this.service.memoryDir,
50596
+ embeddingLookup: this.service.embeddingLookupRef,
50597
+ localLlm: this.service.localLlmRef,
50598
+ fallbackLlm: this.service.fallbackLlmRef,
50599
+ namespace: typeof body.namespace === "string" ? body.namespace : void 0
50600
+ });
50601
+ this.respondJson(res, 200, result);
50602
+ return;
50603
+ }
50269
50604
  this.respondJson(res, 404, { error: "not_found", code: "not_found" });
50270
50605
  }
50271
50606
  async handleMcpRequest(req, res) {
@@ -51536,7 +51871,7 @@ async function runSemanticRulePromoteCliCommand(options) {
51536
51871
  });
51537
51872
  }
51538
51873
  async function runCompoundingPromoteCliCommand(options) {
51539
- const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-BU6GNUJ5.js");
51874
+ const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-WGNTTFYE.js");
51540
51875
  const config = parseConfig({
51541
51876
  memoryDir: options.memoryDir,
51542
51877
  qmdEnabled: false,
@@ -55146,6 +55481,94 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
55146
55481
  }
55147
55482
  console.log(orchestrator.summarizer.formatForRecall(summaries, summaries.length));
55148
55483
  });
55484
+ const reviewCmd = cmd.command("review").description("Manage contradiction review queue");
55485
+ reviewCmd.command("list").description("List contradiction review items").option("--filter <type>", "Filter: all, unresolved, contradicts, independent, duplicates, needs-user", "unresolved").option("--namespace <ns>", "Filter by namespace").option("--limit <n>", "Max items (default 50)", "50").action(async (...args) => {
55486
+ const options = args[0] ?? {};
55487
+ const filter = options.filter ?? "unresolved";
55488
+ const validFilters = ["all", "unresolved", "contradicts", "independent", "duplicates", "needs-user"];
55489
+ if (!validFilters.includes(filter)) {
55490
+ console.error(`Invalid filter: ${filter}. Must be one of: ${validFilters.join(", ")}`);
55491
+ process.exit(1);
55492
+ }
55493
+ const limit = parseInt(options.limit ?? "50", 10);
55494
+ const { listPairs } = await import("./contradiction-review-SVGBS3V5.js");
55495
+ const result = listPairs(orchestrator.config.memoryDir, {
55496
+ filter,
55497
+ namespace: options.namespace,
55498
+ limit: Number.isFinite(limit) ? limit : 50
55499
+ });
55500
+ if (result.pairs.length === 0) {
55501
+ console.log("No review items found.");
55502
+ return;
55503
+ }
55504
+ console.log(`Found ${result.total} item(s) (${result.durationMs}ms):
55505
+ `);
55506
+ for (const pair of result.pairs) {
55507
+ console.log(` [${pair.verdict}] ${pair.pairId}`);
55508
+ console.log(` Memories: ${pair.memoryIds.join(", ")}`);
55509
+ console.log(` Rationale: ${pair.rationale}`);
55510
+ console.log(` Confidence: ${pair.confidence.toFixed(2)}`);
55511
+ if (pair.resolution) console.log(` Resolution: ${pair.resolution}`);
55512
+ console.log();
55513
+ }
55514
+ });
55515
+ reviewCmd.command("show <pairId>").description("Show details of a contradiction pair").action(async (...args) => {
55516
+ const pairId = typeof args[0] === "string" ? args[0] : "";
55517
+ if (!pairId) {
55518
+ console.error("pairId is required");
55519
+ process.exit(1);
55520
+ }
55521
+ const { readPair } = await import("./contradiction-review-SVGBS3V5.js");
55522
+ const pair = readPair(orchestrator.config.memoryDir, pairId);
55523
+ if (!pair) {
55524
+ console.error(`Pair ${pairId} not found.`);
55525
+ process.exit(1);
55526
+ }
55527
+ console.log(JSON.stringify(pair, null, 2));
55528
+ });
55529
+ reviewCmd.command("resolve <pairId>").description("Resolve a contradiction pair").requiredOption("--verb <verb>", "Resolution verb: keep-a, keep-b, merge, both-valid, needs-more-context").action(async (...args) => {
55530
+ const pairId = typeof args[0] === "string" ? args[0] : "";
55531
+ const cmdOpts = args[1] ?? {};
55532
+ if (!pairId) {
55533
+ console.error("pairId is required");
55534
+ process.exit(1);
55535
+ }
55536
+ const verb = cmdOpts.verb;
55537
+ if (!verb) {
55538
+ console.error("--verb is required. Must be one of: keep-a, keep-b, merge, both-valid, needs-more-context");
55539
+ process.exit(1);
55540
+ }
55541
+ const { isValidResolutionVerb, executeResolution } = await import("./resolution-YITUVUTH.js");
55542
+ if (!isValidResolutionVerb(verb)) {
55543
+ console.error(`Invalid verb: ${verb}. Must be one of: keep-a, keep-b, merge, both-valid, needs-more-context`);
55544
+ process.exit(1);
55545
+ }
55546
+ const result = await executeResolution(orchestrator.config.memoryDir, orchestrator.storage, pairId, verb);
55547
+ console.log(result.message);
55548
+ if (result.affectedIds.length > 0) {
55549
+ console.log(`Affected: ${result.affectedIds.join(", ")}`);
55550
+ }
55551
+ });
55552
+ reviewCmd.command("scan").description("Run an on-demand contradiction scan").option("--namespace <ns>", "Namespace to scan").action(async (...args) => {
55553
+ const options = args[0] ?? {};
55554
+ const { runContradictionScan } = await import("./contradiction-scan-LRRLWUOS.js");
55555
+ console.log("Running contradiction scan...");
55556
+ const result = await runContradictionScan({
55557
+ storage: orchestrator.storage,
55558
+ config: orchestrator.config,
55559
+ memoryDir: orchestrator.config.memoryDir,
55560
+ embeddingLookup: void 0,
55561
+ localLlm: orchestrator.localLlm ?? null,
55562
+ fallbackLlm: null,
55563
+ namespace: options.namespace
55564
+ });
55565
+ console.log(`Scan complete in ${result.elapsedMs}ms:`);
55566
+ console.log(` Scanned: ${result.scanned} memories`);
55567
+ console.log(` Candidates: ${result.candidates} pairs`);
55568
+ console.log(` Judged: ${result.judged}`);
55569
+ console.log(` Queued: ${result.queued}`);
55570
+ console.log(` Cooled down: ${result.cooledDown}`);
55571
+ });
55149
55572
  },
55150
55573
  { commands: ["engram"] }
55151
55574
  );