@remnic/plugin-openclaw 1.0.5 → 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,11 +3,13 @@ import {
3
3
  SharedContextManager,
4
4
  defaultTierMigrationCycleBudget,
5
5
  external_exports
6
- } from "./chunk-Y65XJVI3.js";
6
+ } from "./chunk-SVGN3ACY.js";
7
7
  import {
8
+ filterTrajectoriesByLookbackDays,
8
9
  getCausalTrajectoryStoreStatus,
10
+ readCausalTrajectoryRecords,
9
11
  searchCausalTrajectories
10
- } from "./chunk-RMFPW4VK.js";
12
+ } from "./chunk-LN5UZQVG.js";
11
13
  import {
12
14
  compareVersions
13
15
  } from "./chunk-GUSMRW4H.js";
@@ -24,7 +26,7 @@ import {
24
26
  parseConsolidationResponse,
25
27
  renderExtensionsFooter,
26
28
  resolveExtensionsRoot
27
- } from "./chunk-J2FCINY7.js";
29
+ } from "./chunk-QHMR3D7U.js";
28
30
  import {
29
31
  ContentHashIndex,
30
32
  MEMORY_LIFECYCLE_EVENT_SORT_ORDER,
@@ -66,7 +68,7 @@ import {
66
68
  sortMemoryLifecycleEvents,
67
69
  stripCitationForTemplate,
68
70
  toMemoryPathRel
69
- } from "./chunk-5VTGFKKU.js";
71
+ } from "./chunk-KPMXWORS.js";
70
72
  import {
71
73
  BoxBuilder,
72
74
  assertIsoRecordedAt,
@@ -83,12 +85,14 @@ import {
83
85
  import {
84
86
  FallbackLlmClient,
85
87
  buildChatCompletionTokenLimit,
86
- extractJsonCandidates,
87
88
  mergeEnv,
88
89
  readEnvVar,
89
90
  resolveHomeDir,
90
91
  shouldAssumeOpenAiChatCompletions
91
- } from "./chunk-3SA5F4WT.js";
92
+ } from "./chunk-NXLHSCLU.js";
93
+ import {
94
+ extractJsonCandidates
95
+ } from "./chunk-3A5ELHTT.js";
92
96
  import {
93
97
  listJsonFiles,
94
98
  listNamedFiles,
@@ -233,6 +237,18 @@ function coerceBool(value) {
233
237
  function coerceInstallExtension(value) {
234
238
  return coerceBool(value);
235
239
  }
240
+ function coerceNumber(value) {
241
+ if (typeof value === "number") {
242
+ return Number.isFinite(value) ? value : void 0;
243
+ }
244
+ if (typeof value === "string") {
245
+ const trimmed = value.trim();
246
+ if (trimmed.length === 0) return void 0;
247
+ const n = Number(trimmed);
248
+ return Number.isFinite(n) ? n : void 0;
249
+ }
250
+ return void 0;
251
+ }
236
252
 
237
253
  // ../remnic-core/src/config.ts
238
254
  var DEFAULT_MEMORY_DIR = path2.join(
@@ -292,6 +308,31 @@ function normalizeMemoryRelativeDir(raw, fallback) {
292
308
  const normalized = trimmed.replace(/\\/g, "/").split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..").join("/");
293
309
  return normalized.length > 0 ? normalized : fallback;
294
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
+ }
295
336
  function parseSemanticChunkingConfig(raw) {
296
337
  if (!raw || typeof raw !== "object") return {};
297
338
  const src = raw;
@@ -327,7 +368,8 @@ var VALID_MEMORY_CATEGORIES = /* @__PURE__ */ new Set([
327
368
  "commitment",
328
369
  "moment",
329
370
  "skill",
330
- "rule"
371
+ "rule",
372
+ "procedure"
331
373
  ]);
332
374
  var DEFAULT_BEHAVIOR_LOOP_PROTECTED_PARAMS = [
333
375
  "maxMemoryTokens",
@@ -494,6 +536,28 @@ function parseConfig(raw) {
494
536
  ) ? rawCodexCompat.compactionFlushMode : "auto",
495
537
  fingerprintDedup: rawCodexCompat.fingerprintDedup !== false
496
538
  };
539
+ const rawProcedural = cfg.procedural && typeof cfg.procedural === "object" && !Array.isArray(cfg.procedural) ? cfg.procedural : {};
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;
550
+ const procedural = {
551
+ enabled: coerceBool(rawProcedural.enabled) === true,
552
+ /** `0` skips all mining (`minOccurrences_zero`); otherwise clusters need at least this many members. */
553
+ minOccurrences: Math.min(1e3, Math.max(0, proceduralMinRaw)),
554
+ successFloor,
555
+ autoPromoteOccurrences,
556
+ autoPromoteEnabled: coerceBool(rawProcedural.autoPromoteEnabled) === true,
557
+ lookbackDays,
558
+ proceduralMiningCronAutoRegister: coerceBool(rawProcedural.proceduralMiningCronAutoRegister) === true,
559
+ recallMaxProcedures
560
+ };
497
561
  const memoryDir = typeof cfg.memoryDir === "string" && cfg.memoryDir.length > 0 ? cfg.memoryDir : DEFAULT_MEMORY_DIR;
498
562
  const rawIdentityInjectionMode = cfg.identityInjectionMode;
499
563
  const identityInjectionMode = rawIdentityInjectionMode && VALID_IDENTITY_INJECTION_MODES.includes(rawIdentityInjectionMode) ? rawIdentityInjectionMode : "recovery_only";
@@ -676,11 +740,36 @@ function parseConfig(raw) {
676
740
  contradictionSimilarityThreshold: typeof cfg.contradictionSimilarityThreshold === "number" ? cfg.contradictionSimilarityThreshold : 0.7,
677
741
  contradictionMinConfidence: typeof cfg.contradictionMinConfidence === "number" ? cfg.contradictionMinConfidence : 0.9,
678
742
  contradictionAutoResolve: cfg.contradictionAutoResolve !== false,
743
+ // Contradiction Scan cron (issue #520)
744
+ contradictionScan: parseContradictionScanConfig(cfg.contradictionScan),
679
745
  // Temporal Supersession (issue #375)
680
746
  temporalSupersessionEnabled: cfg.temporalSupersessionEnabled !== false,
681
747
  // On by default
682
748
  temporalSupersessionIncludeInRecall: cfg.temporalSupersessionIncludeInRecall === true,
683
749
  // Off by default
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,
756
+ recallDirectAnswerTokenOverlapFloor: (() => {
757
+ const n = coerceNumber(cfg.recallDirectAnswerTokenOverlapFloor);
758
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.55;
759
+ })(),
760
+ recallDirectAnswerImportanceFloor: (() => {
761
+ const n = coerceNumber(cfg.recallDirectAnswerImportanceFloor);
762
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.7;
763
+ })(),
764
+ recallDirectAnswerAmbiguityMargin: (() => {
765
+ const n = coerceNumber(cfg.recallDirectAnswerAmbiguityMargin);
766
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.15;
767
+ })(),
768
+ recallDirectAnswerEligibleTaxonomyBuckets: Array.isArray(
769
+ cfg.recallDirectAnswerEligibleTaxonomyBuckets
770
+ ) ? cfg.recallDirectAnswerEligibleTaxonomyBuckets.filter(
771
+ (v) => typeof v === "string" && v.length > 0
772
+ ) : ["decisions", "principles", "conventions", "runbooks", "entities"],
684
773
  // Memory Linking (Phase 3A)
685
774
  memoryLinkingEnabled: cfg.memoryLinkingEnabled === true,
686
775
  // Off by default initially
@@ -756,6 +845,7 @@ function parseConfig(raw) {
756
845
  activeRecallAttachRecallExplain: cfg.activeRecallAttachRecallExplain === true,
757
846
  activeRecallAllowChainedActiveMemory: cfg.activeRecallAllowChainedActiveMemory === true,
758
847
  dreaming,
848
+ procedural,
759
849
  heartbeat,
760
850
  slotBehavior,
761
851
  codexCompat,
@@ -825,7 +915,7 @@ function parseConfig(raw) {
825
915
  semanticConsolidationMinClusterSize: typeof cfg.semanticConsolidationMinClusterSize === "number" ? Math.max(2, Math.floor(cfg.semanticConsolidationMinClusterSize)) : 3,
826
916
  semanticConsolidationExcludeCategories: Array.isArray(cfg.semanticConsolidationExcludeCategories) ? cfg.semanticConsolidationExcludeCategories.filter(
827
917
  (c) => typeof c === "string" && c.length > 0
828
- ) : ["correction", "commitment"],
918
+ ) : ["correction", "commitment", "procedure"],
829
919
  semanticConsolidationIntervalHours: typeof cfg.semanticConsolidationIntervalHours === "number" ? Math.max(1, Math.floor(cfg.semanticConsolidationIntervalHours)) : 168,
830
920
  semanticConsolidationMaxPerRun: typeof cfg.semanticConsolidationMaxPerRun === "number" ? Math.max(0, Math.floor(cfg.semanticConsolidationMaxPerRun)) : 100,
831
921
  creationMemoryEnabled: cfg.creationMemoryEnabled === true,
@@ -924,6 +1014,22 @@ function parseConfig(raw) {
924
1014
  localLlmFastModel: typeof cfg.localLlmFastModel === "string" && cfg.localLlmFastModel.length > 0 ? cfg.localLlmFastModel : "",
925
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",
926
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,
927
1033
  // Gateway config (passed from index.ts for fallback AI)
928
1034
  gatewayConfig: cfg.gatewayConfig,
929
1035
  // Gateway model source (v9.2) — route LLM calls through gateway agent model chain
@@ -1075,7 +1181,7 @@ function parseConfig(raw) {
1075
1181
  factArchivalAgeDays: typeof cfg.factArchivalAgeDays === "number" ? cfg.factArchivalAgeDays : 90,
1076
1182
  factArchivalMaxImportance: typeof cfg.factArchivalMaxImportance === "number" ? cfg.factArchivalMaxImportance : 0.3,
1077
1183
  factArchivalMaxAccessCount: typeof cfg.factArchivalMaxAccessCount === "number" ? cfg.factArchivalMaxAccessCount : 2,
1078
- factArchivalProtectedCategories: Array.isArray(cfg.factArchivalProtectedCategories) ? cfg.factArchivalProtectedCategories.filter((c) => typeof c === "string") : ["commitment", "preference", "decision", "principle"],
1184
+ factArchivalProtectedCategories: Array.isArray(cfg.factArchivalProtectedCategories) ? cfg.factArchivalProtectedCategories.filter((c) => typeof c === "string") : ["commitment", "preference", "decision", "principle", "procedure"],
1079
1185
  // v8.3 lifecycle policy engine (default off)
1080
1186
  lifecyclePolicyEnabled: cfg.lifecyclePolicyEnabled === true,
1081
1187
  lifecycleFilterStaleEnabled: cfg.lifecycleFilterStaleEnabled === true,
@@ -1084,7 +1190,7 @@ function parseConfig(raw) {
1084
1190
  lifecycleArchiveDecayThreshold: typeof cfg.lifecycleArchiveDecayThreshold === "number" ? Math.min(1, Math.max(0, cfg.lifecycleArchiveDecayThreshold)) : 0.85,
1085
1191
  lifecycleProtectedCategories: Array.isArray(cfg.lifecycleProtectedCategories) ? cfg.lifecycleProtectedCategories.filter(
1086
1192
  (c) => typeof c === "string" && VALID_MEMORY_CATEGORIES.has(c)
1087
- ) : ["decision", "principle", "commitment", "preference"],
1193
+ ) : ["decision", "principle", "commitment", "preference", "procedure"],
1088
1194
  lifecycleMetricsEnabled: typeof cfg.lifecycleMetricsEnabled === "boolean" ? cfg.lifecycleMetricsEnabled : cfg.lifecyclePolicyEnabled === true,
1089
1195
  // v8.3 proactive + policy learning (default off)
1090
1196
  proactiveExtractionEnabled: cfg.proactiveExtractionEnabled === true,
@@ -1356,6 +1462,11 @@ function buildDefaultRecallPipeline(cfg) {
1356
1462
  maxEntities: typeof cfg.knowledgeIndexMaxEntities === "number" ? Math.max(0, Math.floor(cfg.knowledgeIndexMaxEntities)) : 40
1357
1463
  },
1358
1464
  { id: "verbatim-artifacts", enabled: cfg.verbatimArtifactsEnabled === true },
1465
+ {
1466
+ id: "procedure-recall",
1467
+ enabled: typeof cfg.procedural === "object" && cfg.procedural !== null && !Array.isArray(cfg.procedural) && cfg.procedural.enabled === true,
1468
+ maxChars: 2400
1469
+ },
1359
1470
  { id: "memory-boxes", enabled: cfg.memoryBoxesEnabled === true },
1360
1471
  { id: "temporal-memory-tree", enabled: cfg.temporalMemoryTreeEnabled === true },
1361
1472
  { id: "lcm-compressed-history", enabled: cfg.lcmEnabled === true },
@@ -1481,7 +1592,7 @@ function detectSdkCapabilities(api) {
1481
1592
  // ../remnic-core/src/orchestrator.ts
1482
1593
  import path43 from "path";
1483
1594
  import os3 from "os";
1484
- import { createHash as createHash9 } from "crypto";
1595
+ import { createHash as createHash9, randomBytes } from "crypto";
1485
1596
  import { existsSync as existsSync8 } from "fs";
1486
1597
  import {
1487
1598
  mkdir as mkdir30,
@@ -2142,6 +2253,15 @@ var SmartBuffer = class {
2142
2253
  this.state = this.normalizeState(await this.storage.loadBuffer());
2143
2254
  this.loaded = true;
2144
2255
  }
2256
+ /**
2257
+ * Reset the buffer to an empty, usable state.
2258
+ * Called when the persisted buffer file is corrupt and load() fails,
2259
+ * so the buffer can still accept new turns for the rest of the session.
2260
+ */
2261
+ resetToEmpty() {
2262
+ this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
2263
+ this.loaded = true;
2264
+ }
2145
2265
  async save() {
2146
2266
  await this.storage.saveBuffer(this.state);
2147
2267
  }
@@ -2782,6 +2902,10 @@ function trimTrailingSlashes(s) {
2782
2902
  while (end > 0 && s[end - 1] === "/") end--;
2783
2903
  return s.substring(0, end);
2784
2904
  }
2905
+ var THINKING_COMPATIBLE_BACKENDS = /* @__PURE__ */ new Set([
2906
+ "lmstudio",
2907
+ "vllm"
2908
+ ]);
2785
2909
  var LOCAL_SERVERS = [
2786
2910
  {
2787
2911
  type: "ollama",
@@ -2840,9 +2964,16 @@ var LocalLlmClient = class _LocalLlmClient {
2840
2964
  this.modelRegistry = modelRegistry;
2841
2965
  }
2842
2966
  /**
2843
- * Disable thinking/reasoning mode for models that support it (e.g. Qwen 3.5).
2844
- * When enabled, adds chat_template_kwargs to suppress chain-of-thought,
2845
- * 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.
2846
2977
  */
2847
2978
  set disableThinking(value) {
2848
2979
  this._disableThinking = value;
@@ -3340,7 +3471,7 @@ var LocalLlmClient = class _LocalLlmClient {
3340
3471
  if (options.responseFormat?.type === "json_schema") {
3341
3472
  requestBody.response_format = options.responseFormat;
3342
3473
  }
3343
- if (this._disableThinking) {
3474
+ if (this._disableThinking && this.detectedType !== null && THINKING_COMPATIBLE_BACKENDS.has(this.detectedType)) {
3344
3475
  requestBody.chat_template_kwargs = { enable_thinking: false };
3345
3476
  }
3346
3477
  const baseUrl = trimTrailingSlashes(
@@ -3710,14 +3841,40 @@ function parseMemoryActionEligibilityContext(value) {
3710
3841
  source: "unknown"
3711
3842
  };
3712
3843
  }
3844
+ var ProcedureStepExtractSchema = external_exports.object({
3845
+ order: external_exports.number(),
3846
+ intent: external_exports.string(),
3847
+ toolCall: external_exports.object({
3848
+ kind: external_exports.string(),
3849
+ signature: external_exports.string()
3850
+ }).optional().nullable(),
3851
+ expectedOutcome: external_exports.string().optional().nullable(),
3852
+ optional: external_exports.boolean().optional().nullable()
3853
+ });
3713
3854
  var ExtractedFactSchema = external_exports.object({
3714
- category: external_exports.enum(["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]),
3855
+ category: external_exports.enum([
3856
+ "fact",
3857
+ "preference",
3858
+ "correction",
3859
+ "entity",
3860
+ "decision",
3861
+ "relationship",
3862
+ "principle",
3863
+ "commitment",
3864
+ "moment",
3865
+ "skill",
3866
+ "rule",
3867
+ "procedure"
3868
+ ]),
3715
3869
  content: external_exports.string().describe("The memory content \u2014 a clear, standalone statement"),
3716
3870
  confidence: external_exports.number().min(0).max(1).describe("How confident are you this is correct (0-1)"),
3717
3871
  tags: external_exports.array(external_exports.string()).describe("Relevant tags for categorization"),
3718
3872
  entityRef: external_exports.string().optional().nullable().describe("If about an entity, its normalized name (e.g. person-jane-doe)"),
3719
3873
  promptedByQuestion: external_exports.string().optional().nullable().describe("Optional proactive follow-up question that surfaced this fact."),
3720
- structuredAttributes: external_exports.record(external_exports.string(), external_exports.string()).optional().nullable().describe('Structured key-value attributes when the fact contains measurable or categorical data (e.g., {"price": "29.99", "color": "blue", "date": "2024-03-15"}).')
3874
+ structuredAttributes: external_exports.record(external_exports.string(), external_exports.string()).optional().nullable().describe('Structured key-value attributes when the fact contains measurable or categorical data (e.g., {"price": "29.99", "color": "blue", "date": "2024-03-15"}).'),
3875
+ procedureSteps: external_exports.array(ProcedureStepExtractSchema).optional().nullable().describe(
3876
+ 'For category "procedure" only: ordered steps (intent per step). At least two steps; include explicit trigger phrasing in content (e.g. "When you deploy\u2026").'
3877
+ )
3721
3878
  });
3722
3879
  var EntityMentionSchema = external_exports.object({
3723
3880
  name: external_exports.string().describe("Normalized entity name (e.g. jane-doe, acme-corp, my-project)"),
@@ -4465,6 +4622,67 @@ function parallelEfficiency(group) {
4465
4622
  return Math.round(idealMs / group.wallMs * 100);
4466
4623
  }
4467
4624
 
4625
+ // ../remnic-core/src/procedural/procedure-types.ts
4626
+ function normalizeProcedureSteps(raw) {
4627
+ if (!Array.isArray(raw)) return [];
4628
+ const out = [];
4629
+ for (let i = 0; i < raw.length; i++) {
4630
+ const s = raw[i];
4631
+ if (!s || typeof s !== "object") continue;
4632
+ const o = s;
4633
+ const intent = typeof o.intent === "string" ? o.intent.trim() : "";
4634
+ if (!intent) continue;
4635
+ const orderRaw = o.order;
4636
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? Math.max(1, Math.floor(orderRaw)) : i + 1;
4637
+ let toolCall;
4638
+ const tc = o.toolCall;
4639
+ if (tc && typeof tc === "object" && !Array.isArray(tc)) {
4640
+ const t = tc;
4641
+ const kind = typeof t.kind === "string" ? t.kind.trim() : "";
4642
+ const signature = typeof t.signature === "string" ? t.signature.trim() : "";
4643
+ if (kind && signature) {
4644
+ toolCall = { kind, signature };
4645
+ }
4646
+ }
4647
+ const expectedOutcome = typeof o.expectedOutcome === "string" && o.expectedOutcome.trim() ? o.expectedOutcome.trim() : void 0;
4648
+ const optional = o.optional === true ? true : void 0;
4649
+ out.push({ order, intent, toolCall, expectedOutcome, optional });
4650
+ }
4651
+ return out;
4652
+ }
4653
+ function buildProcedurePersistBody(title, procedureSteps) {
4654
+ const head = typeof title === "string" ? title.trim() : "";
4655
+ const steps = normalizeProcedureSteps(procedureSteps);
4656
+ if (steps.length === 0) return head;
4657
+ return `${head}
4658
+
4659
+ ${buildProcedureMarkdownBody(steps)}`.trimEnd() + "\n";
4660
+ }
4661
+ function buildProcedureMarkdownBody(steps) {
4662
+ const sorted = [...steps].sort((a, b) => a.order - b.order);
4663
+ const lines = [];
4664
+ for (const step of sorted) {
4665
+ const n = Number.isFinite(step.order) ? Math.max(1, Math.floor(step.order)) : 1;
4666
+ lines.push(`## Step ${n}`);
4667
+ lines.push("");
4668
+ lines.push(step.intent.trim());
4669
+ if (step.toolCall?.kind && step.toolCall.signature) {
4670
+ lines.push("");
4671
+ lines.push(`- Tool: \`${step.toolCall.kind}\` \u2014 ${step.toolCall.signature}`);
4672
+ }
4673
+ if (step.expectedOutcome?.trim()) {
4674
+ lines.push("");
4675
+ lines.push(`- Expected: ${step.expectedOutcome.trim()}`);
4676
+ }
4677
+ if (step.optional === true) {
4678
+ lines.push("");
4679
+ lines.push("- Optional: true");
4680
+ }
4681
+ lines.push("");
4682
+ }
4683
+ return lines.join("\n").trimEnd() + "\n";
4684
+ }
4685
+
4468
4686
  // ../remnic-core/src/extraction.ts
4469
4687
  var PROACTIVE_MIN_CONFIDENCE = 0.8;
4470
4688
  function normalizeQuestion(question) {
@@ -4555,8 +4773,9 @@ var ExtractionEngine = class {
4555
4773
  return shouldAssumeOpenAiChatCompletions(this.config.openaiBaseUrl);
4556
4774
  }
4557
4775
  sanitizeExtractionResult(result, messageTimestamp) {
4776
+ const proceduralOn = this.config.procedural?.enabled === true;
4558
4777
  const ts = messageTimestamp ?? /* @__PURE__ */ new Date();
4559
- const facts = result.facts.map((fact) => {
4778
+ const facts = result.facts.filter((fact) => proceduralOn || fact.category !== "procedure").map((fact) => {
4560
4779
  const sanitized = sanitizeMemoryContent(fact.content);
4561
4780
  if (!sanitized.clean) {
4562
4781
  log.warn(`extraction fact sanitized; violations=${sanitized.violations.join(", ")}`);
@@ -4583,7 +4802,8 @@ var ExtractionEngine = class {
4583
4802
  promptedByQuestion: typeof f?.promptedByQuestion === "string" ? f.promptedByQuestion : void 0,
4584
4803
  structuredAttributes: f?.structuredAttributes && typeof f.structuredAttributes === "object" && !Array.isArray(f.structuredAttributes) ? Object.fromEntries(
4585
4804
  Object.entries(f.structuredAttributes).filter(([k, v]) => typeof k === "string" && typeof v === "string")
4586
- ) : void 0
4805
+ ) : void 0,
4806
+ procedureSteps: Array.isArray(f?.procedureSteps) ? normalizeProcedureSteps(f.procedureSteps) : void 0
4587
4807
  })).filter((f) => f.content.length > 0) : [];
4588
4808
  const questions = Array.isArray(parsed?.questions) ? parsed.questions.map((q) => {
4589
4809
  if (typeof q === "string") return { question: q, context: "", priority: 0.5 };
@@ -5244,6 +5464,8 @@ Memory categories \u2014 use the MOST SPECIFIC category that fits:
5244
5464
  - commitment: Promises, obligations, deadlines
5245
5465
  - moment: Emotionally significant events
5246
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.
5247
5469
 
5248
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.
5249
5471
 
@@ -5305,7 +5527,7 @@ Also generate:
5305
5527
 
5306
5528
  Output JSON:
5307
5529
  {
5308
- "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}],
5309
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"]}],
5310
5532
  "profileUpdates": ["User prefers dark mode in all editors"],
5311
5533
  "questions": [{"question": "Which cloud provider hosts the staging environment?", "context": "Came up during deployment discussion", "priority": 0.5}],
@@ -5426,7 +5648,9 @@ Respond with valid JSON matching this schema:
5426
5648
  "principle",
5427
5649
  "commitment",
5428
5650
  "moment",
5429
- "skill"
5651
+ "skill",
5652
+ "rule",
5653
+ "procedure"
5430
5654
  ]);
5431
5655
  const allowedEntityTypes = /* @__PURE__ */ new Set([
5432
5656
  "person",
@@ -5483,6 +5707,7 @@ Memory categories:
5483
5707
  - moment: Emotionally significant events or milestones (e.g., "first successful deployment of engram")
5484
5708
  - skill: Capabilities the user or agent has demonstrated (e.g., "user is proficient with Kubernetes")${this.config.causalRuleExtractionEnabled ? `
5485
5709
  - rule: Causal rules discovered through experience (format: "IF <condition> THEN <action/outcome>", e.g., "IF Shopify API returns 401 THEN the admin token is missing read_products scope")` : ""}
5710
+ - procedure: A reusable workflow the user wants remembered the same way across sessions. Set category to "procedure". Use "content" for a short title that includes explicit trigger phrasing (e.g. "When you deploy to production\u2026", "Whenever you ship a release\u2026"). Add "procedureSteps": an array of at least two objects {"order": number, "intent": "concrete step description"} in execution order. Optional per-step "toolCall": {"kind": "\u2026", "signature": "\u2026"}, "expectedOutcome", "optional": true.
5486
5711
 
5487
5712
  Rules:
5488
5713
  - Only extract genuinely NEW information worth remembering across sessions
@@ -6692,6 +6917,8 @@ var CATEGORY_BOOSTS = {
6692
6917
  // Durable rules/values
6693
6918
  rule: 0.11,
6694
6919
  // Causal IF→THEN rules
6920
+ procedure: 0.1,
6921
+ // Repeatable workflows (issue #519)
6695
6922
  preference: 0.1,
6696
6923
  // User preferences matter
6697
6924
  commitment: 0.1,
@@ -6997,6 +7224,18 @@ function enforceMaxCacheSize(cache) {
6997
7224
  }
6998
7225
  }
6999
7226
  var AUTO_APPROVE_CATEGORIES = /* @__PURE__ */ new Set(["correction", "principle"]);
7227
+ var PROCEDURE_TRIGGER_RE = /(when you|whenever|before you|before running|always\s|first\b.*\bthen|to deploy|to ship|run these steps|follow these steps|how (i|we)\s|recipe for|workflow|each time you)/i;
7228
+ function validateProcedureExtraction(input) {
7229
+ const steps = normalizeProcedureSteps(input.procedureSteps);
7230
+ if (steps.length < 2) {
7231
+ return { durable: false, reason: "Procedure requires at least two steps with intents" };
7232
+ }
7233
+ const combined = [input.content, ...steps.map((s) => s.intent)].join(" ").toLowerCase();
7234
+ if (!PROCEDURE_TRIGGER_RE.test(combined)) {
7235
+ return { durable: false, reason: "Procedure missing explicit trigger phrasing" };
7236
+ }
7237
+ return { durable: true, reason: "Procedure structure validated" };
7238
+ }
7000
7239
  async function judgeFactDurability(candidates, config, localLlm, fallbackLlm, cache) {
7001
7240
  const startMs = Date.now();
7002
7241
  const verdicts = /* @__PURE__ */ new Map();
@@ -7177,6 +7416,156 @@ function createVerdictCache() {
7177
7416
  return /* @__PURE__ */ new Map();
7178
7417
  }
7179
7418
 
7419
+ // ../remnic-core/src/intent.ts
7420
+ var GOAL_PATTERNS = [
7421
+ { re: /\b(debug(?:s|ged|ging)?|fix(?:es|ed|ing)?|error(?:s)?|incident(?:s)?|outage(?:s)?|failure(?:s)?)\b/i, goal: "stabilize" },
7422
+ { re: /\b(deploy(?:s|ed|ing)?|release(?:s|d|ing)?|ship(?:s|ped|ping)?|publish(?:es|ed|ing)?)\b/i, goal: "release" },
7423
+ { re: /\b(plan(?:s|ned|ning)?|roadmap(?:s)?|strateg(?:y|ies)|design(?:s|ed|ing)?)\b/i, goal: "plan" },
7424
+ { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|security|hardening)\b/i, goal: "review" },
7425
+ { re: /\b(sales|deal|customer|client|prospect)\b/i, goal: "close_deal" }
7426
+ ];
7427
+ var ACTION_PATTERNS = [
7428
+ { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|inspect(?:s|ed|ing)?|check(?:s|ed|ing)?)\b/i, action: "review" },
7429
+ { re: /\b(plan(?:s|ned|ning)?|design(?:s|ed|ing)?|brainstorm(?:s|ed|ing)?|spec(?:s)?)\b/i, action: "plan" },
7430
+ { re: /\b(implement(?:s|ed|ing)?|build(?:s|ing)?|built|code(?:s|d|ing)?|patch(?:es|ed|ing)?|fix(?:es|ed|ing)?)\b/i, action: "execute" },
7431
+ { re: /\b(summariz(?:e|es|ed|ing)|recap(?:s|ped|ping)?|what happened|timeline)\b/i, action: "summarize" },
7432
+ { re: /\b(decid(?:e|es|ed|ing)|decision(?:s)?|cho(?:ose|oses|osing)|chose|chosen)\b/i, action: "decide" }
7433
+ ];
7434
+ var ENTITY_PATTERNS = [
7435
+ { re: /\b(pr|pull request|branch|repo|github|ci|workflow)\b/i, entityType: "repo" },
7436
+ { re: /\b(discord|slack|channel|gateway|agent)\b/i, entityType: "ops" },
7437
+ { re: /\b(customer|client|deal|lead|account)\b/i, entityType: "client" },
7438
+ { re: /\b(model|llm|qmd|embedding|retrieval|memory)\b/i, entityType: "ai" },
7439
+ { re: /\b(doc|readme|docs|changelog)\b/i, entityType: "docs" }
7440
+ ];
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;
7442
+ function normalizeTextInput(input) {
7443
+ return typeof input === "string" ? input : "";
7444
+ }
7445
+ function inferIntentFromText(text) {
7446
+ const safeText = normalizeTextInput(text);
7447
+ const goal = GOAL_PATTERNS.find((p) => p.re.test(safeText))?.goal ?? "unknown";
7448
+ const actionType = ACTION_PATTERNS.find((p) => p.re.test(safeText))?.action ?? "unknown";
7449
+ const entityTypes = Array.from(
7450
+ new Set(ENTITY_PATTERNS.filter((p) => p.re.test(safeText)).map((p) => p.entityType))
7451
+ );
7452
+ const taskInitiation = TASK_INITIATION_RE.test(safeText);
7453
+ return {
7454
+ goal,
7455
+ actionType,
7456
+ entityTypes,
7457
+ taskInitiation
7458
+ };
7459
+ }
7460
+ function isTaskInitiationIntent(intent) {
7461
+ return intent.taskInitiation === true;
7462
+ }
7463
+ function intentCompatibilityScore(queryIntent, memoryIntent) {
7464
+ const queryHasSignal = queryIntent.goal !== "unknown" || queryIntent.actionType !== "unknown" || queryIntent.entityTypes.length > 0;
7465
+ const memoryHasSignal = memoryIntent.goal !== "unknown" || memoryIntent.actionType !== "unknown" || memoryIntent.entityTypes.length > 0;
7466
+ if (!queryHasSignal || !memoryHasSignal) return 0;
7467
+ let score = 0;
7468
+ if (queryIntent.goal !== "unknown" && memoryIntent.goal !== "unknown" && queryIntent.goal === memoryIntent.goal) {
7469
+ score += 0.5;
7470
+ }
7471
+ if (queryIntent.actionType !== "unknown" && memoryIntent.actionType !== "unknown" && queryIntent.actionType === memoryIntent.actionType) {
7472
+ score += 0.3;
7473
+ }
7474
+ const overlap = queryIntent.entityTypes.filter((et) => memoryIntent.entityTypes.includes(et)).length;
7475
+ if (overlap > 0) {
7476
+ const denom = Math.max(queryIntent.entityTypes.length, memoryIntent.entityTypes.length, 1);
7477
+ score += 0.2 * (overlap / denom);
7478
+ }
7479
+ return Math.max(0, Math.min(1, score));
7480
+ }
7481
+ function planRecallMode(prompt) {
7482
+ const p = normalizeTextInput(prompt).trim();
7483
+ let ackCandidate = p;
7484
+ while (ackCandidate.length > 0) {
7485
+ const ch = ackCandidate.charCodeAt(ackCandidate.length - 1);
7486
+ const isDigit = ch >= 48 && ch <= 57;
7487
+ const isUpper = ch >= 65 && ch <= 90;
7488
+ const isLower = ch >= 97 && ch <= 122;
7489
+ if (isDigit || isUpper || isLower) break;
7490
+ ackCandidate = ackCandidate.slice(0, -1);
7491
+ }
7492
+ ackCandidate = ackCandidate.trim();
7493
+ if (p.length === 0) return "no_recall";
7494
+ if (/\b(timeline|sequence|history|what happened|chain of events|root cause)\b/i.test(p)) {
7495
+ return "graph_mode";
7496
+ }
7497
+ if (p.length <= 18 && /^(ok|okay|kk|thanks|thx|got it|sounds good|yep|yes|nope|no|done|cool|works)$/i.test(ackCandidate)) {
7498
+ return "no_recall";
7499
+ }
7500
+ if (/\b(previous|earlier|remember|last time|did we|what did we decide|context|summarize|summary|recap|key points|decision)\b/i.test(p) || /\?$/.test(p) || /^(what|why|how|when|where|who|which)\b/i.test(p.toLowerCase())) {
7501
+ return "full";
7502
+ }
7503
+ if (p.length <= 100 && /^(check|reload|restart|run|verify|show|status|sync|update|open|close|set|enable|disable|fix|patch)\b/i.test(p)) {
7504
+ return "minimal";
7505
+ }
7506
+ return "full";
7507
+ }
7508
+ function hasBroadGraphIntent(prompt) {
7509
+ const p = normalizeTextInput(prompt).trim().toLowerCase();
7510
+ if (!p) return false;
7511
+ return /\b(what changed|how did we get here|why did this happen|what led to|cause chain|dependency chain|regression chain|failure chain)\b/i.test(
7512
+ p
7513
+ );
7514
+ }
7515
+
7516
+ // ../remnic-core/src/procedural/procedure-recall.ts
7517
+ function tokenOverlapScore(prompt, memoryText) {
7518
+ const norm = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
7519
+ const promptTokens = new Set(norm(prompt));
7520
+ const memTokens = new Set(norm(memoryText));
7521
+ if (promptTokens.size === 0 || memTokens.size === 0) return 0;
7522
+ let inter = 0;
7523
+ for (const t of promptTokens) {
7524
+ if (memTokens.has(t)) inter++;
7525
+ }
7526
+ const union = /* @__PURE__ */ new Set([...promptTokens, ...memTokens]);
7527
+ return inter / Math.max(1, union.size);
7528
+ }
7529
+ function scoreProcedureForPrompt(m, prompt, queryIntent) {
7530
+ const memText = `${m.content}
7531
+ ${(m.frontmatter.tags ?? []).join(" ")}`;
7532
+ const jaccard = tokenOverlapScore(prompt, memText);
7533
+ const memIntent = inferIntentFromText(m.content.slice(0, 2e3));
7534
+ const intentScore = intentCompatibilityScore(queryIntent, memIntent);
7535
+ return jaccard * 0.55 + intentScore * 0.45;
7536
+ }
7537
+ async function buildProcedureRecallSection(storage, prompt, config) {
7538
+ if (config.procedural?.enabled !== true) return null;
7539
+ const trimmed = typeof prompt === "string" ? prompt.trim() : "";
7540
+ if (!trimmed) return null;
7541
+ const queryIntent = inferIntentFromText(trimmed);
7542
+ if (!isTaskInitiationIntent(queryIntent)) return null;
7543
+ const maxN = Math.min(
7544
+ 10,
7545
+ Math.max(
7546
+ 1,
7547
+ typeof config.procedural.recallMaxProcedures === "number" && Number.isFinite(config.procedural.recallMaxProcedures) ? Math.floor(config.procedural.recallMaxProcedures) : 3
7548
+ )
7549
+ );
7550
+ const all = await storage.readAllMemories();
7551
+ const scored = all.filter(
7552
+ (m) => m.frontmatter.category === "procedure" && m.frontmatter.status !== "pending_review" && m.frontmatter.status !== "rejected" && m.frontmatter.status !== "quarantined" && m.frontmatter.status !== "superseded" && m.frontmatter.status !== "archived"
7553
+ ).map((m) => ({ m, score: scoreProcedureForPrompt(m, trimmed, queryIntent) })).filter((x) => x.score > 0.04).sort((a, b) => b.score - a.score).slice(0, maxN);
7554
+ if (scored.length === 0) return null;
7555
+ const blocks = scored.map(({ m, score }) => {
7556
+ const id = m.frontmatter.id;
7557
+ const flat = m.content.replace(/\s+/g, " ").trim();
7558
+ const preview = flat.slice(0, 320);
7559
+ const suffix = flat.length > 320 ? "\u2026" : "";
7560
+ return `### ${id} (match ${score.toFixed(2)})
7561
+
7562
+ ${preview}${suffix}`;
7563
+ });
7564
+ return `## Relevant procedures
7565
+
7566
+ ${blocks.join("\n\n")}`;
7567
+ }
7568
+
7180
7569
  // ../remnic-core/src/reconstruct.ts
7181
7570
  function findUnresolvedEntityRefs(recalledSnippets, recalledEntityRefs, knownEntities) {
7182
7571
  const refSet = new Set(recalledEntityRefs.map((r) => r.toLowerCase()));
@@ -7380,8 +7769,13 @@ async function scanDir(dir) {
7380
7769
  async function scanMemoryDir(memoryDir) {
7381
7770
  const factsDir = path4.join(memoryDir, "facts");
7382
7771
  const correctionsDir = path4.join(memoryDir, "corrections");
7383
- const [facts, corrections] = await Promise.all([scanDir(factsDir), scanDir(correctionsDir)]);
7384
- 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];
7385
7779
  }
7386
7780
 
7387
7781
  // ../remnic-core/src/search/lancedb-backend.ts
@@ -8216,6 +8610,23 @@ var EmbedHelper = class {
8216
8610
  import { createHash as createHash3 } from "crypto";
8217
8611
  import os2 from "os";
8218
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
8219
8630
  var QMD_TIMEOUT_MS = 3e4;
8220
8631
  var QMD_DAEMON_TIMEOUT_MS = 8e3;
8221
8632
  var QMD_PROBE_TIMEOUT_MS = 8e3;
@@ -8246,14 +8657,6 @@ function getGlobalQmdState() {
8246
8657
  }
8247
8658
  return g[QMD_GLOBAL_STATE_KEY];
8248
8659
  }
8249
- function abortError(message) {
8250
- const err = new Error(message);
8251
- Object.defineProperty(err, "name", { value: "AbortError" });
8252
- return err;
8253
- }
8254
- function isAbortError(err) {
8255
- return err instanceof Error && err.name === "AbortError";
8256
- }
8257
8660
  function errorMessage(err) {
8258
8661
  if (typeof err === "string") return err;
8259
8662
  if (err instanceof Error) return err.message;
@@ -8274,11 +8677,6 @@ function isCallerCancellation(err, signal) {
8274
8677
  function isDaemonTimeoutError(err) {
8275
8678
  return /timed out/i.test(errorMessage(err));
8276
8679
  }
8277
- function throwIfAborted(signal, message = "operation aborted") {
8278
- if (signal?.aborted) {
8279
- throw abortError(message);
8280
- }
8281
- }
8282
8680
  function sleepWithSignal(ms, signal) {
8283
8681
  return new Promise((resolve, reject) => {
8284
8682
  throwIfAborted(signal);
@@ -8714,6 +9112,26 @@ var QmdDaemonSession = class {
8714
9112
  this.buffer = "";
8715
9113
  }
8716
9114
  };
9115
+ var QMD_RESULT_LINE_RE = /^#([0-9a-fA-F]+)\s+(\d+)%\s+(.+)/;
9116
+ var QMD_PATH_TITLE_RE = /^(.+?\.[a-zA-Z]{2,10})\s+-\s+(.*)$/;
9117
+ function parseQmdMarkdownResultText(text, transport) {
9118
+ const results = [];
9119
+ for (const line of text.split("\n")) {
9120
+ const m = QMD_RESULT_LINE_RE.exec(line.trim());
9121
+ if (!m) continue;
9122
+ const rest = m[3];
9123
+ const pathTitleSplit = QMD_PATH_TITLE_RE.exec(rest);
9124
+ if (!pathTitleSplit) continue;
9125
+ results.push({
9126
+ docid: m[1],
9127
+ path: pathTitleSplit[1] ?? "unknown",
9128
+ snippet: "",
9129
+ score: parseInt(m[2], 10) / 100,
9130
+ transport
9131
+ });
9132
+ }
9133
+ return results;
9134
+ }
8717
9135
  function parseMcpSearchResult(result, transport = "daemon") {
8718
9136
  const resultObj = result;
8719
9137
  if (!resultObj) return [];
@@ -8746,6 +9164,15 @@ function parseMcpSearchResult(result, transport = "daemon") {
8746
9164
  const textResults = parsed?.results ?? parsed?.documents;
8747
9165
  if (Array.isArray(textResults)) pushDocs(textResults);
8748
9166
  } catch {
9167
+ const existingKeys = new Set(results.map((r) => `${r.docid.toLowerCase()}|${r.path}`));
9168
+ const parsed = parseQmdMarkdownResultText(item.text, transport);
9169
+ for (const p of parsed) {
9170
+ const key = `${p.docid.toLowerCase()}|${p.path}`;
9171
+ if (!existingKeys.has(key)) {
9172
+ results.push(p);
9173
+ existingKeys.add(key);
9174
+ }
9175
+ }
8749
9176
  }
8750
9177
  }
8751
9178
  }
@@ -8801,9 +9228,22 @@ var QmdClient = class _QmdClient {
8801
9228
  collection;
8802
9229
  maxResults;
8803
9230
  available = null;
8804
- lastUpdateFailAtMs = null;
9231
+ _lastUpdateFailAtMs = null;
8805
9232
  lastEmbedFailAtMs = null;
8806
9233
  lastUpdateRunAtMs = null;
9234
+ get lastUpdateFailedAtMs() {
9235
+ return this._lastUpdateFailAtMs;
9236
+ }
9237
+ get lastUpdateRanAtMs() {
9238
+ return this.lastUpdateRunAtMs;
9239
+ }
9240
+ resetUpdateThrottles() {
9241
+ this._lastUpdateFailAtMs = null;
9242
+ this.lastUpdateRunAtMs = null;
9243
+ const gs = getGlobalQmdState();
9244
+ gs.lastGlobalUpdateRunAtMs = null;
9245
+ gs.lastGlobalUpdateFailAtMs = null;
9246
+ }
8807
9247
  updateTimeoutMs;
8808
9248
  updateMinIntervalMs;
8809
9249
  slowLog;
@@ -9420,13 +9860,13 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9420
9860
  return [];
9421
9861
  }
9422
9862
  }
9423
- async update() {
9424
- await this.runUpdateForCollection(this.collection, { perCollectionThrottle: false });
9863
+ async update(signal) {
9864
+ await this.runUpdateForCollection(this.collection, { perCollectionThrottle: false }, signal);
9425
9865
  }
9426
9866
  async updateCollection(collection) {
9427
9867
  await this.runUpdateForCollection(collection, { perCollectionThrottle: true });
9428
9868
  }
9429
- async runUpdateForCollection(collection, options) {
9869
+ async runUpdateForCollection(collection, options, signal) {
9430
9870
  if (this.available === false) return;
9431
9871
  const name = collection.trim();
9432
9872
  if (!name) return;
@@ -9452,7 +9892,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9452
9892
  log.debug("QMD update: suppressed due to min-interval gate");
9453
9893
  return;
9454
9894
  }
9455
- if (this.lastUpdateFailAtMs && now - this.lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
9895
+ if (this._lastUpdateFailAtMs && now - this._lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
9456
9896
  log.debug("QMD update: suppressed due to recent failures (backoff)");
9457
9897
  return;
9458
9898
  }
@@ -9473,7 +9913,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9473
9913
  );
9474
9914
  }
9475
9915
  const startedAtMs = Date.now();
9476
- await this.runQmdCommand(["update", "-c", name], this.updateTimeoutMs);
9916
+ await this.runQmdCommand(["update", "-c", name], this.updateTimeoutMs, signal);
9477
9917
  const durationMs = Date.now() - startedAtMs;
9478
9918
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
9479
9919
  log.warn(`SLOW QMD update: durationMs=${durationMs}`);
@@ -9493,7 +9933,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9493
9933
  globalState.lastUpdateFailByCollectionMs[name] = at;
9494
9934
  globalState.lastGlobalUpdateFailAtMs = at;
9495
9935
  } else {
9496
- this.lastUpdateFailAtMs = at;
9936
+ this._lastUpdateFailAtMs = at;
9497
9937
  globalState.lastGlobalUpdateFailAtMs = at;
9498
9938
  }
9499
9939
  const msg = err instanceof Error ? err.message : String(err);
@@ -12622,6 +13062,8 @@ import { mkdir as mkdir8, readFile as readFile10, rename, rm as rm2, stat as sta
12622
13062
  import path15 from "path";
12623
13063
  var DAY_SUMMARY_CRON_ID = "engram-day-summary";
12624
13064
  var GOVERNANCE_CRON_ID = "engram-nightly-governance";
13065
+ var PROCEDURAL_MINING_CRON_ID = "engram-procedural-mining";
13066
+ var CONTRADICTION_SCAN_CRON_ID = "engram-contradiction-scan";
12625
13067
  async function acquireCronJobsLock(jobsPath) {
12626
13068
  const lockPath2 = `${jobsPath}.lock`;
12627
13069
  const start = Date.now();
@@ -12734,13 +13176,61 @@ async function ensureNightlyGovernanceCron(jobsPath, options) {
12734
13176
  delivery: { mode: "none" }
12735
13177
  }));
12736
13178
  }
13179
+ async function ensureProceduralMiningCron(jobsPath, options) {
13180
+ const scheduleExpr = typeof options.scheduleExpr === "string" && options.scheduleExpr.trim().length > 0 ? options.scheduleExpr.trim() : "17 3 * * *";
13181
+ const agentId = typeof options.agentId === "string" && options.agentId.trim().length > 0 ? options.agentId.trim() : "main";
13182
+ return ensureCronJob(jobsPath, PROCEDURAL_MINING_CRON_ID, () => ({
13183
+ id: PROCEDURAL_MINING_CRON_ID,
13184
+ agentId,
13185
+ name: "Remnic Procedural Mining (nightly)",
13186
+ enabled: true,
13187
+ schedule: {
13188
+ kind: "cron",
13189
+ expr: scheduleExpr,
13190
+ tz: options.timezone
13191
+ },
13192
+ sessionTarget: "isolated",
13193
+ wakeMode: "now",
13194
+ payload: {
13195
+ kind: "agentTurn",
13196
+ timeoutSeconds: 900,
13197
+ thinking: "off",
13198
+ message: "You are OpenClaw automation. Call tool `engram.procedure_mining_run` with empty params. If successful output exactly NO_REPLY. On error output one concise line. Do NOT use message tool."
13199
+ },
13200
+ delivery: { mode: "none" }
13201
+ }));
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
+ }
12737
13227
 
12738
13228
  // ../remnic-core/src/lifecycle.ts
12739
13229
  var DEFAULT_POLICY = {
12740
13230
  promoteHeatThreshold: 0.55,
12741
13231
  staleDecayThreshold: 0.65,
12742
13232
  archiveDecayThreshold: 0.85,
12743
- protectedCategories: ["decision", "principle", "commitment", "preference"]
13233
+ protectedCategories: ["decision", "principle", "commitment", "preference", "procedure"]
12744
13234
  };
12745
13235
  function clamp01(value) {
12746
13236
  if (!Number.isFinite(value)) return 0;
@@ -14497,6 +14987,14 @@ function clampGraphRecallExpandedEntries(entries, maxEntries = 64) {
14497
14987
  };
14498
14988
  }).filter((item) => item.path.length > 0 && item.namespace.length > 0).slice(0, limit);
14499
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
+ }
14500
14998
  var DEFAULT_TIER_MIGRATION_STATUS = {
14501
14999
  updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
14502
15000
  lastCycle: null,
@@ -14527,13 +15025,17 @@ var LastRecallStore = class {
14527
15025
  }
14528
15026
  }
14529
15027
  get(sessionKey) {
14530
- return this.state[sessionKey] ?? null;
15028
+ return cloneLastRecallSnapshot(this.state[sessionKey] ?? null);
14531
15029
  }
14532
15030
  getMostRecent() {
14533
15031
  const snapshots = Object.values(this.state);
14534
15032
  if (snapshots.length === 0) return null;
14535
- snapshots.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
14536
- 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);
14537
15039
  }
14538
15040
  /**
14539
15041
  * Persist last-recall snapshot and append an impression log entry.
@@ -14542,7 +15044,7 @@ var LastRecallStore = class {
14542
15044
  async record(opts) {
14543
15045
  const now = (/* @__PURE__ */ new Date()).toISOString();
14544
15046
  const queryHash = createHash4("sha256").update(opts.query).digest("hex");
14545
- const snapshot = {
15047
+ const liveSnapshot = {
14546
15048
  sessionKey: opts.sessionKey,
14547
15049
  recordedAt: now,
14548
15050
  queryHash,
@@ -14554,15 +15056,17 @@ var LastRecallStore = class {
14554
15056
  requestedMode: opts.requestedMode,
14555
15057
  source: opts.source,
14556
15058
  fallbackUsed: opts.fallbackUsed,
14557
- sourcesUsed: opts.sourcesUsed ? [...opts.sourcesUsed] : void 0,
14558
- budgetsApplied: opts.budgetsApplied ? { ...opts.budgetsApplied } : void 0,
15059
+ sourcesUsed: opts.sourcesUsed,
15060
+ budgetsApplied: opts.budgetsApplied,
14559
15061
  latencyMs: opts.latencyMs,
14560
- resultPaths: opts.resultPaths ? [...opts.resultPaths] : void 0,
15062
+ resultPaths: opts.resultPaths,
14561
15063
  policyVersion: opts.policyVersion,
14562
15064
  identityInjectionMode: opts.identityInjection?.mode,
14563
15065
  identityInjectedChars: opts.identityInjection?.injectedChars,
14564
- identityInjectionTruncated: opts.identityInjection?.truncated
15066
+ identityInjectionTruncated: opts.identityInjection?.truncated,
15067
+ tierExplain: opts.tierExplain
14565
15068
  };
15069
+ const snapshot = cloneLastRecallSnapshot(liveSnapshot);
14566
15070
  this.state[opts.sessionKey] = snapshot;
14567
15071
  const keys = Object.keys(this.state);
14568
15072
  if (keys.length > 50) {
@@ -14586,6 +15090,31 @@ var LastRecallStore = class {
14586
15090
  }
14587
15091
  }
14588
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
+ }
14589
15118
  };
14590
15119
  var TierMigrationStatusStore = class {
14591
15120
  statePath;
@@ -18168,97 +18697,6 @@ async function readRecentEntityTranscriptEntries(transcriptEntriesPromise, recen
18168
18697
  }
18169
18698
  var entityRecentTranscriptLookbackHours = RECENT_TRANSCRIPT_LOOKBACK_HOURS;
18170
18699
 
18171
- // ../remnic-core/src/intent.ts
18172
- var GOAL_PATTERNS = [
18173
- { re: /\b(debug(?:s|ged|ging)?|fix(?:es|ed|ing)?|error(?:s)?|incident(?:s)?|outage(?:s)?|failure(?:s)?)\b/i, goal: "stabilize" },
18174
- { re: /\b(deploy(?:s|ed|ing)?|release(?:s|d|ing)?|ship(?:s|ped|ping)?|publish(?:es|ed|ing)?)\b/i, goal: "release" },
18175
- { re: /\b(plan(?:s|ned|ning)?|roadmap(?:s)?|strateg(?:y|ies)|design(?:s|ed|ing)?)\b/i, goal: "plan" },
18176
- { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|security|hardening)\b/i, goal: "review" },
18177
- { re: /\b(sales|deal|customer|client|prospect)\b/i, goal: "close_deal" }
18178
- ];
18179
- var ACTION_PATTERNS = [
18180
- { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|inspect(?:s|ed|ing)?|check(?:s|ed|ing)?)\b/i, action: "review" },
18181
- { re: /\b(plan(?:s|ned|ning)?|design(?:s|ed|ing)?|brainstorm(?:s|ed|ing)?|spec(?:s)?)\b/i, action: "plan" },
18182
- { re: /\b(implement(?:s|ed|ing)?|build(?:s|ing)?|built|code(?:s|d|ing)?|patch(?:es|ed|ing)?|fix(?:es|ed|ing)?)\b/i, action: "execute" },
18183
- { re: /\b(summariz(?:e|es|ed|ing)|recap(?:s|ped|ping)?|what happened|timeline)\b/i, action: "summarize" },
18184
- { re: /\b(decid(?:e|es|ed|ing)|decision(?:s)?|cho(?:ose|oses|osing)|chose|chosen)\b/i, action: "decide" }
18185
- ];
18186
- var ENTITY_PATTERNS = [
18187
- { re: /\b(pr|pull request|branch|repo|github|ci|workflow)\b/i, entityType: "repo" },
18188
- { re: /\b(discord|slack|channel|gateway|agent)\b/i, entityType: "ops" },
18189
- { re: /\b(customer|client|deal|lead|account)\b/i, entityType: "client" },
18190
- { re: /\b(model|llm|qmd|embedding|retrieval|memory)\b/i, entityType: "ai" },
18191
- { re: /\b(doc|readme|docs|changelog)\b/i, entityType: "docs" }
18192
- ];
18193
- function normalizeTextInput(input) {
18194
- return typeof input === "string" ? input : "";
18195
- }
18196
- function inferIntentFromText(text) {
18197
- const safeText = normalizeTextInput(text);
18198
- const goal = GOAL_PATTERNS.find((p) => p.re.test(safeText))?.goal ?? "unknown";
18199
- const actionType = ACTION_PATTERNS.find((p) => p.re.test(safeText))?.action ?? "unknown";
18200
- const entityTypes = Array.from(
18201
- new Set(ENTITY_PATTERNS.filter((p) => p.re.test(safeText)).map((p) => p.entityType))
18202
- );
18203
- return {
18204
- goal,
18205
- actionType,
18206
- entityTypes
18207
- };
18208
- }
18209
- function intentCompatibilityScore(queryIntent, memoryIntent) {
18210
- const queryHasSignal = queryIntent.goal !== "unknown" || queryIntent.actionType !== "unknown" || queryIntent.entityTypes.length > 0;
18211
- const memoryHasSignal = memoryIntent.goal !== "unknown" || memoryIntent.actionType !== "unknown" || memoryIntent.entityTypes.length > 0;
18212
- if (!queryHasSignal || !memoryHasSignal) return 0;
18213
- let score = 0;
18214
- if (queryIntent.goal !== "unknown" && memoryIntent.goal !== "unknown" && queryIntent.goal === memoryIntent.goal) {
18215
- score += 0.5;
18216
- }
18217
- if (queryIntent.actionType !== "unknown" && memoryIntent.actionType !== "unknown" && queryIntent.actionType === memoryIntent.actionType) {
18218
- score += 0.3;
18219
- }
18220
- const overlap = queryIntent.entityTypes.filter((et) => memoryIntent.entityTypes.includes(et)).length;
18221
- if (overlap > 0) {
18222
- const denom = Math.max(queryIntent.entityTypes.length, memoryIntent.entityTypes.length, 1);
18223
- score += 0.2 * (overlap / denom);
18224
- }
18225
- return Math.max(0, Math.min(1, score));
18226
- }
18227
- function planRecallMode(prompt) {
18228
- const p = normalizeTextInput(prompt).trim();
18229
- let ackCandidate = p;
18230
- while (ackCandidate.length > 0) {
18231
- const ch = ackCandidate.charCodeAt(ackCandidate.length - 1);
18232
- const isDigit = ch >= 48 && ch <= 57;
18233
- const isUpper = ch >= 65 && ch <= 90;
18234
- const isLower = ch >= 97 && ch <= 122;
18235
- if (isDigit || isUpper || isLower) break;
18236
- ackCandidate = ackCandidate.slice(0, -1);
18237
- }
18238
- ackCandidate = ackCandidate.trim();
18239
- if (p.length === 0) return "no_recall";
18240
- if (/\b(timeline|sequence|history|what happened|chain of events|root cause)\b/i.test(p)) {
18241
- return "graph_mode";
18242
- }
18243
- if (p.length <= 18 && /^(ok|okay|kk|thanks|thx|got it|sounds good|yep|yes|nope|no|done|cool|works)$/i.test(ackCandidate)) {
18244
- return "no_recall";
18245
- }
18246
- if (/\b(previous|earlier|remember|last time|did we|what did we decide|context|summarize|summary|recap|key points|decision)\b/i.test(p) || /\?$/.test(p) || /^(what|why|how|when|where|who|which)\b/i.test(p.toLowerCase())) {
18247
- return "full";
18248
- }
18249
- if (p.length <= 100 && /^(check|reload|restart|run|verify|show|status|sync|update|open|close|set|enable|disable|fix|patch)\b/i.test(p)) {
18250
- return "minimal";
18251
- }
18252
- return "full";
18253
- }
18254
- function hasBroadGraphIntent(prompt) {
18255
- const p = normalizeTextInput(prompt).trim().toLowerCase();
18256
- if (!p) return false;
18257
- return /\b(what changed|how did we get here|why did this happen|what led to|cause chain|dependency chain|regression chain|failure chain)\b/i.test(
18258
- p
18259
- );
18260
- }
18261
-
18262
18700
  // ../remnic-core/src/recall-query-policy.ts
18263
18701
  var DEFAULT_STOPWORDS = /* @__PURE__ */ new Set([
18264
18702
  "the",
@@ -18553,11 +18991,11 @@ function computeCompressionGuidelineCandidate(events, options = {}) {
18553
18991
  notes
18554
18992
  };
18555
18993
  }
18556
- const successRate = summary.outcomes.applied / summary.total;
18994
+ const successRate2 = summary.outcomes.applied / summary.total;
18557
18995
  const failureRate = summary.outcomes.failed / summary.total;
18558
18996
  const qualitySeen = summary.quality.good + summary.quality.poor;
18559
18997
  const qualitySignal = qualitySeen > 0 ? (summary.quality.good - summary.quality.poor) / qualitySeen : 0;
18560
- const rawDelta = clamp((successRate - failureRate) * 0.12 + qualitySignal * 0.06, -MAX_DELTA, MAX_DELTA);
18998
+ const rawDelta = clamp((successRate2 - failureRate) * 0.12 + qualitySignal * 0.06, -MAX_DELTA, MAX_DELTA);
18561
18999
  const delta = roundDelta(rawDelta);
18562
19000
  const direction = directionForDelta(delta);
18563
19001
  if (direction === "decrease" && summary.outcomes.failed > summary.outcomes.applied) {
@@ -20261,13 +20699,13 @@ async function readCueAnchors(options) {
20261
20699
  return anchors;
20262
20700
  }
20263
20701
  async function searchHarmonicRetrieval(options) {
20264
- throwIfAborted2(options.abortSignal);
20702
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20265
20703
  const queryTokens = new Set(normalizeRecallTokens(options.query, ["what", "which"]));
20266
20704
  if (queryTokens.size === 0 || options.maxResults <= 0) return [];
20267
20705
  const nodes = await readAbstractionNodes(options);
20268
20706
  const candidates = /* @__PURE__ */ new Map();
20269
20707
  for (const node of nodes) {
20270
- throwIfAborted2(options.abortSignal);
20708
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20271
20709
  const { score, matchedFields } = scoreNode(node, queryTokens);
20272
20710
  if (score <= 0) continue;
20273
20711
  candidates.set(node.nodeId, {
@@ -20279,11 +20717,11 @@ async function searchHarmonicRetrieval(options) {
20279
20717
  });
20280
20718
  }
20281
20719
  if (options.anchorsEnabled) {
20282
- throwIfAborted2(options.abortSignal);
20720
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20283
20721
  const anchors = await readCueAnchors(options);
20284
20722
  const nodeIndex = new Map(nodes.map((node) => [node.nodeId, node]));
20285
20723
  for (const anchor of anchors) {
20286
- throwIfAborted2(options.abortSignal);
20724
+ throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
20287
20725
  const { score, matchedFields } = scoreAnchor(anchor, queryTokens);
20288
20726
  if (score <= 0) continue;
20289
20727
  for (const nodeRef of anchor.nodeRefs) {
@@ -20325,12 +20763,6 @@ async function searchHarmonicRetrieval(options) {
20325
20763
  (left, right) => right.score - left.score || right.anchorScore - left.anchorScore || right.node.recordedAt.localeCompare(left.node.recordedAt)
20326
20764
  ).slice(0, options.maxResults);
20327
20765
  }
20328
- function throwIfAborted2(signal) {
20329
- if (!signal?.aborted) return;
20330
- const err = new Error("harmonic retrieval aborted");
20331
- Object.defineProperty(err, "name", { value: "AbortError" });
20332
- throw err;
20333
- }
20334
20766
 
20335
20767
  // ../remnic-core/src/verified-recall.ts
20336
20768
  function createReadOnlyBoxBuilder(memoryDir) {
@@ -20873,14 +21305,75 @@ async function getWorkProductLedgerStatus(options) {
20873
21305
  };
20874
21306
  }
20875
21307
 
21308
+ // ../remnic-core/src/utils/iso-timestamp.ts
21309
+ var ISO_UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
21310
+ var ISO_OFFSET_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
21311
+ function validateDateComponents(isoString) {
21312
+ const match = isoString.match(
21313
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/
21314
+ );
21315
+ if (!match) return false;
21316
+ const [, yStr, mStr, dStr, hStr, minStr, sStr] = match;
21317
+ const y = Number(yStr);
21318
+ const m = Number(mStr);
21319
+ const d = Number(dStr);
21320
+ const h = Number(hStr);
21321
+ const min = Number(minStr);
21322
+ const s = Number(sStr);
21323
+ if (m < 1 || m > 12) return false;
21324
+ if (d < 1 || d > 31) return false;
21325
+ if (h > 23 || min > 59 || s > 59) return false;
21326
+ const daysInMonth = new Date(y, m, 0).getDate();
21327
+ if (d > daysInMonth) return false;
21328
+ return true;
21329
+ }
21330
+ function validateOffset(isoString) {
21331
+ const offsetMatch = isoString.match(/([+-])(\d{2}):(\d{2})$/);
21332
+ if (!offsetMatch) return true;
21333
+ const oh = Number(offsetMatch[2]);
21334
+ const om = Number(offsetMatch[3]);
21335
+ if (oh > 14 || om > 59) return false;
21336
+ if (oh === 14 && om > 0) return false;
21337
+ return true;
21338
+ }
21339
+ function normalizeUtcForComparison(value) {
21340
+ const fracMatch = value.match(/\.(\d+)Z$/);
21341
+ if (fracMatch) {
21342
+ const ms = (fracMatch[1] + "000").slice(0, 3);
21343
+ return value.replace(/\.\d+Z$/, `.${ms}Z`);
21344
+ }
21345
+ return value.replace(/Z$/, ".000Z");
21346
+ }
21347
+ function parseIsoUtcTimestamp(value) {
21348
+ if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_RE.test(value)) {
21349
+ return null;
21350
+ }
21351
+ const ts = Date.parse(value);
21352
+ if (!Number.isFinite(ts)) return null;
21353
+ if (!validateDateComponents(value)) return null;
21354
+ const roundTrip = new Date(ts).toISOString();
21355
+ if (roundTrip !== normalizeUtcForComparison(value)) return null;
21356
+ return ts;
21357
+ }
21358
+ function parseIsoOffsetTimestamp(value) {
21359
+ if (typeof value !== "string" || !ISO_OFFSET_TIMESTAMP_RE.test(value)) {
21360
+ return null;
21361
+ }
21362
+ const ts = Date.parse(value);
21363
+ if (!Number.isFinite(ts)) return null;
21364
+ if (!validateDateComponents(value)) return null;
21365
+ if (!validateOffset(value)) return null;
21366
+ if (value.endsWith("Z")) {
21367
+ const roundTrip = new Date(ts).toISOString();
21368
+ if (roundTrip !== normalizeUtcForComparison(value)) return null;
21369
+ }
21370
+ return ts;
21371
+ }
21372
+
20876
21373
  // ../remnic-core/src/replay/types.ts
20877
21374
  var VALID_SOURCES = /* @__PURE__ */ new Set(["openclaw", "claude", "chatgpt"]);
20878
21375
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
20879
- var ISO_UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
20880
21376
  var REPLAY_UNKNOWN_SESSION_KEY = "replay:unknown";
20881
- function normalizeIsoForComparison(value) {
20882
- return value.includes(".") ? value : value.replace("Z", ".000Z");
20883
- }
20884
21377
  function isReplaySource(value) {
20885
21378
  return typeof value === "string" && VALID_SOURCES.has(value);
20886
21379
  }
@@ -20893,12 +21386,7 @@ function normalizeReplaySessionKey(value) {
20893
21386
  return trimmed.length > 0 ? trimmed : REPLAY_UNKNOWN_SESSION_KEY;
20894
21387
  }
20895
21388
  function parseIsoTimestamp(value) {
20896
- if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_RE.test(value)) return null;
20897
- const ts = Date.parse(value);
20898
- if (!Number.isFinite(ts)) return null;
20899
- const roundTrip = new Date(ts).toISOString();
20900
- if (roundTrip !== normalizeIsoForComparison(value)) return null;
20901
- return ts;
21389
+ return parseIsoUtcTimestamp(value);
20902
21390
  }
20903
21391
  function validateReplayTurn(turn, index) {
20904
21392
  const issues = [];
@@ -24961,7 +25449,8 @@ var DEFAULT_CATEGORIES = [
24961
25449
  "commitment",
24962
25450
  "moment",
24963
25451
  "skill",
24964
- "rule"
25452
+ "rule",
25453
+ "procedure"
24965
25454
  ];
24966
25455
  function normalizeNamespace(namespace) {
24967
25456
  return namespace.trim();
@@ -25066,7 +25555,8 @@ var INLINE_ALLOWED_CATEGORIES = /* @__PURE__ */ new Set([
25066
25555
  "commitment",
25067
25556
  "moment",
25068
25557
  "skill",
25069
- "rule"
25558
+ "rule",
25559
+ "procedure"
25070
25560
  ]);
25071
25561
  var SECRET_PATTERNS = [
25072
25562
  /\bsk-[A-Za-z0-9]{16,}\b/,
@@ -25665,15 +26155,24 @@ var NamespaceSearchRouter = class {
25665
26155
  );
25666
26156
  return mergeNamespaceSearchResults(resultsByNamespace, maxResults);
25667
26157
  }
26158
+ /**
26159
+ * Update all namespace backends.
26160
+ * Returns the number of backends for which an update was attempted
26161
+ * (i.e., available and collection present). Callers can treat 0 as a
26162
+ * signal that no backend was eligible — useful for success-verification in
26163
+ * startup-sync when namespacesEnabled is true.
26164
+ */
25668
26165
  async updateNamespaces(namespaces) {
25669
26166
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
25670
- await Promise.all(
26167
+ const results = await Promise.all(
25671
26168
  unique.map(async (namespace) => {
25672
26169
  const record = await this.backendRecordFor(namespace);
25673
- if (!record.available || record.collectionState === "missing") return;
26170
+ if (!record.available || record.collectionState === "missing") return 0;
25674
26171
  await record.backend.update();
26172
+ return 1;
25675
26173
  })
25676
26174
  );
26175
+ return results.reduce((sum, v) => sum + v, 0);
25677
26176
  }
25678
26177
  async embedNamespaces(namespaces) {
25679
26178
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
@@ -25689,6 +26188,10 @@ var NamespaceSearchRouter = class {
25689
26188
  const record = await this.backendRecordFor(namespace);
25690
26189
  return record.collectionState;
25691
26190
  }
26191
+ /** Clear cached backend records so the next access re-probes availability. */
26192
+ clearCache() {
26193
+ this.cache.clear();
26194
+ }
25692
26195
  async backendRecordFor(namespace) {
25693
26196
  const key = namespace.trim() || this.config.defaultNamespace;
25694
26197
  const existing = this.cache.get(key);
@@ -26846,15 +27349,9 @@ function fingerprintEntitySynthesisEvidence(entity) {
26846
27349
  fingerprint.update(fingerprintEntityStructuredFacts(entity) ?? "");
26847
27350
  return fingerprint.digest("hex");
26848
27351
  }
26849
- function abortRecallError(message) {
26850
- const err = new Error(message);
26851
- Object.defineProperty(err, "name", { value: "AbortError" });
26852
- return err;
26853
- }
27352
+ var abortRecallError = abortError;
26854
27353
  function throwIfRecallAborted(signal, message = "recall aborted") {
26855
- if (signal?.aborted) {
26856
- throw abortRecallError(message);
26857
- }
27354
+ throwIfAborted(signal, message);
26858
27355
  }
26859
27356
  async function raceRecallAbort(promise, signal, message = "recall aborted") {
26860
27357
  throwIfRecallAborted(signal, message);
@@ -27108,7 +27605,8 @@ function parseMemoryIntentSnapshot(value) {
27108
27605
  actionType: typeof candidate.actionType === "string" ? candidate.actionType : "unknown",
27109
27606
  entityTypes: Array.isArray(candidate.entityTypes) ? candidate.entityTypes.filter(
27110
27607
  (item) => typeof item === "string"
27111
- ) : []
27608
+ ) : [],
27609
+ taskInitiation: candidate.taskInitiation === true
27112
27610
  };
27113
27611
  }
27114
27612
  function buildQmdIntentHint(intent) {
@@ -27122,6 +27620,9 @@ function buildQmdIntentHint(intent) {
27122
27620
  if (intent.entityTypes.length > 0) {
27123
27621
  parts.push(`entities:${intent.entityTypes.join(",")}`);
27124
27622
  }
27623
+ if (intent.taskInitiation === true) {
27624
+ parts.push("task_initiation");
27625
+ }
27125
27626
  return parts.length > 0 ? parts.join(" ") : void 0;
27126
27627
  }
27127
27628
  function parseQmdRecallResults(value) {
@@ -27297,6 +27798,41 @@ var Orchestrator = class _Orchestrator {
27297
27798
  // Initialization gate: recall() awaits this before proceeding
27298
27799
  initPromise = null;
27299
27800
  resolveInit = null;
27801
+ /**
27802
+ * Resolves when deferred initialization (QMD probe, warmup, caches, cron)
27803
+ * completes. CLI and http-serve callers that need `qmd.isAvailable()` to
27804
+ * reflect reality should `await orchestrator.deferredReady` after
27805
+ * `initialize()`. Gateway callers can ignore it — recall() degrades
27806
+ * gracefully when QMD isn't ready yet.
27807
+ *
27808
+ * Also resolves (without error) when `initialize()` throws before reaching
27809
+ * the deferred-init phase, so callers never hang on a permanently-pending
27810
+ * promise.
27811
+ *
27812
+ * Host adapters that need to tie deferred init to their stop() lifecycle
27813
+ * should `await orchestrator.deferredReady` before proceeding with teardown
27814
+ * to prevent background QMD/warmup/cron tasks from racing with shutdown.
27815
+ */
27816
+ deferredReady = Promise.resolve();
27817
+ resolveDeferredReady = null;
27818
+ deferredInitAbort = null;
27819
+ /**
27820
+ * Whether the deferred init's QMD startup sync completed successfully.
27821
+ * When false after deferredReady resolves, the server retry loop should
27822
+ * attempt startupSearchSync() even if `qmd.isAvailable()` is true —
27823
+ * availability only means probe succeeded, not that the index is current.
27824
+ */
27825
+ deferredSyncSucceeded = false;
27826
+ /**
27827
+ * Abort deferred initialization so background QMD sync/warmup stops
27828
+ * promptly on shutdown. Safe to call multiple times or before init.
27829
+ */
27830
+ abortDeferredInit() {
27831
+ if (this.deferredInitAbort) {
27832
+ this.deferredInitAbort.abort();
27833
+ this.deferredInitAbort = null;
27834
+ }
27835
+ }
27300
27836
  /** Set per-session workspace for the next recall() call (compaction reset). @internal */
27301
27837
  setRecallWorkspaceOverride(sessionKey, dir) {
27302
27838
  this._recallWorkspaceOverrides.set(sessionKey, dir);
@@ -27449,6 +27985,7 @@ var Orchestrator = class _Orchestrator {
27449
27985
  );
27450
27986
  this.judgeVerdictCache = createVerdictCache();
27451
27987
  this.localLlm = new LocalLlmClient(config, this.modelRegistry);
27988
+ this.localLlm.disableThinking = config.localLlmDisableThinking;
27452
27989
  this.fastLlm = config.localLlmFastEnabled ? (() => {
27453
27990
  const client = new LocalLlmClient(
27454
27991
  {
@@ -27687,100 +28224,173 @@ var Orchestrator = class _Orchestrator {
27687
28224
  return this.fastLlm;
27688
28225
  }
27689
28226
  async initialize() {
27690
- await migrateFromEngram({
27691
- quiet: true,
27692
- logger: (message) => log.info(message)
28227
+ this.deferredReady = new Promise((resolve) => {
28228
+ this.resolveDeferredReady = resolve;
27693
28229
  });
27694
- await this.storage.ensureDirectories();
27695
- await this.storage.loadAliases();
27696
- if (this.config.namespacesEnabled) {
27697
- const namespaces = /* @__PURE__ */ new Set([
27698
- this.config.defaultNamespace,
27699
- this.config.sharedNamespace,
27700
- ...this.config.namespacePolicies.map((p) => p.name)
27701
- ]);
27702
- for (const ns of namespaces) {
27703
- const sm = await this.storageRouter.storageFor(ns);
27704
- await sm.ensureDirectories();
27705
- await sm.loadAliases().catch(() => void 0);
28230
+ try {
28231
+ await migrateFromEngram({
28232
+ quiet: true,
28233
+ logger: (message) => log.info(message)
28234
+ });
28235
+ await this.storage.ensureDirectories();
28236
+ await this.storage.loadAliases();
28237
+ if (this.config.namespacesEnabled) {
28238
+ const namespaces = /* @__PURE__ */ new Set([
28239
+ this.config.defaultNamespace,
28240
+ this.config.sharedNamespace,
28241
+ ...this.config.namespacePolicies.map((p) => p.name)
28242
+ ]);
28243
+ for (const ns of namespaces) {
28244
+ const sm = await this.storageRouter.storageFor(ns);
28245
+ await sm.ensureDirectories();
28246
+ await sm.loadAliases().catch(() => void 0);
28247
+ }
28248
+ }
28249
+ await this.relevance.load();
28250
+ await this.negatives.load();
28251
+ await this.lastRecall.load();
28252
+ await this.tierMigrationStatus.load();
28253
+ await this.sessionObserver.load();
28254
+ this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
28255
+ this.utilityRuntimeValues = await loadUtilityRuntimeValues({
28256
+ memoryDir: this.config.memoryDir,
28257
+ memoryUtilityLearningEnabled: this.config.memoryUtilityLearningEnabled,
28258
+ promotionByOutcomeEnabled: this.config.promotionByOutcomeEnabled
28259
+ });
28260
+ if (this.config.factDeduplicationEnabled) {
28261
+ const stateDir2 = path43.join(this.config.memoryDir, "state");
28262
+ this.contentHashIndex = new ContentHashIndex(stateDir2);
28263
+ await this.contentHashIndex.load();
28264
+ log.info(
28265
+ `content-hash dedup: loaded ${this.contentHashIndex.size} hashes`
28266
+ );
27706
28267
  }
27707
- }
27708
- await this.relevance.load();
27709
- await this.negatives.load();
27710
- await this.lastRecall.load();
27711
- await this.tierMigrationStatus.load();
27712
- await this.sessionObserver.load();
27713
- this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
27714
- this.utilityRuntimeValues = await loadUtilityRuntimeValues({
27715
- memoryDir: this.config.memoryDir,
27716
- memoryUtilityLearningEnabled: this.config.memoryUtilityLearningEnabled,
27717
- promotionByOutcomeEnabled: this.config.promotionByOutcomeEnabled
27718
- });
27719
- if (this.config.factDeduplicationEnabled) {
27720
- const stateDir2 = path43.join(this.config.memoryDir, "state");
27721
- this.contentHashIndex = new ContentHashIndex(stateDir2);
27722
- await this.contentHashIndex.load();
27723
- log.info(
27724
- `content-hash dedup: loaded ${this.contentHashIndex.size} hashes`
27725
- );
27726
- }
27727
- await this.transcript.initialize();
27728
- await this.summarizer.initialize();
27729
- if (this.sharedContext) {
27730
- await this.sharedContext.ensureStructure();
27731
- }
27732
- if (this.compounding) {
27733
- await this.compounding.ensureDirs();
27734
- }
27735
- if (this.resolveInit) {
27736
- this.resolveInit();
27737
- this.resolveInit = null;
27738
- log.info("init gate opened (essential state loaded)");
27739
- }
27740
- {
27741
- const available = await this.qmd.probe();
27742
- if (available) {
27743
- log.info(`Search backend: available ${this.qmd.debugStatus()}`);
27744
- const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
27745
- const states = await Promise.all(
27746
- namespaces.map(async (namespace) => ({
27747
- namespace,
27748
- state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(
27749
- namespace
27750
- ) : await this.qmd.ensureCollection(this.config.memoryDir)
27751
- }))
28268
+ await this.transcript.initialize();
28269
+ await this.summarizer.initialize();
28270
+ if (this.sharedContext) {
28271
+ await this.sharedContext.ensureStructure();
28272
+ }
28273
+ if (this.compounding) {
28274
+ await this.compounding.ensureDirs();
28275
+ }
28276
+ try {
28277
+ await this.buffer.load();
28278
+ } catch (bufErr) {
28279
+ log.error(
28280
+ `buffer.load() failed (init gate will still open): ${bufErr}`
27752
28281
  );
27753
- const defaultState2 = states.find(
27754
- (entry) => entry.namespace === this.config.defaultNamespace
27755
- )?.state ?? "unknown";
27756
- if (defaultState2 === "missing") {
27757
- this.qmd = new NoopSearchBackend();
27758
- log.warn(
27759
- "Search collection missing for Remnic memory store; disabling search retrieval for this runtime (fallback retrieval remains enabled)"
27760
- );
27761
- } else if (defaultState2 === "unknown") {
27762
- log.warn(
27763
- "Search collection check unavailable; keeping search retrieval enabled for fail-open behavior"
27764
- );
27765
- } else if (defaultState2 === "skipped") {
27766
- log.debug(
27767
- "Search collection check skipped (remote or daemon-only mode)"
27768
- );
28282
+ this.buffer.resetToEmpty();
28283
+ }
28284
+ if (this.config.compactionResetEnabled) {
28285
+ try {
28286
+ const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
28287
+ const files = await readdir14(wsDir).catch(() => []);
28288
+ for (const f of files) {
28289
+ if (!f.startsWith(".compaction-reset-signal-")) continue;
28290
+ const fp = path43.join(wsDir, f);
28291
+ const s = await stat10(fp).catch(() => null);
28292
+ if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
28293
+ await unlink7(fp).catch(() => {
28294
+ });
28295
+ log.debug(`initialize: removed stale compaction signal ${f}`);
28296
+ }
28297
+ }
28298
+ } catch (err) {
28299
+ log.debug("initialize: stale signal sweep failed:", err);
27769
28300
  }
27770
- for (const entry of states) {
27771
- if (entry.namespace === this.config.defaultNamespace) continue;
27772
- if (entry.state === "missing") {
28301
+ }
28302
+ try {
28303
+ const available = await this.qmd.probe();
28304
+ if (available) {
28305
+ log.info(`Search backend: available ${this.qmd.debugStatus()}`);
28306
+ const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
28307
+ const states = await Promise.all(
28308
+ namespaces.map(async (namespace) => ({
28309
+ namespace,
28310
+ state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(
28311
+ namespace
28312
+ ) : await this.qmd.ensureCollection(this.config.memoryDir)
28313
+ }))
28314
+ );
28315
+ const defaultState2 = states.find(
28316
+ (entry) => entry.namespace === this.config.defaultNamespace
28317
+ )?.state ?? "unknown";
28318
+ if (defaultState2 === "missing") {
28319
+ this.qmd = new NoopSearchBackend();
27773
28320
  log.warn(
27774
- `Search collection missing for namespace '${entry.namespace}'; namespace retrieval will fail open to non-search paths`
28321
+ "Search collection missing for Remnic memory store; disabling search retrieval for this runtime (fallback retrieval remains enabled)"
27775
28322
  );
28323
+ } else if (defaultState2 === "unknown") {
28324
+ log.warn(
28325
+ "Search collection check unavailable; keeping search retrieval enabled for fail-open behavior"
28326
+ );
28327
+ } else if (defaultState2 === "skipped") {
28328
+ log.debug(
28329
+ "Search collection check skipped (remote or daemon-only mode)"
28330
+ );
28331
+ }
28332
+ for (const entry of states) {
28333
+ if (entry.namespace === this.config.defaultNamespace) continue;
28334
+ if (entry.state === "missing") {
28335
+ log.warn(
28336
+ `Search collection missing for namespace '${entry.namespace}'; namespace retrieval will fail open to non-search paths`
28337
+ );
28338
+ }
27776
28339
  }
28340
+ } else if (this.qmd instanceof NoopSearchBackend) {
28341
+ log.debug(`Search backend: noop (search intentionally disabled)`);
28342
+ } else {
28343
+ log.warn(`Search backend: not available ${this.qmd.debugStatus()}`);
27777
28344
  }
27778
- } else if (this.qmd instanceof NoopSearchBackend) {
27779
- log.debug(`Search backend: noop (search intentionally disabled)`);
27780
- } else {
27781
- log.warn(`Search backend: not available ${this.qmd.debugStatus()}`);
28345
+ } catch (err) {
28346
+ log.error(`QMD probe/collection check failed (non-fatal): ${err}`);
28347
+ }
28348
+ if (this.resolveInit) {
28349
+ this.resolveInit();
28350
+ this.resolveInit = null;
28351
+ log.info("init gate opened (essential state + QMD state loaded)");
28352
+ }
28353
+ const resolveDeferred = this.resolveDeferredReady;
28354
+ this.resolveDeferredReady = null;
28355
+ this.deferredInitAbort = new AbortController();
28356
+ this.deferredInitialize(this.deferredInitAbort.signal).catch((err) => {
28357
+ log.error(`deferred initialization failed (non-fatal): ${err}`);
28358
+ }).finally(() => {
28359
+ resolveDeferred?.();
28360
+ });
28361
+ } catch (err) {
28362
+ if (this.resolveInit) {
28363
+ this.resolveInit();
28364
+ this.resolveInit = null;
28365
+ }
28366
+ if (this.resolveDeferredReady) {
28367
+ this.resolveDeferredReady();
28368
+ this.resolveDeferredReady = null;
28369
+ }
28370
+ throw err;
28371
+ }
28372
+ }
28373
+ async deferredInitialize(signal) {
28374
+ if (this.qmd.isAvailable() && this.config.qmdMaintenanceEnabled) {
28375
+ try {
28376
+ log.info("QMD startup sync: updating index to match current disk state");
28377
+ if (this.config.namespacesEnabled) {
28378
+ await this.namespaceSearchRouter.updateNamespaces(
28379
+ this.configuredNamespaces()
28380
+ );
28381
+ } else {
28382
+ await this.qmd.update();
28383
+ }
28384
+ log.info("QMD startup sync: complete");
28385
+ this.deferredSyncSucceeded = true;
28386
+ } catch (err) {
28387
+ log.warn(`QMD startup sync failed (non-fatal): ${err}`);
27782
28388
  }
28389
+ } else if (!this.qmd.isAvailable()) {
28390
+ } else {
28391
+ this.deferredSyncSucceeded = true;
27783
28392
  }
28393
+ if (signal.aborted) return;
27784
28394
  const warmupPromises = [];
27785
28395
  if (this.qmd.isAvailable()) {
27786
28396
  const warmupNs = this.config.defaultNamespace;
@@ -27805,68 +28415,180 @@ var Orchestrator = class _Orchestrator {
27805
28415
  );
27806
28416
  }
27807
28417
  await Promise.all(warmupPromises);
28418
+ if (signal.aborted) return;
28419
+ const cacheWarmups = [];
27808
28420
  if (this.config.knowledgeIndexEnabled) {
27809
- (async () => {
27810
- try {
27811
- const t0 = Date.now();
27812
- await this.storage.buildKnowledgeIndex(this.config);
27813
- log.info(`Knowledge Index warmup: complete in ${Date.now() - t0}ms`);
27814
- } catch (err) {
27815
- log.debug(`Knowledge Index warmup failed (non-fatal): ${err}`);
27816
- }
27817
- })().catch(() => {
27818
- });
28421
+ cacheWarmups.push(
28422
+ (async () => {
28423
+ try {
28424
+ const t0 = Date.now();
28425
+ await this.storage.buildKnowledgeIndex(this.config);
28426
+ log.info(`Knowledge Index warmup: complete in ${Date.now() - t0}ms`);
28427
+ } catch (err) {
28428
+ log.debug(`Knowledge Index warmup failed (non-fatal): ${err}`);
28429
+ }
28430
+ })()
28431
+ );
27819
28432
  }
27820
- this.storage.readAllMemories().catch(() => {
27821
- });
27822
- this.storage.readAllEntityFiles().catch(() => {
27823
- });
28433
+ cacheWarmups.push(this.storage.readAllMemories().then(() => {
28434
+ }).catch(() => {
28435
+ }));
28436
+ cacheWarmups.push(this.storage.readAllEntityFiles().then(() => {
28437
+ }).catch(() => {
28438
+ }));
28439
+ await Promise.all(cacheWarmups);
28440
+ if (signal.aborted) return;
27824
28441
  if (this.config.conversationIndexEnabled && this.conversationIndexBackend) {
27825
- const init = await this.conversationIndexBackend.initialize();
27826
- if (!init.enabled) {
28442
+ try {
28443
+ const init = await this.conversationIndexBackend.initialize();
28444
+ if (!init.enabled) {
28445
+ this.config.conversationIndexEnabled = false;
28446
+ }
28447
+ if (init.logLevel === "info") {
28448
+ log.info(init.message);
28449
+ } else if (init.logLevel === "warn") {
28450
+ log.warn(init.message);
28451
+ } else {
28452
+ log.debug(init.message);
28453
+ }
28454
+ } catch (err) {
28455
+ log.error(`Conversation index initialization failed (non-fatal): ${err}`);
27827
28456
  this.config.conversationIndexEnabled = false;
27828
28457
  }
27829
- if (init.logLevel === "info") {
27830
- log.info(init.message);
27831
- } else if (init.logLevel === "warn") {
27832
- log.warn(init.message);
27833
- } else {
27834
- log.debug(init.message);
27835
- }
27836
28458
  }
27837
- await this.buffer.load();
28459
+ if (signal.aborted) return;
27838
28460
  if (this.config.localLlmEnabled) {
27839
- await this.validateLocalLlmModel();
27840
- }
27841
- if (this.config.compactionResetEnabled) {
27842
28461
  try {
27843
- const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
27844
- const files = await readdir14(wsDir).catch(() => []);
27845
- for (const f of files) {
27846
- if (!f.startsWith(".compaction-reset-signal-")) continue;
27847
- const fp = path43.join(wsDir, f);
27848
- const s = await stat10(fp).catch(() => null);
27849
- if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
27850
- await unlink7(fp).catch(() => {
27851
- });
27852
- log.debug(`initialize: removed stale compaction signal ${f}`);
27853
- }
27854
- }
28462
+ await this.validateLocalLlmModel();
27855
28463
  } catch (err) {
27856
- log.debug("initialize: stale signal sweep failed:", err);
28464
+ log.error(`Local LLM validation failed (non-fatal): ${err}`);
27857
28465
  }
27858
28466
  }
27859
- log.info("orchestrator initialized (full)");
28467
+ if (signal.aborted) return;
27860
28468
  if (this.config.daySummaryEnabled) {
27861
- this.autoRegisterDaySummaryCron().catch((err) => {
28469
+ try {
28470
+ await this.autoRegisterDaySummaryCron();
28471
+ } catch (err) {
27862
28472
  log.debug(`day-summary cron auto-register failed (non-fatal): ${err}`);
27863
- });
28473
+ }
27864
28474
  }
27865
28475
  if (this.config.nightlyGovernanceCronAutoRegister) {
27866
- this.autoRegisterNightlyGovernanceCron().catch((err) => {
28476
+ try {
28477
+ await this.autoRegisterNightlyGovernanceCron();
28478
+ } catch (err) {
27867
28479
  log.debug(`nightly governance cron auto-register failed (non-fatal): ${err}`);
27868
- });
28480
+ }
28481
+ }
28482
+ if (this.config.procedural?.proceduralMiningCronAutoRegister) {
28483
+ try {
28484
+ await this.autoRegisterProceduralMiningCron();
28485
+ } catch (err) {
28486
+ log.debug(`procedural mining cron auto-register failed (non-fatal): ${err}`);
28487
+ }
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
+ }
28496
+ log.info("orchestrator initialized (full \u2014 deferred steps complete)");
28497
+ }
28498
+ /**
28499
+ * Namespace-aware startup search sync. Re-probes QMD, ensures collections
28500
+ * (namespace-aware when namespacesEnabled), runs update, and warms up search.
28501
+ * Designed for server retry paths that run after the deferred init completes
28502
+ * when QMD was not available during initial startup.
28503
+ *
28504
+ * Accepts an optional AbortSignal so callers can interrupt the sync during
28505
+ * shutdown. The signal is checked between phases and forwarded into the QMD
28506
+ * update and warmup search calls so a long-running `qmd update` subprocess
28507
+ * is killed promptly rather than left in flight after `httpServer.stop()`.
28508
+ *
28509
+ * Returns true if the sync succeeded (QMD now available), false otherwise.
28510
+ */
28511
+ async startupSearchSync(signal) {
28512
+ if (signal?.aborted) return false;
28513
+ const available = await this.qmd.probe();
28514
+ if (!available) return false;
28515
+ if (signal?.aborted) {
28516
+ log.debug("startupSearchSync: aborted after probe");
28517
+ return false;
27869
28518
  }
28519
+ log.info(`startupSearchSync: backend now available ${this.qmd.debugStatus()}`);
28520
+ if (this.config.namespacesEnabled) {
28521
+ this.namespaceSearchRouter.clearCache();
28522
+ }
28523
+ const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
28524
+ const states = await Promise.all(
28525
+ namespaces.map(async (namespace) => ({
28526
+ namespace,
28527
+ state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(namespace) : await this.qmd.ensureCollection(this.config.memoryDir)
28528
+ }))
28529
+ );
28530
+ if (signal?.aborted) {
28531
+ log.debug("startupSearchSync: aborted after ensureCollection");
28532
+ return false;
28533
+ }
28534
+ const defaultState2 = states.find((e) => e.namespace === this.config.defaultNamespace)?.state ?? "unknown";
28535
+ if (defaultState2 === "missing") {
28536
+ if ("available" in this.qmd) {
28537
+ this.qmd.available = false;
28538
+ }
28539
+ this.qmd = new NoopSearchBackend();
28540
+ log.warn("startupSearchSync: search collection missing; disabling search (fallback retrieval remains enabled)");
28541
+ return false;
28542
+ }
28543
+ if (this.config.qmdMaintenanceEnabled) {
28544
+ try {
28545
+ const failTsBefore = "lastUpdateFailedAtMs" in this.qmd ? this.qmd.lastUpdateFailedAtMs : null;
28546
+ const hasRunTs = "lastUpdateRanAtMs" in this.qmd;
28547
+ if ("resetUpdateThrottles" in this.qmd) {
28548
+ this.qmd.resetUpdateThrottles();
28549
+ }
28550
+ log.info("startupSearchSync: updating index to match current disk state");
28551
+ let namespacesUpdated = 0;
28552
+ if (this.config.namespacesEnabled) {
28553
+ namespacesUpdated = await this.namespaceSearchRouter.updateNamespaces(namespaces);
28554
+ } else {
28555
+ await this.qmd.update(signal);
28556
+ }
28557
+ if (signal?.aborted) {
28558
+ log.debug("startupSearchSync: aborted after update");
28559
+ return false;
28560
+ }
28561
+ const failTsAfter = "lastUpdateFailedAtMs" in this.qmd ? this.qmd.lastUpdateFailedAtMs : null;
28562
+ const runTsAfter = hasRunTs ? this.qmd.lastUpdateRanAtMs : null;
28563
+ if (failTsAfter !== null && failTsAfter !== failTsBefore) {
28564
+ log.warn("startupSearchSync: update silently failed (detected via fail timestamp)");
28565
+ return false;
28566
+ }
28567
+ if (this.config.namespacesEnabled) {
28568
+ if (namespacesUpdated === 0) {
28569
+ log.warn("startupSearchSync: no namespace backends were eligible for update (all unavailable or collections missing)");
28570
+ return false;
28571
+ }
28572
+ log.info(`startupSearchSync: namespace updates succeeded (${namespacesUpdated}/${namespaces.length} namespaces updated)`);
28573
+ } else if (hasRunTs && runTsAfter === null) {
28574
+ log.warn("startupSearchSync: update was throttled/skipped (run timestamp is null after reset + update)");
28575
+ return false;
28576
+ }
28577
+ log.info("startupSearchSync: sync complete");
28578
+ } catch (err) {
28579
+ log.warn(`startupSearchSync: update failed: ${err}`);
28580
+ return false;
28581
+ }
28582
+ }
28583
+ if (!signal?.aborted) {
28584
+ try {
28585
+ await this.qmd.search("warmup", this.config.defaultNamespace, 1, void 0, { signal });
28586
+ log.info("startupSearchSync: warmup complete");
28587
+ } catch (err) {
28588
+ log.debug(`startupSearchSync: warmup search failed (non-fatal): ${err}`);
28589
+ }
28590
+ }
28591
+ return true;
27870
28592
  }
27871
28593
  /**
27872
28594
  * Auto-register the engram-day-summary cron job in OpenClaw if it doesn't exist.
@@ -27918,6 +28640,46 @@ var Orchestrator = class _Orchestrator {
27918
28640
  log.debug(`nightly governance cron auto-register error: ${err}`);
27919
28641
  }
27920
28642
  }
28643
+ async autoRegisterProceduralMiningCron() {
28644
+ const home = resolveHomeDir();
28645
+ const jobsPath = path43.join(home, ".openclaw", "cron", "jobs.json");
28646
+ try {
28647
+ if (!existsSync8(jobsPath)) {
28648
+ log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
28649
+ return;
28650
+ }
28651
+ const created = await ensureProceduralMiningCron(jobsPath, {
28652
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
28653
+ });
28654
+ if (created.created) {
28655
+ log.info(`procedural mining cron auto-registered (${created.jobId})`);
28656
+ } else {
28657
+ log.debug("procedural mining cron already exists, skipping auto-register");
28658
+ }
28659
+ } catch (err) {
28660
+ log.debug(`procedural mining cron auto-register error: ${err}`);
28661
+ }
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
+ }
27921
28683
  async applyBehaviorRuntimePolicy(state) {
27922
28684
  const result = await this.policyRuntime.applyFromBehaviorState(state);
27923
28685
  this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
@@ -28045,7 +28807,7 @@ var Orchestrator = class _Orchestrator {
28045
28807
  );
28046
28808
  return result;
28047
28809
  }
28048
- const { FallbackLlmClient: FallbackLlmClient2 } = await import("./fallback-llm-HJRCHKSA.js");
28810
+ const { FallbackLlmClient: FallbackLlmClient2 } = await import("./fallback-llm-QEAPMDW7.js");
28049
28811
  const useGateway = this.config.modelSource === "gateway";
28050
28812
  const modelSetting = this.config.semanticConsolidationModel;
28051
28813
  if (modelSetting === "fast" && this.fastLlm && !useGateway) {
@@ -30381,7 +31143,7 @@ ${trimmedBody}`;
30381
31143
  return null;
30382
31144
  }
30383
31145
  try {
30384
- const { retrieveCausalChains } = await import("./causal-retrieval-5UPIKZ4I.js");
31146
+ const { retrieveCausalChains } = await import("./causal-retrieval-3BKBXVXD.js");
30385
31147
  const section = await retrieveCausalChains({
30386
31148
  memoryDir: this.config.memoryDir,
30387
31149
  causalTrajectoryStoreDir: this.config.causalTrajectoryStoreDir,
@@ -30434,7 +31196,7 @@ ${trimmedBody}`;
30434
31196
  return null;
30435
31197
  }
30436
31198
  try {
30437
- const { getCalibrationRulesForRecall, buildCalibrationRecallSection } = await import("./calibration-3JHF25QT.js");
31199
+ const { getCalibrationRulesForRecall, buildCalibrationRecallSection } = await import("./calibration-BAC7KNKR.js");
30438
31200
  const rules = await getCalibrationRulesForRecall(this.config.memoryDir);
30439
31201
  if (rules.length === 0) {
30440
31202
  recordRecallSectionMetric({
@@ -31317,6 +32079,22 @@ ${formatted}`;
31317
32079
  });
31318
32080
  return section;
31319
32081
  })();
32082
+ const procedureRecallPromise = (async () => {
32083
+ if (this.config.procedural?.enabled !== true) return null;
32084
+ if (!this.isRecallSectionEnabled("procedure-recall", true)) return null;
32085
+ try {
32086
+ return await buildProcedureRecallSection(
32087
+ profileStorage,
32088
+ retrievalQuery,
32089
+ this.config
32090
+ );
32091
+ } catch (err) {
32092
+ log.debug(
32093
+ `procedure-recall: failed open: ${err instanceof Error ? err.message : String(err)}`
32094
+ );
32095
+ return null;
32096
+ }
32097
+ })();
31320
32098
  const compoundingPromise = observeEnrichmentPromise(
31321
32099
  (async () => {
31322
32100
  const t0 = Date.now();
@@ -31381,6 +32159,7 @@ ${formatted}`;
31381
32159
  causalTrajectorySection,
31382
32160
  cmcCausalChainsSection,
31383
32161
  calibrationSection,
32162
+ procedureRecallSection,
31384
32163
  trustZoneSection,
31385
32164
  verifiedRecallSection,
31386
32165
  verifiedRulesSection,
@@ -31402,6 +32181,7 @@ ${formatted}`;
31402
32181
  ["causalTraj", causalTrajectoryPromise],
31403
32182
  ["cmc", cmcRetrievalPromise],
31404
32183
  ["calibration", calibrationPromise],
32184
+ ["procedureRecall", procedureRecallPromise],
31405
32185
  ["trustZone", trustZonePromise],
31406
32186
  ["verifiedRecall", verifiedRecallPromise],
31407
32187
  ["verifiedRules", verifiedRulesPromise],
@@ -31507,6 +32287,13 @@ ${profile}`
31507
32287
  calibrationSection
31508
32288
  );
31509
32289
  }
32290
+ if (procedureRecallSection) {
32291
+ this.appendRecallSection(
32292
+ sectionBuckets,
32293
+ "procedure-recall",
32294
+ procedureRecallSection
32295
+ );
32296
+ }
31510
32297
  if (identityContinuity) {
31511
32298
  this.appendRecallSection(
31512
32299
  sectionBuckets,
@@ -32551,6 +33338,87 @@ _Context: ${topQuestion.context}_`
32551
33338
  }
32552
33339
  }
32553
33340
  }
33341
+ /**
33342
+ * Return the namespace that `ingestBulkImportBatch` writes into (#460).
33343
+ *
33344
+ * Exposed so host CLIs can snapshot the same storage root that extraction
33345
+ * actually writes to, avoiding the "CLI counts files at namespace A while
33346
+ * writes land in namespace B" footgun that a naïve
33347
+ * `config.defaultNamespace` snapshot could hit when a namespace policy
33348
+ * named `"default"` also exists.
33349
+ *
33350
+ * Today bulk-import is pinned to `config.defaultNamespace`; future
33351
+ * per-invocation namespace routing would thread an explicit target here
33352
+ * and through `ingestBulkImportBatch`.
33353
+ */
33354
+ bulkImportWriteNamespace() {
33355
+ return this.config.defaultNamespace;
33356
+ }
33357
+ /**
33358
+ * Ingest a batch of bulk-import turns (#460). Like ingestReplayBatch, this
33359
+ * normalizes user/assistant turns into the extraction buffer and awaits
33360
+ * settlement, but it intentionally bypasses the captureMode="explicit"
33361
+ * gate because bulk-import is itself an explicit user action — the user
33362
+ * ran `bulk-import --source <name> --file ...` and would be surprised to
33363
+ * see the command silently no-op when capture is otherwise restricted.
33364
+ *
33365
+ * Turns with role="other" are skipped (not supported by the extraction
33366
+ * pipeline).
33367
+ *
33368
+ * Two design decisions worth calling out:
33369
+ *
33370
+ * - **sessionKey is truthy and per-batch-unique.**
33371
+ * `ThreadingManager.shouldStartNewThread` only applies the session-key
33372
+ * boundary check when `turn.sessionKey` is truthy (threading.ts:82);
33373
+ * with an empty string, imported turns could attach to the current
33374
+ * live thread or merge across unrelated import batches. A unique
33375
+ * `bulk-import:batch:<timestamp>-<rand>` key forces a fresh thread per
33376
+ * batch without matching common prefix/map rules in
33377
+ * `principalFromSessionKeyRules`. (Catch-all regex rules could still
33378
+ * remap the principal, but that only affects metadata provenance —
33379
+ * see the next point for why write routing is unaffected.)
33380
+ *
33381
+ * - **writeNamespaceOverride pins the storage target.**
33382
+ * We pass `writeNamespaceOverride: this.bulkImportWriteNamespace()` to
33383
+ * `queueBufferedExtraction`, which tells `runExtraction` to skip
33384
+ * `defaultNamespaceForPrincipal` and write directly into the
33385
+ * orchestrator's declared bulk-import write namespace. This keeps
33386
+ * writes deterministic even when namespace policies named `"default"`
33387
+ * exist alongside a different `config.defaultNamespace`, and also
33388
+ * guards against regex-catch-all principal rules steering bulk-import
33389
+ * into an unexpected tenant.
33390
+ *
33391
+ * Per-invocation namespace routing (letting callers target a namespace
33392
+ * other than `bulkImportWriteNamespace()`) is a separate feature tracked
33393
+ * as a follow-up — the hook is the `writeNamespaceOverride` option, but
33394
+ * the CLI surface does not yet expose a `--namespace` flag.
33395
+ */
33396
+ async ingestBulkImportBatch(turns, options = {}) {
33397
+ if (!Array.isArray(turns) || turns.length === 0) return;
33398
+ const sessionKey = `bulk-import:batch:${Date.now().toString(36)}-` + randomBytes(6).toString("hex");
33399
+ const sessionTurns = [];
33400
+ for (const turn of turns) {
33401
+ if (turn.role !== "user" && turn.role !== "assistant") continue;
33402
+ sessionTurns.push({
33403
+ role: turn.role,
33404
+ content: turn.content,
33405
+ timestamp: turn.timestamp,
33406
+ sessionKey
33407
+ });
33408
+ }
33409
+ if (sessionTurns.length === 0) return;
33410
+ await new Promise((resolve, reject) => {
33411
+ void this.queueBufferedExtraction(sessionTurns, "trigger_mode", {
33412
+ skipDedupeCheck: true,
33413
+ clearBufferAfterExtraction: false,
33414
+ skipCharThreshold: true,
33415
+ bufferKey: sessionKey,
33416
+ extractionDeadlineMs: options.deadlineMs,
33417
+ writeNamespaceOverride: this.bulkImportWriteNamespace(),
33418
+ onTaskSettled: (err) => err ? reject(err) : resolve()
33419
+ }).catch(reject);
33420
+ });
33421
+ }
32554
33422
  async observeSessionHeartbeat(sessionKey, options = {}) {
32555
33423
  if (this.config.sessionObserverEnabled !== true) return;
32556
33424
  if (!sessionKey || sessionKey.length === 0) return;
@@ -32617,7 +33485,8 @@ _Context: ${topQuestion.context}_`
32617
33485
  skipCharThreshold: options.skipCharThreshold ?? false,
32618
33486
  deadlineMs: options.extractionDeadlineMs,
32619
33487
  bufferKey,
32620
- abortSignal: options.abortSignal
33488
+ abortSignal: options.abortSignal,
33489
+ writeNamespaceOverride: options.writeNamespaceOverride
32621
33490
  });
32622
33491
  options.onTaskSettled?.();
32623
33492
  } catch (err) {
@@ -32628,7 +33497,7 @@ _Context: ${topQuestion.context}_`
32628
33497
  if (!this.queueProcessing) {
32629
33498
  this.queueProcessing = true;
32630
33499
  this.processQueue().catch((err) => {
32631
- log.error("background extraction queue processor failed", err);
33500
+ this.logExtractionQueueFailure(err, "processor");
32632
33501
  this.queueProcessing = false;
32633
33502
  });
32634
33503
  }
@@ -32686,12 +33555,38 @@ ${normalized}`).digest("hex");
32686
33555
  try {
32687
33556
  await task();
32688
33557
  } catch (err) {
32689
- log.error("background extraction task failed", err);
33558
+ this.logExtractionQueueFailure(err, "task");
32690
33559
  }
32691
33560
  }
32692
33561
  }
32693
33562
  this.queueProcessing = false;
32694
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
+ }
32695
33590
  async runExtraction(turns, options = {}) {
32696
33591
  log.debug(`running extraction on ${turns.length} turns`);
32697
33592
  const clearBufferAfterExtraction = options.clearBufferAfterExtraction ?? true;
@@ -32703,12 +33598,12 @@ ${normalized}`).digest("hex");
32703
33598
  throw new Error(`replay extraction deadline exceeded (${stage})`);
32704
33599
  }
32705
33600
  };
32706
- const throwIfAborted3 = (stage) => {
33601
+ const throwIfAborted2 = (stage) => {
32707
33602
  throwIfRecallAborted(options.abortSignal, `extraction aborted (${stage})`);
32708
33603
  };
32709
33604
  const clearBuffer = async (options2) => {
32710
33605
  if (options2?.ignoreAbort !== true) {
32711
- throwIfAborted3("before_clear_buffer");
33606
+ throwIfAborted2("before_clear_buffer");
32712
33607
  }
32713
33608
  if (clearBufferAfterExtraction) {
32714
33609
  await this.buffer.clearAfterExtraction(bufferKey);
@@ -32727,7 +33622,7 @@ ${normalized}`).digest("hex");
32727
33622
  content: t.content.trim().slice(0, this.config.extractionMaxTurnChars)
32728
33623
  })).filter((t) => t.content.length > 0);
32729
33624
  throwIfDeadlineExceeded("before_extract");
32730
- throwIfAborted3("before_extract");
33625
+ throwIfAborted2("before_extract");
32731
33626
  const userTurns = normalizedTurns.filter((t) => t.role === "user");
32732
33627
  const totalChars = normalizedTurns.reduce(
32733
33628
  (sum, t) => sum + t.content.length,
@@ -32743,7 +33638,7 @@ ${normalized}`).digest("hex");
32743
33638
  return;
32744
33639
  }
32745
33640
  const principal = resolvePrincipal(sessionKey, this.config);
32746
- const selfNamespace = defaultNamespaceForPrincipal(principal, this.config);
33641
+ const selfNamespace = typeof options.writeNamespaceOverride === "string" && options.writeNamespaceOverride.length > 0 ? options.writeNamespaceOverride : defaultNamespaceForPrincipal(principal, this.config);
32747
33642
  const storage = await this.storageRouter.storageFor(selfNamespace);
32748
33643
  const shouldPersistProcessedFingerprint = normalizedTurns.some(
32749
33644
  (turn) => turn.persistProcessedFingerprint === true
@@ -32772,7 +33667,7 @@ ${normalized}`).digest("hex");
32772
33667
  "extraction aborted (during_extract)"
32773
33668
  );
32774
33669
  throwIfDeadlineExceeded("before_persist");
32775
- throwIfAborted3("before_persist");
33670
+ throwIfAborted2("before_persist");
32776
33671
  if (!result) {
32777
33672
  log.warn("runExtraction: extraction returned null/undefined");
32778
33673
  await clearBuffer();
@@ -33427,6 +34322,9 @@ ${normalized}`).digest("hex");
33427
34322
  continue;
33428
34323
  }
33429
34324
  const judgeCategory = preRoutedCategories[fi] ?? f.category;
34325
+ if (judgeCategory === "procedure") {
34326
+ continue;
34327
+ }
33430
34328
  const tags = Array.isArray(f.tags) ? f.tags : [];
33431
34329
  const imp = scoreImportance(
33432
34330
  f.content,
@@ -33482,16 +34380,6 @@ ${normalized}`).digest("hex");
33482
34380
  }
33483
34381
  fact.tags = Array.isArray(fact.tags) ? fact.tags.filter((t) => typeof t === "string") : [];
33484
34382
  fact.confidence = typeof fact.confidence === "number" ? fact.confidence : 0.7;
33485
- if (this.contentHashIndex) {
33486
- const canonicalContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
33487
- if (this.contentHashIndex.has(canonicalContent)) {
33488
- log.debug(
33489
- `dedup: skipping duplicate fact "${fact.content.slice(0, 60)}\u2026"`
33490
- );
33491
- dedupedCount++;
33492
- continue;
33493
- }
33494
- }
33495
34383
  let writeCategory = fact.category;
33496
34384
  let targetStorage = storage;
33497
34385
  let routedRuleId;
@@ -33516,11 +34404,24 @@ ${normalized}`).digest("hex");
33516
34404
  );
33517
34405
  }
33518
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
+ }
33519
34416
  const importance = scoreImportance(
33520
34417
  fact.content,
33521
34418
  writeCategory,
33522
34419
  fact.tags
33523
34420
  );
34421
+ if (writeCategory === "procedure" && this.config.procedural?.enabled !== true) {
34422
+ log.debug("persistExtraction: skip procedure memory (procedural.enabled is false)");
34423
+ continue;
34424
+ }
33524
34425
  if (!isAboveImportanceThreshold(
33525
34426
  importance.level,
33526
34427
  this.config.extractionMinImportanceLevel
@@ -33549,6 +34450,18 @@ ${normalized}`).digest("hex");
33549
34450
  }
33550
34451
  }
33551
34452
  }
34453
+ if (writeCategory === "procedure") {
34454
+ const procGate = validateProcedureExtraction({
34455
+ content: fact.content,
34456
+ procedureSteps: fact.procedureSteps
34457
+ });
34458
+ if (!procGate.durable) {
34459
+ log.debug(
34460
+ `extraction-procedure-gate: rejected "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34461
+ );
34462
+ continue;
34463
+ }
34464
+ }
33552
34465
  let pendingSemanticSkip = null;
33553
34466
  if (this.config.semanticDedupEnabled) {
33554
34467
  let semanticDecision;
@@ -33628,7 +34541,7 @@ ${normalized}`).digest("hex");
33628
34541
  dedupedCount++;
33629
34542
  continue;
33630
34543
  }
33631
- if (this.config.chunkingEnabled) {
34544
+ if (this.config.chunkingEnabled && writeCategory !== "procedure") {
33632
34545
  let chunkResult;
33633
34546
  if (this.config.semanticChunkingEnabled) {
33634
34547
  try {
@@ -33823,8 +34736,9 @@ ${normalized}`).digest("hex");
33823
34736
  links.push(...suggestedLinks);
33824
34737
  }
33825
34738
  }
33826
- const memoryKind = this.config.episodeNoteModeEnabled ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory) : void 0;
33827
- const citedFactContent = applyInlineCitation(fact.content);
34739
+ const memoryKind = writeCategory === "procedure" ? void 0 : this.config.episodeNoteModeEnabled ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory) : void 0;
34740
+ const rawPersistBody = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : fact.content;
34741
+ const citedFactContent = applyInlineCitation(rawPersistBody);
33828
34742
  const memoryId = await targetStorage.writeMemory(
33829
34743
  writeCategory,
33830
34744
  citedFactContent,
@@ -33841,7 +34755,7 @@ ${normalized}`).digest("hex");
33841
34755
  intentEntityTypes: inferredIntent?.entityTypes,
33842
34756
  memoryKind,
33843
34757
  structuredAttributes: fact.structuredAttributes,
33844
- contentHashSource: fact.content
34758
+ contentHashSource: writeCategory === "fact" ? fact.content : void 0
33845
34759
  }
33846
34760
  );
33847
34761
  if (routedRuleId) {
@@ -33942,7 +34856,8 @@ ${normalized}`).digest("hex");
33942
34856
  }
33943
34857
  if (this.contentHashIndex) {
33944
34858
  const canonicalFactContent = citationEnabled && hasCitationForTemplate(fact.content, citationTemplate) ? stripCitationForTemplate(fact.content, citationTemplate) : fact.content;
33945
- this.contentHashIndex.add(canonicalFactContent);
34859
+ const hashRegisterKey = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : canonicalFactContent;
34860
+ this.contentHashIndex.add(hashRegisterKey);
33946
34861
  }
33947
34862
  }
33948
34863
  for (const entity of entities) {
@@ -39065,8 +39980,8 @@ Best for:
39065
39980
  ),
39066
39981
  category: Type.Optional(
39067
39982
  Type.String({
39068
- description: 'Category: "fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule" (default: "fact")',
39069
- enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]
39983
+ description: 'Category: "fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure" (default: "fact")',
39984
+ enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure"]
39070
39985
  })
39071
39986
  ),
39072
39987
  tags: Type.Optional(
@@ -39124,7 +40039,7 @@ Best for:
39124
40039
  category: Type.Optional(
39125
40040
  Type.String({
39126
40041
  description: "Memory category.",
39127
- enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]
40042
+ enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure"]
39128
40043
  })
39129
40044
  ),
39130
40045
  tags: Type.Optional(
@@ -40193,8 +41108,8 @@ Returns: Performance trace data with timing breakdown`,
40193
41108
  }
40194
41109
 
40195
41110
  // ../remnic-core/src/cli.ts
40196
- import path71 from "path";
40197
- import { access as access5, readFile as readFile45, readdir as readdir25, unlink as unlink11 } from "fs/promises";
41111
+ import path72 from "path";
41112
+ import { access as access5, readFile as readFile46, readdir as readdir26, unlink as unlink11 } from "fs/promises";
40198
41113
  import { createHash as createHash14 } from "crypto";
40199
41114
 
40200
41115
  // ../remnic-core/src/transfer/export-json.ts
@@ -41071,8 +41986,8 @@ function gatherCandidates(input, warnings) {
41071
41986
  const record = rec;
41072
41987
  const content = typeof record.content === "string" ? record.content : null;
41073
41988
  if (!content) continue;
41074
- const path99 = typeof record.path === "string" ? record.path : "";
41075
- if (!path99.startsWith("transcripts/") && !path99.includes("/transcripts/")) continue;
41989
+ const path100 = typeof record.path === "string" ? record.path : "";
41990
+ if (!path100.startsWith("transcripts/") && !path100.includes("/transcripts/")) continue;
41076
41991
  rows.push(...parseJsonl(content, warnings));
41077
41992
  }
41078
41993
  return rows;
@@ -41127,6 +42042,158 @@ var openclawReplayNormalizer = {
41127
42042
  }
41128
42043
  };
41129
42044
 
42045
+ // ../remnic-core/src/bulk-import/types.ts
42046
+ var VALID_ROLES2 = /* @__PURE__ */ new Set(["user", "assistant", "other"]);
42047
+ function isImportRole(value) {
42048
+ return typeof value === "string" && VALID_ROLES2.has(value);
42049
+ }
42050
+ function parseIsoTimestamp2(value) {
42051
+ return parseIsoOffsetTimestamp(value);
42052
+ }
42053
+ function validateImportTurn(turn, index) {
42054
+ const issues = [];
42055
+ if (!turn || typeof turn !== "object") {
42056
+ issues.push({
42057
+ code: "turn.invalid",
42058
+ message: "Import turn must be an object.",
42059
+ index
42060
+ });
42061
+ return issues;
42062
+ }
42063
+ if (!isImportRole(turn.role)) {
42064
+ issues.push({
42065
+ code: "turn.role.invalid",
42066
+ message: `Import turn role must be 'user', 'assistant', or 'other', received '${String(turn.role)}'.`,
42067
+ index
42068
+ });
42069
+ }
42070
+ if (!turn.content || typeof turn.content !== "string" || turn.content.trim().length === 0) {
42071
+ issues.push({
42072
+ code: "turn.content.invalid",
42073
+ message: "Import turn content must be a non-empty string.",
42074
+ index
42075
+ });
42076
+ }
42077
+ if (!turn.timestamp || typeof turn.timestamp !== "string" || parseIsoTimestamp2(turn.timestamp) === null) {
42078
+ issues.push({
42079
+ code: "turn.timestamp.invalid",
42080
+ message: `Import turn timestamp must be a valid ISO timestamp, received '${String(turn.timestamp)}'.`,
42081
+ index
42082
+ });
42083
+ }
42084
+ return issues;
42085
+ }
42086
+
42087
+ // ../remnic-core/src/bulk-import/registry.ts
42088
+ var adapters = /* @__PURE__ */ new Map();
42089
+ function registerBulkImportSource(adapter) {
42090
+ if (!adapter || typeof adapter !== "object") {
42091
+ throw new Error("bulk-import adapter must be an object");
42092
+ }
42093
+ if (!adapter.name || typeof adapter.name !== "string" || adapter.name.trim().length === 0) {
42094
+ throw new Error("bulk-import adapter name must be a non-empty string");
42095
+ }
42096
+ if (typeof adapter.parse !== "function") {
42097
+ throw new Error(
42098
+ `bulk-import adapter '${adapter.name}' must have a parse function`
42099
+ );
42100
+ }
42101
+ const key = adapter.name.trim();
42102
+ if (adapters.has(key)) {
42103
+ throw new Error(
42104
+ `bulk-import source adapter '${key}' is already registered`
42105
+ );
42106
+ }
42107
+ const normalized = adapter.name === key ? adapter : { ...adapter, name: key };
42108
+ adapters.set(key, normalized);
42109
+ }
42110
+ function getBulkImportSource(name) {
42111
+ if (typeof name !== "string") return void 0;
42112
+ const key = name.trim();
42113
+ if (key.length === 0) return void 0;
42114
+ return adapters.get(key);
42115
+ }
42116
+ function listBulkImportSources() {
42117
+ return [...adapters.keys()];
42118
+ }
42119
+
42120
+ // ../remnic-core/src/bulk-import/pipeline.ts
42121
+ var DEFAULT_BATCH_SIZE = 20;
42122
+ var MIN_BATCH_SIZE = 1;
42123
+ var MAX_BATCH_SIZE = 1e3;
42124
+ function validateBatchSize(value) {
42125
+ if (value === void 0) return DEFAULT_BATCH_SIZE;
42126
+ if (typeof value !== "number" || !Number.isFinite(value)) {
42127
+ throw new Error(
42128
+ `batchSize must be a finite number, received ${String(value)}`
42129
+ );
42130
+ }
42131
+ if (!Number.isInteger(value)) {
42132
+ throw new Error(
42133
+ `batchSize must be an integer, received ${value}`
42134
+ );
42135
+ }
42136
+ if (value < MIN_BATCH_SIZE || value > MAX_BATCH_SIZE) {
42137
+ throw new Error(
42138
+ `batchSize must be between ${MIN_BATCH_SIZE} and ${MAX_BATCH_SIZE}, received ${value}`
42139
+ );
42140
+ }
42141
+ return value;
42142
+ }
42143
+ async function runBulkImportPipeline(source, options = {}, processBatch) {
42144
+ const batchSize = validateBatchSize(options.batchSize);
42145
+ const dryRun = options.dryRun === true;
42146
+ const result = {
42147
+ memoriesCreated: 0,
42148
+ duplicatesSkipped: 0,
42149
+ entitiesCreated: 0,
42150
+ turnsProcessed: 0,
42151
+ batchesProcessed: 0,
42152
+ errors: []
42153
+ };
42154
+ const turns = source.turns;
42155
+ if (!turns || turns.length === 0) {
42156
+ return result;
42157
+ }
42158
+ const validTurns = [];
42159
+ for (let i = 0; i < turns.length; i += 1) {
42160
+ const issues = validateImportTurn(turns[i], i);
42161
+ if (issues.length > 0) {
42162
+ const error = {
42163
+ batchIndex: -1,
42164
+ message: issues.map((iss) => iss.message).join("; ")
42165
+ };
42166
+ result.errors.push(error);
42167
+ } else {
42168
+ validTurns.push(turns[i]);
42169
+ }
42170
+ }
42171
+ if (dryRun) {
42172
+ result.turnsProcessed = validTurns.length;
42173
+ result.batchesProcessed = validTurns.length > 0 ? Math.ceil(validTurns.length / batchSize) : 0;
42174
+ return result;
42175
+ }
42176
+ let batchIndex = 0;
42177
+ for (let i = 0; i < validTurns.length; i += batchSize) {
42178
+ const batch = validTurns.slice(i, i + batchSize);
42179
+ try {
42180
+ const batchResult = await processBatch(batch);
42181
+ result.memoriesCreated += batchResult.memoriesCreated;
42182
+ result.duplicatesSkipped += batchResult.duplicatesSkipped;
42183
+ if (typeof batchResult.entitiesCreated === "number") {
42184
+ result.entitiesCreated += batchResult.entitiesCreated;
42185
+ }
42186
+ } catch (err) {
42187
+ const message = err instanceof Error ? err.message : String(err);
42188
+ result.errors.push({ batchIndex, message });
42189
+ }
42190
+ result.turnsProcessed += batch.length;
42191
+ result.batchesProcessed += 1;
42192
+ batchIndex += 1;
42193
+ }
42194
+ return result;
42195
+ }
42196
+
41130
42197
  // ../remnic-core/src/maintenance/archive-observations.ts
41131
42198
  import path55 from "path";
41132
42199
  import { mkdir as mkdir40, readdir as readdir19, readFile as readFile33, unlink as unlink8, writeFile as writeFile37 } from "fs/promises";
@@ -44475,6 +45542,102 @@ function sleep2(ms) {
44475
45542
  return new Promise((resolve) => setTimeout(resolve, ms));
44476
45543
  }
44477
45544
 
45545
+ // ../remnic-core/src/procedural/procedure-miner.ts
45546
+ var PROCEDURE_CLUSTER_ATTR_MAX = 500;
45547
+ function clusterKey(record) {
45548
+ const goal = record.goal.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 120);
45549
+ const refs = [...record.entityRefs ?? []].map((r) => r.trim().toLowerCase()).sort();
45550
+ return `${goal}|${refs.join(",")}`;
45551
+ }
45552
+ function successRate(group) {
45553
+ if (group.length === 0) return 0;
45554
+ const ok = group.filter((g) => g.outcomeKind === "success" || g.outcomeKind === "partial").length;
45555
+ return ok / group.length;
45556
+ }
45557
+ function pseudoStepsFromCluster(group) {
45558
+ const sentences = [];
45559
+ const pushUnique = (raw) => {
45560
+ const t = raw.trim();
45561
+ if (t.length < 8) return;
45562
+ if (!sentences.includes(t)) sentences.push(t);
45563
+ };
45564
+ for (const g of group) {
45565
+ const parts = [g.actionSummary, g.observationSummary, g.outcomeSummary].join(" ").split(/[.!?]\s+|;|\n+/).map((s) => s.trim()).filter((s) => s.length > 12);
45566
+ for (const p of parts) pushUnique(p);
45567
+ if (sentences.length >= 5) break;
45568
+ }
45569
+ if (sentences.length < 2 && group[0]) {
45570
+ pushUnique(`${group[0].goal.trim()} \u2014 confirm prerequisites and context.`);
45571
+ pushUnique("Execute the planned actions, then record the outcome.");
45572
+ }
45573
+ return sentences.slice(0, 6).map((intent, i) => ({
45574
+ order: i + 1,
45575
+ intent
45576
+ }));
45577
+ }
45578
+ async function hasExistingClusterWrite(storage, cluster) {
45579
+ const clusterKey2 = cluster.slice(0, PROCEDURE_CLUSTER_ATTR_MAX);
45580
+ const memories = await storage.readAllMemories();
45581
+ for (const m of memories) {
45582
+ if (m.frontmatter.category !== "procedure") continue;
45583
+ const c = m.frontmatter.structuredAttributes?.procedure_cluster;
45584
+ if (c === clusterKey2) return true;
45585
+ }
45586
+ return false;
45587
+ }
45588
+ async function runProcedureMining(options) {
45589
+ const cfg = options.config.procedural;
45590
+ if (!cfg?.enabled) {
45591
+ return { clustersProcessed: 0, proceduresWritten: 0, skippedReason: "procedural_disabled" };
45592
+ }
45593
+ if (cfg.minOccurrences <= 0) {
45594
+ return { clustersProcessed: 0, proceduresWritten: 0, skippedReason: "minOccurrences_zero" };
45595
+ }
45596
+ const trajectoryDir = typeof options.config.causalTrajectoryStoreDir === "string" && options.config.causalTrajectoryStoreDir.trim().length > 0 ? options.config.causalTrajectoryStoreDir.trim() : void 0;
45597
+ const { trajectories } = await readCausalTrajectoryRecords({
45598
+ memoryDir: options.memoryDir,
45599
+ causalTrajectoryStoreDir: trajectoryDir
45600
+ });
45601
+ const recent = filterTrajectoriesByLookbackDays(trajectories, cfg.lookbackDays);
45602
+ const clusters = /* @__PURE__ */ new Map();
45603
+ for (const t of recent) {
45604
+ const key = clusterKey(t);
45605
+ const arr = clusters.get(key) ?? [];
45606
+ arr.push(t);
45607
+ clusters.set(key, arr);
45608
+ }
45609
+ let clustersProcessed = 0;
45610
+ let proceduresWritten = 0;
45611
+ for (const [key, group] of clusters) {
45612
+ if (group.length < cfg.minOccurrences) continue;
45613
+ const rate = successRate(group);
45614
+ if (rate < cfg.successFloor) continue;
45615
+ clustersProcessed += 1;
45616
+ if (await hasExistingClusterWrite(options.storage, key)) {
45617
+ log.debug(`procedure-miner: skip duplicate cluster key=${key.slice(0, 40)}\u2026`);
45618
+ continue;
45619
+ }
45620
+ const steps = normalizeProcedureSteps(pseudoStepsFromCluster(group));
45621
+ if (steps.length < 2) continue;
45622
+ const title = `When you work on goals like: ${group[0].goal.trim().slice(0, 140)}`;
45623
+ const body = buildProcedurePersistBody(title, steps);
45624
+ const promote = cfg.autoPromoteEnabled === true && group.length >= cfg.autoPromoteOccurrences && rate >= cfg.successFloor;
45625
+ await options.storage.writeMemory("procedure", body, {
45626
+ source: "procedure-miner",
45627
+ status: promote ? "active" : "pending_review",
45628
+ tags: ["procedure-miner", "causal-trajectory"],
45629
+ structuredAttributes: {
45630
+ procedure_cluster: key.slice(0, PROCEDURE_CLUSTER_ATTR_MAX),
45631
+ trajectory_ids: group.map((g) => g.trajectoryId).join(",").slice(0, 1900),
45632
+ trajectory_count: String(group.length),
45633
+ success_rate: rate.toFixed(4)
45634
+ }
45635
+ });
45636
+ proceduresWritten += 1;
45637
+ }
45638
+ return { clustersProcessed, proceduresWritten };
45639
+ }
45640
+
44478
45641
  // ../remnic-core/src/briefing.ts
44479
45642
  import { readFile as readFile41 } from "fs/promises";
44480
45643
  import path67 from "path";
@@ -46186,6 +47349,25 @@ var EngramAccessService = class {
46186
47349
  reportPath: result.reportPath
46187
47350
  };
46188
47351
  }
47352
+ async procedureMiningRun(request, principal) {
47353
+ const resolvedNamespace = this.resolveWritableNamespace(
47354
+ request.namespace,
47355
+ void 0,
47356
+ request.authenticatedPrincipal ?? principal
47357
+ );
47358
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
47359
+ const result = await runProcedureMining({
47360
+ memoryDir: storage.dir,
47361
+ storage,
47362
+ config: this.orchestrator.config
47363
+ });
47364
+ return {
47365
+ namespace: resolvedNamespace,
47366
+ clustersProcessed: result.clustersProcessed,
47367
+ proceduresWritten: result.proceduresWritten,
47368
+ skippedReason: result.skippedReason
47369
+ };
47370
+ }
46189
47371
  async trustZoneStatus(namespace, principal) {
46190
47372
  const resolvedNamespace = this.resolveReadableNamespace(namespace, principal);
46191
47373
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
@@ -47066,6 +48248,25 @@ ${next}`);
47066
48248
  }
47067
48249
  return { submitted: memoryIds.length, matched: matchedIds.length };
47068
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
+ }
47069
48270
  };
47070
48271
 
47071
48272
  // ../remnic-core/src/access-http.ts
@@ -47214,6 +48415,17 @@ var EngramMcpServer = class {
47214
48415
  additionalProperties: false
47215
48416
  }
47216
48417
  },
48418
+ {
48419
+ name: "engram.procedure_mining_run",
48420
+ description: "Run procedural memory mining from causal trajectories (issue #519). Respects procedural.enabled; writes under procedures/ when clusters qualify.",
48421
+ inputSchema: {
48422
+ type: "object",
48423
+ properties: {
48424
+ namespace: { type: "string" }
48425
+ },
48426
+ additionalProperties: false
48427
+ }
48428
+ },
47217
48429
  {
47218
48430
  name: "engram.memory_get",
47219
48431
  description: "Fetch one Remnic memory by id.",
@@ -47803,7 +49015,45 @@ var EngramMcpServer = class {
47803
49015
  },
47804
49016
  additionalProperties: false
47805
49017
  }
47806
- }] : []
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
+ }
47807
49057
  ].flatMap((tool) => withToolAliases(tool));
47808
49058
  }
47809
49059
  service;
@@ -48093,6 +49343,14 @@ ${body}`;
48093
49343
  batchSize: typeof args.batchSize === "number" && Number.isFinite(args.batchSize) ? args.batchSize : void 0,
48094
49344
  authenticatedPrincipal: effectivePrincipal
48095
49345
  }, effectivePrincipal);
49346
+ case "engram.procedure_mining_run":
49347
+ return this.service.procedureMiningRun(
49348
+ {
49349
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0,
49350
+ authenticatedPrincipal: effectivePrincipal
49351
+ },
49352
+ effectivePrincipal
49353
+ );
48096
49354
  case "engram.memory_get":
48097
49355
  return this.service.memoryGet(
48098
49356
  typeof args.memoryId === "string" ? args.memoryId : "",
@@ -48402,6 +49660,39 @@ ${body}`;
48402
49660
  principal: effectivePrincipal
48403
49661
  });
48404
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
+ }
48405
49696
  default:
48406
49697
  throw new Error(`unknown tool: ${name}`);
48407
49698
  }
@@ -48458,7 +49749,8 @@ var categorySchema = external_exports.enum([
48458
49749
  "commitment",
48459
49750
  "moment",
48460
49751
  "skill",
48461
- "rule"
49752
+ "rule",
49753
+ "procedure"
48462
49754
  ]).optional();
48463
49755
  var confidenceSchema = external_exports.number().min(0).max(1).optional();
48464
49756
  var tagsSchema = external_exports.array(external_exports.string().max(256)).max(50).optional();
@@ -48657,8 +49949,8 @@ var HermesAdapter = class {
48657
49949
  // ../remnic-core/src/adapters/registry.ts
48658
49950
  var AdapterRegistry = class {
48659
49951
  adapters;
48660
- constructor(adapters) {
48661
- this.adapters = adapters ?? [
49952
+ constructor(adapters2) {
49953
+ this.adapters = adapters2 ?? [
48662
49954
  new HermesAdapter(),
48663
49955
  new ReplitAdapter(),
48664
49956
  new CodexAdapter(),
@@ -49248,6 +50540,67 @@ var EngramAccessHttpServer = class {
49248
50540
  });
49249
50541
  return;
49250
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
+ }
49251
50604
  this.respondJson(res, 404, { error: "not_found", code: "not_found" });
49252
50605
  }
49253
50606
  async handleMcpRequest(req, res) {
@@ -50130,6 +51483,10 @@ async function promoteSemanticRuleFromMemory(options) {
50130
51483
  return report;
50131
51484
  }
50132
51485
 
51486
+ // ../remnic-core/src/training-export/converter.ts
51487
+ import { lstat as lstat2, readdir as readdir25, readFile as readFile45, realpath as realpath3 } from "fs/promises";
51488
+ import path71 from "path";
51489
+
50133
51490
  // ../remnic-core/src/cli.ts
50134
51491
  function rankCandidateForKeep(a, b) {
50135
51492
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -50320,7 +51677,7 @@ async function runRepairMemoryProjectionCliCommand(options) {
50320
51677
  });
50321
51678
  }
50322
51679
  async function runMemoryTimelineCliCommand(options) {
50323
- const storage = new (await import("./storage-HW6SRQCK.js")).StorageManager(options.memoryDir);
51680
+ const storage = new (await import("./storage-BA6OBLMK.js")).StorageManager(options.memoryDir);
50324
51681
  return storage.getMemoryTimeline(options.memoryId, options.limit);
50325
51682
  }
50326
51683
  async function runMemoryGovernanceCliCommand(options) {
@@ -50348,7 +51705,7 @@ async function runMemoryGovernanceRestoreCliCommand(options) {
50348
51705
  });
50349
51706
  }
50350
51707
  async function runMemoryReviewDispositionCliCommand(options) {
50351
- const storage = new (await import("./storage-HW6SRQCK.js")).StorageManager(options.memoryDir);
51708
+ const storage = new (await import("./storage-BA6OBLMK.js")).StorageManager(options.memoryDir);
50352
51709
  const memory = await storage.getMemoryById(options.memoryId);
50353
51710
  if (!memory) throw new Error(`memory not found: ${options.memoryId}`);
50354
51711
  const updated = await storage.writeMemoryFrontmatter(memory, {
@@ -50514,7 +51871,7 @@ async function runSemanticRulePromoteCliCommand(options) {
50514
51871
  });
50515
51872
  }
50516
51873
  async function runCompoundingPromoteCliCommand(options) {
50517
- const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-2TLD4YSC.js");
51874
+ const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-WGNTTFYE.js");
50518
51875
  const config = parseConfig({
50519
51876
  memoryDir: options.memoryDir,
50520
51877
  qmdEnabled: false,
@@ -50931,7 +52288,7 @@ function policyVersionForValues(values, config) {
50931
52288
  return createHash14("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
50932
52289
  }
50933
52290
  async function readRuntimePolicySnapshot2(config, fileName) {
50934
- const filePath = path71.join(config.memoryDir, "state", fileName);
52291
+ const filePath = path72.join(config.memoryDir, "state", fileName);
50935
52292
  const snapshot = await readRuntimePolicySnapshot(filePath, {
50936
52293
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
50937
52294
  });
@@ -51487,7 +52844,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
51487
52844
  }
51488
52845
  async function runReplayCliCommand(orchestrator, options) {
51489
52846
  const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
51490
- const inputRaw = await readFile45(options.inputPath, "utf-8");
52847
+ const inputRaw = await readFile46(options.inputPath, "utf-8");
51491
52848
  const registry = buildReplayNormalizerRegistry([
51492
52849
  openclawReplayNormalizer,
51493
52850
  claudeReplayNormalizer,
@@ -51549,10 +52906,101 @@ async function runReplayCliCommand(orchestrator, options) {
51549
52906
  }
51550
52907
  return summary;
51551
52908
  }
52909
+ async function ensureBuiltInBulkImportAdapters() {
52910
+ if (!getBulkImportSource("weclone")) {
52911
+ const wecloneSpecifier = "@remnic/import-weclone";
52912
+ try {
52913
+ const mod = await import(wecloneSpecifier);
52914
+ if (mod.wecloneImportAdapter) {
52915
+ try {
52916
+ registerBulkImportSource(mod.wecloneImportAdapter);
52917
+ } catch {
52918
+ }
52919
+ }
52920
+ } catch {
52921
+ }
52922
+ }
52923
+ }
52924
+ async function runBulkImportCliCommand(opts) {
52925
+ await ensureBuiltInBulkImportAdapters();
52926
+ const adapter = getBulkImportSource(opts.source);
52927
+ if (!adapter) {
52928
+ const registered = listBulkImportSources();
52929
+ const list = registered.length > 0 ? registered.map((n) => `'${n}'`).join(", ") : "(none registered)";
52930
+ throw new Error(
52931
+ `Unknown bulk-import source '${opts.source}'. Valid sources: ${list}`
52932
+ );
52933
+ }
52934
+ if (opts.dryRun !== true && typeof opts.ingestBatch !== "function") {
52935
+ throw new Error(
52936
+ "Bulk import persistence is not wired: no ingestBatch callback was provided by the host CLI. Use --dry-run to validate without persisting, or invoke via `openclaw engram bulk-import` which supplies the orchestrator-backed ingestion path."
52937
+ );
52938
+ }
52939
+ const inputRaw = await readFile46(opts.file, "utf-8");
52940
+ let inputParsed;
52941
+ try {
52942
+ inputParsed = JSON.parse(inputRaw);
52943
+ } catch (err) {
52944
+ throw new Error(
52945
+ `Failed to parse import file as JSON: ${err.message}`
52946
+ );
52947
+ }
52948
+ if (typeof inputParsed !== "object" || inputParsed === null) {
52949
+ throw new Error(
52950
+ "Import file must contain a JSON object or array, got " + (inputParsed === null ? "null" : typeof inputParsed)
52951
+ );
52952
+ }
52953
+ const parsed = await adapter.parse(inputParsed, {
52954
+ strict: opts.strict === true,
52955
+ platform: opts.platform
52956
+ });
52957
+ const processBatch = opts.ingestBatch ?? (async () => {
52958
+ throw new Error(
52959
+ "Bulk import persistence is not wired: no ingestBatch callback was provided by the host CLI."
52960
+ );
52961
+ });
52962
+ const result = await runBulkImportPipeline(
52963
+ parsed,
52964
+ {
52965
+ batchSize: opts.batchSize,
52966
+ dryRun: opts.dryRun,
52967
+ dedup: true,
52968
+ trustLevel: "import"
52969
+ },
52970
+ processBatch
52971
+ );
52972
+ const out = opts.stdout;
52973
+ out.write(`Bulk import complete (source: ${opts.source})
52974
+ `);
52975
+ out.write(` Turns processed: ${result.turnsProcessed}
52976
+ `);
52977
+ out.write(` Batches processed: ${result.batchesProcessed}
52978
+ `);
52979
+ out.write(` Memories created: ${result.memoriesCreated}
52980
+ `);
52981
+ out.write(` Duplicates skipped: ${result.duplicatesSkipped}
52982
+ `);
52983
+ if (result.errors.length > 0) {
52984
+ out.write(` Errors: ${result.errors.length}
52985
+ `);
52986
+ if (opts.verbose) {
52987
+ for (const err of result.errors) {
52988
+ opts.stderr.write(
52989
+ ` [batch ${err.batchIndex}] ${err.message}
52990
+ `
52991
+ );
52992
+ }
52993
+ }
52994
+ }
52995
+ if (opts.dryRun) {
52996
+ out.write(" (dry run \u2014 no memories were stored)\n");
52997
+ }
52998
+ return result;
52999
+ }
51552
53000
  async function getPluginVersion() {
51553
53001
  try {
51554
53002
  const pkgPath = new URL("../package.json", import.meta.url);
51555
- const raw = await readFile45(pkgPath, "utf-8");
53003
+ const raw = await readFile46(pkgPath, "utf-8");
51556
53004
  const parsed = JSON.parse(raw);
51557
53005
  return parsed.version ?? "unknown";
51558
53006
  } catch {
@@ -51576,59 +53024,71 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace, options) {
51576
53024
  }
51577
53025
  return orchestrator.config.memoryDir;
51578
53026
  }
51579
- const candidate = path71.join(orchestrator.config.memoryDir, "namespaces", ns);
53027
+ const candidate = path72.join(orchestrator.config.memoryDir, "namespaces", ns);
51580
53028
  if (ns === orchestrator.config.defaultNamespace) {
51581
53029
  return await exists3(candidate) ? candidate : orchestrator.config.memoryDir;
51582
53030
  }
51583
53031
  return candidate;
51584
53032
  }
51585
- async function readAllMemoryFiles(memoryDir) {
51586
- const roots = [path71.join(memoryDir, "facts"), path71.join(memoryDir, "corrections")];
51587
- const out = [];
53033
+ async function walkMemoryMarkdownFiles(memoryDir, visit) {
53034
+ const roots = [path72.join(memoryDir, "facts"), path72.join(memoryDir, "corrections")];
51588
53035
  const walk = async (dir) => {
51589
53036
  let entries;
51590
53037
  try {
51591
- entries = await readdir25(dir, { withFileTypes: true });
53038
+ entries = await readdir26(dir, { withFileTypes: true });
51592
53039
  } catch {
51593
53040
  return;
51594
53041
  }
51595
53042
  for (const entry of entries) {
51596
53043
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
51597
- const fullPath = path71.join(dir, entryName);
53044
+ const fullPath = path72.join(dir, entryName);
51598
53045
  if (entry.isDirectory()) {
51599
53046
  await walk(fullPath);
51600
53047
  continue;
51601
53048
  }
51602
53049
  if (!entry.isFile() || !entryName.endsWith(".md")) continue;
51603
- try {
51604
- const raw = await readFile45(fullPath, "utf-8");
51605
- const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
51606
- if (!parsed) continue;
51607
- const fmRaw = parsed[1];
51608
- const body = parsed[2] ?? "";
51609
- const get = (key) => {
51610
- const match = fmRaw.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
51611
- return match ? match[1].trim() : "";
51612
- };
51613
- const confidenceRaw = get("confidence");
51614
- const confidence = confidenceRaw.length > 0 ? Number(confidenceRaw) : void 0;
51615
- out.push({
51616
- path: fullPath,
51617
- content: body,
51618
- frontmatter: {
51619
- id: get("id") || void 0,
51620
- confidence: Number.isFinite(confidence) ? confidence : void 0,
51621
- updated: get("updated") || void 0,
51622
- created: get("created") || void 0
51623
- }
51624
- });
51625
- } catch {
51626
- }
53050
+ await visit(fullPath);
51627
53051
  }
51628
53052
  };
51629
53053
  for (const root of roots) {
51630
53054
  await walk(root);
51631
53055
  }
53056
+ }
53057
+ async function listMemoryMarkdownFilePaths(memoryDir) {
53058
+ const paths = [];
53059
+ await walkMemoryMarkdownFiles(memoryDir, (fullPath) => {
53060
+ paths.push(fullPath);
53061
+ });
53062
+ return paths;
53063
+ }
53064
+ async function readAllMemoryFiles(memoryDir) {
53065
+ const out = [];
53066
+ await walkMemoryMarkdownFiles(memoryDir, async (fullPath) => {
53067
+ try {
53068
+ const raw = await readFile46(fullPath, "utf-8");
53069
+ const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
53070
+ if (!parsed) return;
53071
+ const fmRaw = parsed[1];
53072
+ const body = parsed[2] ?? "";
53073
+ const get = (key) => {
53074
+ const match = fmRaw.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
53075
+ return match ? match[1].trim() : "";
53076
+ };
53077
+ const confidenceRaw = get("confidence");
53078
+ const confidence = confidenceRaw.length > 0 ? Number(confidenceRaw) : void 0;
53079
+ out.push({
53080
+ path: fullPath,
53081
+ content: body,
53082
+ frontmatter: {
53083
+ id: get("id") || void 0,
53084
+ confidence: Number.isFinite(confidence) ? confidence : void 0,
53085
+ updated: get("updated") || void 0,
53086
+ created: get("created") || void 0
53087
+ }
53088
+ });
53089
+ } catch {
53090
+ }
53091
+ });
51632
53092
  return out;
51633
53093
  }
51634
53094
  function formatContinuityIncidentCli(incident) {
@@ -51937,7 +53397,7 @@ function registerCli(api, orchestrator) {
51937
53397
  if (plan.moved.length > 0) {
51938
53398
  console.log("\nEntries:");
51939
53399
  for (const move of plan.moved) {
51940
- console.log(`- ${path71.basename(move.from)}`);
53400
+ console.log(`- ${path72.basename(move.from)}`);
51941
53401
  }
51942
53402
  }
51943
53403
  if (dryRun) {
@@ -52135,6 +53595,64 @@ function registerCli(api, orchestrator) {
52135
53595
  }
52136
53596
  console.log("OK");
52137
53597
  });
53598
+ cmd.command("bulk-import").description(
53599
+ "Bulk-import chat history via a registered source adapter (e.g. --source weclone)."
53600
+ ).option("--source <source>", "Bulk-import source adapter name (e.g. weclone)").option("--file <path>", "Path to the import file (JSON)").option("--platform <platform>", "Optional platform override forwarded to the adapter").option("--batch-size <n>", "Turns per batch", "50").option("--dry-run", "Parse and validate only; do not persist").option("--strict", "Fail on any invalid source row").option("--verbose", "Print per-batch error details").action(async (...args) => {
53601
+ const options = args[0] ?? {};
53602
+ const sourceRaw = typeof options.source === "string" ? options.source.trim() : "";
53603
+ const filePathRaw = typeof options.file === "string" ? options.file.trim() : "";
53604
+ if (sourceRaw.length === 0) {
53605
+ console.log("Missing --source. Example: openclaw engram bulk-import --source weclone --file /tmp/export.json");
53606
+ return;
53607
+ }
53608
+ if (filePathRaw.length === 0) {
53609
+ console.log("Missing --file. Example: openclaw engram bulk-import --source weclone --file /tmp/export.json");
53610
+ return;
53611
+ }
53612
+ const batchSizeRaw = parseInt(String(options.batchSize ?? "50"), 10);
53613
+ const batchSize = Number.isFinite(batchSizeRaw) && batchSizeRaw > 0 ? batchSizeRaw : 50;
53614
+ const platformRaw = typeof options.platform === "string" ? options.platform.trim() : "";
53615
+ const writeNamespace = orchestrator.bulkImportWriteNamespace();
53616
+ const writeStorage = await orchestrator.getStorageForNamespace(
53617
+ writeNamespace
53618
+ );
53619
+ const writeRoot = writeStorage.dir;
53620
+ const ingestBatch = async (turns) => {
53621
+ const before = new Set(await listMemoryMarkdownFilePaths(writeRoot));
53622
+ await orchestrator.ingestBulkImportBatch(turns, {});
53623
+ const after = await listMemoryMarkdownFilePaths(writeRoot);
53624
+ let memoriesCreated = 0;
53625
+ for (const p of after) {
53626
+ if (!before.has(p)) memoriesCreated += 1;
53627
+ }
53628
+ return { memoriesCreated, duplicatesSkipped: 0 };
53629
+ };
53630
+ try {
53631
+ const result = await runBulkImportCliCommand({
53632
+ memoryDir: writeRoot,
53633
+ source: sourceRaw,
53634
+ file: filePathRaw,
53635
+ platform: platformRaw.length > 0 ? platformRaw : void 0,
53636
+ batchSize,
53637
+ dryRun: options.dryRun === true,
53638
+ verbose: options.verbose === true,
53639
+ strict: options.strict === true,
53640
+ ingestBatch,
53641
+ stdout: process.stdout,
53642
+ stderr: process.stderr
53643
+ });
53644
+ if (result.errors.length > 0) {
53645
+ console.error(`Bulk import completed with ${result.errors.length} batch error(s).`);
53646
+ process.exitCode = 1;
53647
+ } else {
53648
+ console.log("OK");
53649
+ }
53650
+ } catch (err) {
53651
+ const message = err instanceof Error ? err.message : String(err);
53652
+ console.error(`Bulk import failed: ${message}`);
53653
+ process.exitCode = 1;
53654
+ }
53655
+ });
52138
53656
  cmd.command("benchmark-status").description("Show benchmark/evaluation harness status, benchmark packs, and latest run summary").action(async () => {
52139
53657
  const status = await runBenchmarkStatusCliCommand({
52140
53658
  memoryDir: orchestrator.config.memoryDir,
@@ -53587,7 +55105,7 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
53587
55105
  }
53588
55106
  });
53589
55107
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
53590
- const workspaceDir = path71.join(resolveHomeDir(), ".openclaw", "workspace");
55108
+ const workspaceDir = path72.join(resolveHomeDir(), ".openclaw", "workspace");
53591
55109
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
53592
55110
  if (!identity) {
53593
55111
  console.log("No identity file found.");
@@ -53810,8 +55328,8 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
53810
55328
  const options = args[0] ?? {};
53811
55329
  const threadId = options.thread;
53812
55330
  const top = parseInt(options.top ?? "10", 10);
53813
- const memoryDir = path71.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
53814
- const threading = new ThreadingManager(path71.join(memoryDir, "threads"));
55331
+ const memoryDir = path72.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
55332
+ const threading = new ThreadingManager(path72.join(memoryDir, "threads"));
53815
55333
  if (threadId) {
53816
55334
  const thread = await threading.loadThread(threadId);
53817
55335
  if (!thread) {
@@ -53963,6 +55481,94 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
53963
55481
  }
53964
55482
  console.log(orchestrator.summarizer.formatForRecall(summaries, summaries.length));
53965
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
+ });
53966
55572
  },
53967
55573
  { commands: ["engram"] }
53968
55574
  );
@@ -54287,19 +55893,19 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
54287
55893
  }
54288
55894
 
54289
55895
  // ../../src/index.ts
54290
- import { readFile as readFile51, writeFile as writeFile46 } from "fs/promises";
55896
+ import { readFile as readFile52, realpath as realpath5, writeFile as writeFile46 } from "fs/promises";
54291
55897
  import { readFileSync as readFileSync6 } from "fs";
54292
- import path97 from "path";
55898
+ import path98 from "path";
54293
55899
  import os6 from "os";
54294
55900
 
54295
55901
  // ../remnic-core/src/opik-exporter.ts
54296
- import { createHash as createHash15, randomBytes } from "crypto";
55902
+ import { createHash as createHash15, randomBytes as randomBytes2 } from "crypto";
54297
55903
  import { readFileSync as readFileSync4 } from "fs";
54298
- import path72 from "path";
55904
+ import path73 from "path";
54299
55905
  var OPIK_EXPORTER_SLOT = "__openclawOpikExporter";
54300
55906
  function readOpikOpenclawConfig(log2) {
54301
55907
  try {
54302
- const configPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH") || path72.join(resolveHomeDir(), ".openclaw", "openclaw.json");
55908
+ const configPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH") || path73.join(resolveHomeDir(), ".openclaw", "openclaw.json");
54303
55909
  const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
54304
55910
  const entry = raw?.plugins?.entries?.["opik-openclaw"];
54305
55911
  if (!entry?.enabled || !entry?.config) return {};
@@ -54317,7 +55923,7 @@ function readOpikOpenclawConfig(log2) {
54317
55923
  }
54318
55924
  function uuidV7() {
54319
55925
  const now = Date.now();
54320
- const bytes = randomBytes(16);
55926
+ const bytes = randomBytes2(16);
54321
55927
  bytes[0] = now / 2 ** 40 & 255;
54322
55928
  bytes[1] = now / 2 ** 32 & 255;
54323
55929
  bytes[2] = now / 2 ** 24 & 255;
@@ -54661,8 +56267,8 @@ function cleanUserMessage(content) {
54661
56267
  }
54662
56268
 
54663
56269
  // src/public-artifacts.ts
54664
- import { readdir as readdir26, access as access6, stat as stat20, lstat as lstat2, realpath as realpath3 } from "fs/promises";
54665
- import path73 from "path";
56270
+ import { readdir as readdir27, access as access6, stat as stat20, lstat as lstat3, realpath as realpath4 } from "fs/promises";
56271
+ import path74 from "path";
54666
56272
  var PUBLIC_DIRS = [
54667
56273
  { dir: "facts", kind: "fact", contentType: "markdown" },
54668
56274
  { dir: "entities", kind: "entity", contentType: "markdown" },
@@ -54674,9 +56280,9 @@ var PUBLIC_FILES = [
54674
56280
  ];
54675
56281
  async function isContainedWithin(target, boundary) {
54676
56282
  try {
54677
- const resolvedTarget = await realpath3(target);
54678
- const resolvedBoundary = await realpath3(boundary);
54679
- return resolvedTarget === resolvedBoundary || resolvedTarget.startsWith(resolvedBoundary + path73.sep);
56283
+ const resolvedTarget = await realpath4(target);
56284
+ const resolvedBoundary = await realpath4(boundary);
56285
+ return resolvedTarget === resolvedBoundary || resolvedTarget.startsWith(resolvedBoundary + path74.sep);
54680
56286
  } catch {
54681
56287
  return false;
54682
56288
  }
@@ -54685,7 +56291,7 @@ async function listMarkdownFilesRecursive(rootDir, boundary, ancestorRealPaths)
54685
56291
  const boundaryDir = boundary ?? rootDir;
54686
56292
  let resolvedRoot;
54687
56293
  try {
54688
- resolvedRoot = await realpath3(rootDir);
56294
+ resolvedRoot = await realpath4(rootDir);
54689
56295
  } catch {
54690
56296
  return [];
54691
56297
  }
@@ -54694,18 +56300,18 @@ async function listMarkdownFilesRecursive(rootDir, boundary, ancestorRealPaths)
54694
56300
  nextAncestors.add(resolvedRoot);
54695
56301
  let entries;
54696
56302
  try {
54697
- entries = await readdir26(rootDir, { withFileTypes: true });
56303
+ entries = await readdir27(rootDir, { withFileTypes: true });
54698
56304
  } catch {
54699
56305
  return [];
54700
56306
  }
54701
56307
  entries.sort((a, b) => String(a.name).localeCompare(String(b.name)));
54702
56308
  const files = [];
54703
56309
  for (const entry of entries) {
54704
- const fullPath = path73.join(rootDir, String(entry.name));
56310
+ const fullPath = path74.join(rootDir, String(entry.name));
54705
56311
  let isDir = entry.isDirectory();
54706
56312
  let isFile = entry.isFile();
54707
56313
  try {
54708
- const linkStat = await lstat2(fullPath);
56314
+ const linkStat = await lstat3(fullPath);
54709
56315
  if (linkStat.isSymbolicLink()) {
54710
56316
  if (!await isContainedWithin(fullPath, boundaryDir)) {
54711
56317
  continue;
@@ -54739,21 +56345,21 @@ async function listRemnicPublicArtifacts(params) {
54739
56345
  const { memoryDir, workspaceDir, agentIds } = params;
54740
56346
  const artifacts = [];
54741
56347
  for (const spec of PUBLIC_DIRS) {
54742
- const dirPath = path73.join(memoryDir, spec.dir);
56348
+ const dirPath = path74.join(memoryDir, spec.dir);
54743
56349
  if (!await pathExists2(dirPath)) continue;
54744
56350
  if (!await isContainedWithin(dirPath, memoryDir)) continue;
54745
56351
  try {
54746
- const resolvedDir = await realpath3(dirPath);
54747
- const expectedParent = await realpath3(memoryDir);
54748
- const resolvedName = path73.basename(resolvedDir);
56352
+ const resolvedDir = await realpath4(dirPath);
56353
+ const expectedParent = await realpath4(memoryDir);
56354
+ const resolvedName = path74.basename(resolvedDir);
54749
56355
  if (resolvedName !== spec.dir) continue;
54750
- if (path73.dirname(resolvedDir) !== expectedParent) continue;
56356
+ if (path74.dirname(resolvedDir) !== expectedParent) continue;
54751
56357
  } catch {
54752
56358
  continue;
54753
56359
  }
54754
56360
  const files = await listMarkdownFilesRecursive(dirPath, dirPath);
54755
56361
  for (const absolutePath of files) {
54756
- const relativePath = path73.relative(memoryDir, absolutePath).replace(/\\/g, "/");
56362
+ const relativePath = path74.relative(memoryDir, absolutePath).replace(/\\/g, "/");
54757
56363
  artifacts.push({
54758
56364
  kind: spec.kind,
54759
56365
  workspaceDir,
@@ -54765,16 +56371,16 @@ async function listRemnicPublicArtifacts(params) {
54765
56371
  }
54766
56372
  }
54767
56373
  for (const spec of PUBLIC_FILES) {
54768
- const absolutePath = path73.join(memoryDir, spec.relativePath);
56374
+ const absolutePath = path74.join(memoryDir, spec.relativePath);
54769
56375
  if (!await pathExists2(absolutePath)) continue;
54770
56376
  if (!await isContainedWithin(absolutePath, memoryDir)) continue;
54771
56377
  try {
54772
- const linkStat = await lstat2(absolutePath);
56378
+ const linkStat = await lstat3(absolutePath);
54773
56379
  if (linkStat.isSymbolicLink()) {
54774
- const resolvedPath = await realpath3(absolutePath);
54775
- const expectedParent = await realpath3(memoryDir);
54776
- if (path73.dirname(resolvedPath) !== expectedParent) continue;
54777
- if (path73.basename(resolvedPath) !== path73.basename(spec.relativePath)) continue;
56380
+ const resolvedPath = await realpath4(absolutePath);
56381
+ const expectedParent = await realpath4(memoryDir);
56382
+ if (path74.dirname(resolvedPath) !== expectedParent) continue;
56383
+ if (path74.basename(resolvedPath) !== path74.basename(spec.relativePath)) continue;
54778
56384
  }
54779
56385
  } catch {
54780
56386
  continue;
@@ -54925,28 +56531,28 @@ var DEFAULT_MAX_BINARY_SIZE_BYTES = 50 * 1024 * 1024;
54925
56531
  // ../remnic-core/src/binary-lifecycle/backend.ts
54926
56532
  import fs3 from "fs";
54927
56533
  import fsp2 from "fs/promises";
54928
- import path74 from "path";
56534
+ import path75 from "path";
54929
56535
 
54930
56536
  // ../remnic-core/src/binary-lifecycle/scanner.ts
54931
56537
  import fsp3 from "fs/promises";
54932
- import path75 from "path";
56538
+ import path76 from "path";
54933
56539
 
54934
56540
  // ../remnic-core/src/binary-lifecycle/manifest.ts
54935
56541
  import fsp4 from "fs/promises";
54936
- import path76 from "path";
56542
+ import path77 from "path";
54937
56543
  import crypto3 from "crypto";
54938
56544
 
54939
56545
  // ../remnic-core/src/binary-lifecycle/pipeline.ts
54940
56546
  import fsp5 from "fs/promises";
54941
- import path77 from "path";
56547
+ import path78 from "path";
54942
56548
  import crypto4 from "crypto";
54943
56549
 
54944
56550
  // ../remnic-core/src/projection/index.ts
54945
56551
  import fs4 from "fs";
54946
- import path79 from "path";
56552
+ import path80 from "path";
54947
56553
 
54948
56554
  // ../remnic-core/src/utils/category-dir.ts
54949
- import path78 from "path";
56555
+ import path79 from "path";
54950
56556
  var CATEGORY_DIR_MAP = {
54951
56557
  correction: "corrections",
54952
56558
  question: "questions",
@@ -54957,7 +56563,8 @@ var CATEGORY_DIR_MAP = {
54957
56563
  principle: "principles",
54958
56564
  rule: "rules",
54959
56565
  skill: "skills",
54960
- relationship: "relationships"
56566
+ relationship: "relationships",
56567
+ procedure: "procedures"
54961
56568
  };
54962
56569
  var ALL_CATEGORY_DIRS = [
54963
56570
  "facts",
@@ -54970,30 +56577,30 @@ var ALL_CATEGORY_KEYS = [
54970
56577
 
54971
56578
  // ../remnic-core/src/onboarding/index.ts
54972
56579
  import fs5 from "fs";
54973
- import path80 from "path";
56580
+ import path81 from "path";
54974
56581
 
54975
56582
  // ../remnic-core/src/curation/index.ts
54976
56583
  import fs6 from "fs";
54977
- import path81 from "path";
56584
+ import path82 from "path";
54978
56585
  import crypto5 from "crypto";
54979
56586
 
54980
56587
  // ../remnic-core/src/dedup/index.ts
54981
56588
  import fs7 from "fs";
54982
- import path82 from "path";
56589
+ import path83 from "path";
54983
56590
  import crypto6 from "crypto";
54984
56591
 
54985
56592
  // ../remnic-core/src/review/index.ts
54986
56593
  import fs8 from "fs";
54987
- import path83 from "path";
56594
+ import path84 from "path";
54988
56595
 
54989
56596
  // ../remnic-core/src/sync/index.ts
54990
56597
  import fs9 from "fs";
54991
- import path84 from "path";
56598
+ import path85 from "path";
54992
56599
  import crypto7 from "crypto";
54993
56600
 
54994
56601
  // ../remnic-core/src/connectors/index.ts
54995
56602
  import fs11 from "fs";
54996
- import path87 from "path";
56603
+ import path88 from "path";
54997
56604
  import os4 from "os";
54998
56605
  import { spawnSync } from "child_process";
54999
56606
  import { createRequire as createRequire2 } from "module";
@@ -55001,31 +56608,31 @@ import { fileURLToPath as fileURLToPath4 } from "url";
55001
56608
 
55002
56609
  // ../remnic-core/src/tokens.ts
55003
56610
  import fs10 from "fs";
55004
- import path85 from "path";
55005
- import { randomBytes as randomBytes2 } from "crypto";
56611
+ import path86 from "path";
56612
+ import { randomBytes as randomBytes3 } from "crypto";
55006
56613
 
55007
56614
  // ../remnic-core/src/connectors/codex-marketplace.ts
55008
56615
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
55009
- import path86 from "path";
56616
+ import path87 from "path";
55010
56617
 
55011
56618
  // ../remnic-core/src/spaces/index.ts
55012
56619
  import fs12 from "fs";
55013
- import path88 from "path";
56620
+ import path89 from "path";
55014
56621
  import crypto8 from "crypto";
55015
56622
 
55016
56623
  // ../remnic-core/src/memory-extension/codex-publisher.ts
55017
56624
  import fs13 from "fs";
55018
56625
  import os5 from "os";
55019
- import path89 from "path";
56626
+ import path90 from "path";
55020
56627
 
55021
56628
  // ../remnic-core/src/taxonomy/taxonomy-loader.ts
55022
- import { readFile as readFile46, mkdir as mkdir49, writeFile as writeFile43 } from "fs/promises";
55023
- import path90 from "path";
56629
+ import { readFile as readFile47, mkdir as mkdir49, writeFile as writeFile43 } from "fs/promises";
56630
+ import path91 from "path";
55024
56631
 
55025
56632
  // ../remnic-core/src/enrichment/audit.ts
55026
- import { mkdir as mkdir50, readFile as readFile47, appendFile as appendFile4 } from "fs/promises";
56633
+ import { mkdir as mkdir50, readFile as readFile48, appendFile as appendFile4 } from "fs/promises";
55027
56634
  import { existsSync as existsSync11 } from "fs";
55028
- import path91 from "path";
56635
+ import path92 from "path";
55029
56636
 
55030
56637
  // src/openclaw-tools/shapes.ts
55031
56638
  var MemorySearchInputSchema = Type.Object({
@@ -55439,7 +57046,7 @@ async function syncHeartbeatOutcomeLinks(params) {
55439
57046
  }
55440
57047
  return { created: 0, updated: 0, linked };
55441
57048
  }
55442
- function parseIsoTimestamp2(value) {
57049
+ function parseIsoTimestamp3(value) {
55443
57050
  if (!value) return null;
55444
57051
  const parsed = Date.parse(value);
55445
57052
  return Number.isFinite(parsed) ? parsed : null;
@@ -55448,7 +57055,7 @@ function planDreamEntryFromConsolidation(params) {
55448
57055
  const now = params.now ?? /* @__PURE__ */ new Date();
55449
57056
  const latestDreamAt = Math.max(
55450
57057
  -1,
55451
- ...params.existingDreams.map((entry) => parseIsoTimestamp2(entry.timestamp)).filter((value) => value !== null)
57058
+ ...params.existingDreams.map((entry) => parseIsoTimestamp3(entry.timestamp)).filter((value) => value !== null)
55452
57059
  );
55453
57060
  if (latestDreamAt > 0 && now.getTime() - latestDreamAt < params.minIntervalMinutes * 6e4) {
55454
57061
  return null;
@@ -55592,88 +57199,100 @@ function resolveSession(commandCtx) {
55592
57199
  };
55593
57200
  }
55594
57201
  function buildSessionCommandDescriptors(pluginId, runtime) {
57202
+ const subcommands = [
57203
+ {
57204
+ name: "off",
57205
+ description: "Disable Remnic recall for this session",
57206
+ args: [],
57207
+ handler: async (commandCtx = {}) => {
57208
+ const { sessionKey, agentId } = resolveSession(commandCtx);
57209
+ await runtime.toggles.setDisabled(sessionKey, agentId, true);
57210
+ return `Remnic recall disabled for session ${sessionKey}.`;
57211
+ }
57212
+ },
57213
+ {
57214
+ name: "on",
57215
+ description: "Re-enable Remnic recall for this session",
57216
+ args: [],
57217
+ handler: async (commandCtx = {}) => {
57218
+ const { sessionKey, agentId } = resolveSession(commandCtx);
57219
+ await runtime.toggles.setDisabled(sessionKey, agentId, false);
57220
+ return `Remnic recall re-enabled for session ${sessionKey}.`;
57221
+ }
57222
+ },
57223
+ {
57224
+ name: "status",
57225
+ description: "Show Remnic recall status and last injected summary",
57226
+ args: [],
57227
+ handler: async (commandCtx = {}) => {
57228
+ const { sessionKey, agentId } = resolveSession(commandCtx);
57229
+ const resolved = await runtime.toggles.resolve(sessionKey, agentId);
57230
+ const lastRecall = runtime.getLastRecall(sessionKey);
57231
+ const summaryText = runtime.getLastRecallSummary(sessionKey);
57232
+ const summary = summaryText && summaryText.length > 0 ? summaryText : lastRecall && lastRecall.memoryIds.length > 0 ? `${lastRecall.memoryIds.length} memory item(s), latency ${lastRecall.latencyMs ?? "?"}ms` : "NONE";
57233
+ return [
57234
+ `Remnic recall is ${resolved.disabled ? "disabled" : "enabled"} for session ${sessionKey}.`,
57235
+ `Source: ${describeToggleSource(resolved.source)}.`,
57236
+ `Last recall: ${summary}.`
57237
+ ].join(" ");
57238
+ }
57239
+ },
57240
+ {
57241
+ name: "clear",
57242
+ description: "Clear the session override and use global config again",
57243
+ args: [],
57244
+ handler: async (commandCtx = {}) => {
57245
+ const { sessionKey, agentId } = resolveSession(commandCtx);
57246
+ await runtime.toggles.clear(sessionKey, agentId);
57247
+ return `Cleared the Remnic session override for ${sessionKey}.`;
57248
+ }
57249
+ },
57250
+ {
57251
+ name: "stats",
57252
+ description: "Show Remnic extraction and recall stats for this session",
57253
+ args: [],
57254
+ handler: async (commandCtx = {}) => {
57255
+ const { sessionKey } = resolveSession(commandCtx);
57256
+ const lastRecall = runtime.getLastRecall(sessionKey);
57257
+ if (!lastRecall) {
57258
+ return `No Remnic recall stats are available for session ${sessionKey} yet.`;
57259
+ }
57260
+ return [
57261
+ `Session ${sessionKey}.`,
57262
+ `Planner mode: ${lastRecall.plannerMode ?? "unknown"}.`,
57263
+ `Latency: ${lastRecall.latencyMs ?? "?"}ms.`,
57264
+ `Memories: ${lastRecall.memoryIds.length}.`
57265
+ ].join(" ");
57266
+ }
57267
+ },
57268
+ {
57269
+ name: "flush",
57270
+ description: "Force-flush the extraction buffer now",
57271
+ args: [],
57272
+ handler: async (commandCtx = {}) => {
57273
+ const { sessionKey } = resolveSession(commandCtx);
57274
+ await runtime.flushSession(sessionKey);
57275
+ return `Flushed the Remnic buffer for session ${sessionKey}.`;
57276
+ }
57277
+ }
57278
+ ];
57279
+ const subcommandNames = subcommands.map((entry) => entry.name).join(", ");
55595
57280
  return [
55596
57281
  {
55597
57282
  name: "remnic",
57283
+ description: `Remnic memory controls (${subcommandNames})`,
55598
57284
  category: "memory",
55599
57285
  pluginId,
55600
- subcommands: [
55601
- {
55602
- name: "off",
55603
- description: "Disable Remnic recall for this session",
55604
- args: [],
55605
- handler: async (commandCtx = {}) => {
55606
- const { sessionKey, agentId } = resolveSession(commandCtx);
55607
- await runtime.toggles.setDisabled(sessionKey, agentId, true);
55608
- return `Remnic recall disabled for session ${sessionKey}.`;
55609
- }
55610
- },
55611
- {
55612
- name: "on",
55613
- description: "Re-enable Remnic recall for this session",
55614
- args: [],
55615
- handler: async (commandCtx = {}) => {
55616
- const { sessionKey, agentId } = resolveSession(commandCtx);
55617
- await runtime.toggles.setDisabled(sessionKey, agentId, false);
55618
- return `Remnic recall re-enabled for session ${sessionKey}.`;
55619
- }
55620
- },
55621
- {
55622
- name: "status",
55623
- description: "Show Remnic recall status and last injected summary",
55624
- args: [],
55625
- handler: async (commandCtx = {}) => {
55626
- const { sessionKey, agentId } = resolveSession(commandCtx);
55627
- const resolved = await runtime.toggles.resolve(sessionKey, agentId);
55628
- const lastRecall = runtime.getLastRecall(sessionKey);
55629
- const summaryText = runtime.getLastRecallSummary(sessionKey);
55630
- const summary = summaryText && summaryText.length > 0 ? summaryText : lastRecall && lastRecall.memoryIds.length > 0 ? `${lastRecall.memoryIds.length} memory item(s), latency ${lastRecall.latencyMs ?? "?"}ms` : "NONE";
55631
- return [
55632
- `Remnic recall is ${resolved.disabled ? "disabled" : "enabled"} for session ${sessionKey}.`,
55633
- `Source: ${describeToggleSource(resolved.source)}.`,
55634
- `Last recall: ${summary}.`
55635
- ].join(" ");
55636
- }
55637
- },
55638
- {
55639
- name: "clear",
55640
- description: "Clear the session override and use global config again",
55641
- args: [],
55642
- handler: async (commandCtx = {}) => {
55643
- const { sessionKey, agentId } = resolveSession(commandCtx);
55644
- await runtime.toggles.clear(sessionKey, agentId);
55645
- return `Cleared the Remnic session override for ${sessionKey}.`;
55646
- }
55647
- },
55648
- {
55649
- name: "stats",
55650
- description: "Show Remnic extraction and recall stats for this session",
55651
- args: [],
55652
- handler: async (commandCtx = {}) => {
55653
- const { sessionKey } = resolveSession(commandCtx);
55654
- const lastRecall = runtime.getLastRecall(sessionKey);
55655
- if (!lastRecall) {
55656
- return `No Remnic recall stats are available for session ${sessionKey} yet.`;
55657
- }
55658
- return [
55659
- `Session ${sessionKey}.`,
55660
- `Planner mode: ${lastRecall.plannerMode ?? "unknown"}.`,
55661
- `Latency: ${lastRecall.latencyMs ?? "?"}ms.`,
55662
- `Memories: ${lastRecall.memoryIds.length}.`
55663
- ].join(" ");
55664
- }
55665
- },
55666
- {
55667
- name: "flush",
55668
- description: "Force-flush the extraction buffer now",
55669
- args: [],
55670
- handler: async (commandCtx = {}) => {
55671
- const { sessionKey } = resolveSession(commandCtx);
55672
- await runtime.flushSession(sessionKey);
55673
- return `Flushed the Remnic buffer for session ${sessionKey}.`;
55674
- }
57286
+ acceptsArgs: true,
57287
+ subcommands,
57288
+ handler: async (commandCtx = {}) => {
57289
+ const requested = commandCtx.args?.[0]?.trim().toLowerCase() ?? "status";
57290
+ const match = subcommands.find((entry) => entry.name === requested);
57291
+ if (!match) {
57292
+ return `Unknown Remnic subcommand "${requested}". Try one of: ${subcommandNames}.`;
55675
57293
  }
55676
- ]
57294
+ return match.handler(commandCtx);
57295
+ }
55677
57296
  }
55678
57297
  ];
55679
57298
  }
@@ -55773,8 +57392,8 @@ function validateSlotSelection(ctx) {
55773
57392
  }
55774
57393
 
55775
57394
  // ../remnic-core/src/session-toggles.ts
55776
- import { mkdir as mkdir51, readFile as readFile48, writeFile as writeFile44 } from "fs/promises";
55777
- import path92 from "path";
57395
+ import { mkdir as mkdir51, readFile as readFile49, writeFile as writeFile44 } from "fs/promises";
57396
+ import path93 from "path";
55778
57397
  function encodeToggleKey(sessionKey, agentId) {
55779
57398
  return `${encodeURIComponent(sessionKey)}::${encodeURIComponent(agentId)}`;
55780
57399
  }
@@ -55788,7 +57407,7 @@ function decodeToggleKey(key) {
55788
57407
  }
55789
57408
  async function safeReadToggleFile(filePath) {
55790
57409
  try {
55791
- const raw = await readFile48(filePath, "utf8");
57410
+ const raw = await readFile49(filePath, "utf8");
55792
57411
  const parsed = JSON.parse(raw);
55793
57412
  if (!parsed || typeof parsed !== "object" || typeof parsed.entries !== "object") {
55794
57413
  return { version: 1, entries: {} };
@@ -55813,7 +57432,7 @@ function createFileToggleStore(filePath, options = {}) {
55813
57432
  await run;
55814
57433
  }
55815
57434
  async function writeToggleFile(next) {
55816
- await mkdir51(path92.dirname(filePath), { recursive: true });
57435
+ await mkdir51(path93.dirname(filePath), { recursive: true });
55817
57436
  await writeFile44(filePath, JSON.stringify(next, null, 2), "utf8");
55818
57437
  }
55819
57438
  async function readPrimary() {
@@ -55886,8 +57505,8 @@ function createFileToggleStore(filePath, options = {}) {
55886
57505
  }
55887
57506
 
55888
57507
  // ../remnic-core/src/recall-audit.ts
55889
- import { appendFile as appendFile5, mkdir as mkdir52, readdir as readdir27, rm as rm10 } from "fs/promises";
55890
- import path93 from "path";
57508
+ import { appendFile as appendFile5, mkdir as mkdir52, readdir as readdir28, rm as rm10 } from "fs/promises";
57509
+ import path94 from "path";
55891
57510
  function formatIsoDate(ts) {
55892
57511
  const normalized = new Date(ts);
55893
57512
  if (Number.isNaN(normalized.getTime())) {
@@ -55897,24 +57516,24 @@ function formatIsoDate(ts) {
55897
57516
  }
55898
57517
  function buildRecallAuditPath(rootDir, ts, sessionKey) {
55899
57518
  const safeSessionKey = encodeURIComponent(sessionKey);
55900
- return path93.join(rootDir, "transcripts", formatIsoDate(ts), `${safeSessionKey}.jsonl`);
57519
+ return path94.join(rootDir, "transcripts", formatIsoDate(ts), `${safeSessionKey}.jsonl`);
55901
57520
  }
55902
57521
  async function appendRecallAuditEntry(rootDir, entry) {
55903
57522
  const filePath = buildRecallAuditPath(rootDir, entry.ts, entry.sessionKey);
55904
- await mkdir52(path93.dirname(filePath), { recursive: true });
57523
+ await mkdir52(path94.dirname(filePath), { recursive: true });
55905
57524
  await appendFile5(filePath, `${JSON.stringify(entry)}
55906
57525
  `, "utf8");
55907
57526
  return filePath;
55908
57527
  }
55909
57528
  async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE__ */ new Date()) {
55910
- const transcriptsDir = path93.join(rootDir, "transcripts");
57529
+ const transcriptsDir = path94.join(rootDir, "transcripts");
55911
57530
  const removed = [];
55912
57531
  const cutoff = new Date(now);
55913
57532
  cutoff.setUTCHours(0, 0, 0, 0);
55914
57533
  cutoff.setUTCDate(cutoff.getUTCDate() - Math.max(1, Math.floor(retentionDays)));
55915
57534
  let entries;
55916
57535
  try {
55917
- entries = await readdir27(transcriptsDir, { withFileTypes: true });
57536
+ entries = await readdir28(transcriptsDir, { withFileTypes: true });
55918
57537
  } catch {
55919
57538
  return removed;
55920
57539
  }
@@ -55923,7 +57542,7 @@ async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE_
55923
57542
  if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.name)) continue;
55924
57543
  const day = /* @__PURE__ */ new Date(`${entry.name}T00:00:00.000Z`);
55925
57544
  if (Number.isNaN(day.getTime()) || day >= cutoff) continue;
55926
- const dirPath = path93.join(transcriptsDir, entry.name);
57545
+ const dirPath = path94.join(transcriptsDir, entry.name);
55927
57546
  await rm10(dirPath, { recursive: true, force: true });
55928
57547
  removed.push(dirPath);
55929
57548
  }
@@ -55932,7 +57551,7 @@ async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE_
55932
57551
 
55933
57552
  // ../remnic-core/src/active-recall.ts
55934
57553
  import { appendFile as appendFile6, mkdir as mkdir53 } from "fs/promises";
55935
- import path94 from "path";
57554
+ import path95 from "path";
55936
57555
  var ACTIVE_RECALL_CACHE_MAX_ENTRIES = 256;
55937
57556
  var NONE_SET = /* @__PURE__ */ new Set([
55938
57557
  "",
@@ -56075,14 +57694,14 @@ ${params.recallExplain}` : null,
56075
57694
  }
56076
57695
  async function appendActiveRecallTranscript(transcriptRoot, input, config, result, queryBundle) {
56077
57696
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
56078
- const filePath = path94.join(
57697
+ const filePath = path95.join(
56079
57698
  transcriptRoot,
56080
57699
  "agents",
56081
57700
  sanitizeTranscriptPathSegment(input.agentId),
56082
57701
  date,
56083
57702
  `${sanitizeTranscriptPathSegment(input.sessionKey)}.jsonl`
56084
57703
  );
56085
- await mkdir53(path94.dirname(filePath), { recursive: true });
57704
+ await mkdir53(path95.dirname(filePath), { recursive: true });
56086
57705
  await appendFile6(
56087
57706
  filePath,
56088
57707
  `${JSON.stringify({
@@ -56314,8 +57933,8 @@ import { resolvePrincipal as resolvePrincipal2 } from "@remnic/core";
56314
57933
  // ../remnic-core/src/surfaces/dreams.ts
56315
57934
  import { createHash as createHash16 } from "crypto";
56316
57935
  import { statSync, watch as watch2 } from "fs";
56317
- import { mkdir as mkdir54, readFile as readFile49, writeFile as writeFile45 } from "fs/promises";
56318
- import path95 from "path";
57936
+ import { mkdir as mkdir54, readFile as readFile50, writeFile as writeFile45 } from "fs/promises";
57937
+ import path96 from "path";
56319
57938
  var DIARY_START_MARKER = "<!-- openclaw:dreaming:diary:start -->";
56320
57939
  var DIARY_END_MARKER = "<!-- openclaw:dreaming:diary:end -->";
56321
57940
  function stableDreamId(params) {
@@ -56480,7 +58099,7 @@ function createDreamsSurface() {
56480
58099
  return {
56481
58100
  async read(filePath) {
56482
58101
  try {
56483
- const content = await readFile49(filePath, "utf8");
58102
+ const content = await readFile50(filePath, "utf8");
56484
58103
  return parseDreamEntries(content);
56485
58104
  } catch (error) {
56486
58105
  if (error.code === "ENOENT") {
@@ -56490,10 +58109,10 @@ function createDreamsSurface() {
56490
58109
  }
56491
58110
  },
56492
58111
  async append(filePath, entry) {
56493
- await mkdir54(path95.dirname(filePath), { recursive: true });
58112
+ await mkdir54(path96.dirname(filePath), { recursive: true });
56494
58113
  let content = "";
56495
58114
  try {
56496
- content = await readFile49(filePath, "utf8");
58115
+ content = await readFile50(filePath, "utf8");
56497
58116
  } catch (error) {
56498
58117
  if (error.code !== "ENOENT") throw error;
56499
58118
  }
@@ -56511,22 +58130,22 @@ ${ensured.slice(endIndex)}` : `${ensureDiary("")}${block}`;
56511
58130
  let fileWatcher = null;
56512
58131
  let parentWatcher = null;
56513
58132
  let timer = null;
56514
- const watchedName = path95.basename(filePath);
56515
- const watchedDir = path95.dirname(filePath);
58133
+ const watchedName = path96.basename(filePath);
58134
+ const watchedDir = path96.dirname(filePath);
56516
58135
  const resolveParentWatchTarget = () => {
56517
58136
  let candidateDir = watchedDir;
56518
58137
  while (true) {
56519
58138
  try {
56520
58139
  if (statSync(candidateDir).isDirectory()) {
56521
- const relative = path95.relative(candidateDir, watchedDir);
58140
+ const relative = path96.relative(candidateDir, watchedDir);
56522
58141
  return {
56523
58142
  dir: candidateDir,
56524
- expectedName: relative.length === 0 ? watchedName : relative.split(path95.sep)[0] ?? watchedName
58143
+ expectedName: relative.length === 0 ? watchedName : relative.split(path96.sep)[0] ?? watchedName
56525
58144
  };
56526
58145
  }
56527
58146
  } catch {
56528
58147
  }
56529
- const parentDir = path95.dirname(candidateDir);
58148
+ const parentDir = path96.dirname(candidateDir);
56530
58149
  if (parentDir === candidateDir) {
56531
58150
  return null;
56532
58151
  }
@@ -56591,8 +58210,8 @@ ${ensured.slice(endIndex)}` : `${ensureDiary("")}${block}`;
56591
58210
  // ../remnic-core/src/surfaces/heartbeat.ts
56592
58211
  import { createHash as createHash17 } from "crypto";
56593
58212
  import { statSync as statSync2, watch as watch3 } from "fs";
56594
- import { readFile as readFile50 } from "fs/promises";
56595
- import path96 from "path";
58213
+ import { readFile as readFile51 } from "fs/promises";
58214
+ import path97 from "path";
56596
58215
  function stableHeartbeatId(params) {
56597
58216
  const digest = createHash17("sha1").update(
56598
58217
  JSON.stringify({
@@ -56755,7 +58374,7 @@ function createHeartbeatSurface() {
56755
58374
  return {
56756
58375
  async read(filePath) {
56757
58376
  try {
56758
- const content = await readFile50(filePath, "utf8");
58377
+ const content = await readFile51(filePath, "utf8");
56759
58378
  return parseHeartbeatEntries(content);
56760
58379
  } catch (error) {
56761
58380
  if (error.code === "ENOENT") {
@@ -56768,22 +58387,22 @@ function createHeartbeatSurface() {
56768
58387
  let fileWatcher = null;
56769
58388
  let parentWatcher = null;
56770
58389
  let timer = null;
56771
- const watchedName = path96.basename(filePath);
56772
- const watchedDir = path96.dirname(filePath);
58390
+ const watchedName = path97.basename(filePath);
58391
+ const watchedDir = path97.dirname(filePath);
56773
58392
  const resolveParentWatchTarget = () => {
56774
58393
  let candidateDir = watchedDir;
56775
58394
  while (true) {
56776
58395
  try {
56777
58396
  if (statSync2(candidateDir).isDirectory()) {
56778
- const relative = path96.relative(candidateDir, watchedDir);
58397
+ const relative = path97.relative(candidateDir, watchedDir);
56779
58398
  return {
56780
58399
  dir: candidateDir,
56781
- expectedName: relative.length === 0 ? watchedName : relative.split(path96.sep)[0] ?? watchedName
58400
+ expectedName: relative.length === 0 ? watchedName : relative.split(path97.sep)[0] ?? watchedName
56782
58401
  };
56783
58402
  }
56784
58403
  } catch {
56785
58404
  }
56786
- const parentDir = path96.dirname(candidateDir);
58405
+ const parentDir = path97.dirname(candidateDir);
56787
58406
  if (parentDir === candidateDir) {
56788
58407
  return null;
56789
58408
  }
@@ -56869,7 +58488,7 @@ function loadPluginEntryFromFile(pluginId) {
56869
58488
  try {
56870
58489
  const explicitConfigPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH");
56871
58490
  const homeDir = resolveHomeDir();
56872
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path97.join(homeDir, ".openclaw", "openclaw.json");
58491
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path98.join(homeDir, ".openclaw", "openclaw.json");
56873
58492
  const content = readFileSync6(configPath, "utf-8");
56874
58493
  const config = JSON.parse(content);
56875
58494
  return resolveRemnicPluginEntry(config, pluginId);
@@ -56885,7 +58504,7 @@ function loadRawConfigFromFile() {
56885
58504
  try {
56886
58505
  const explicitConfigPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH");
56887
58506
  const homeDir = resolveHomeDir();
56888
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path97.join(homeDir, ".openclaw", "openclaw.json");
58507
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path98.join(homeDir, ".openclaw", "openclaw.json");
56889
58508
  const content = readFileSync6(configPath, "utf-8");
56890
58509
  const config = JSON.parse(content);
56891
58510
  return config && typeof config === "object" ? config : void 0;
@@ -57119,9 +58738,9 @@ var pluginDefinition = {
57119
58738
  citationsAutoDetect: cfg.citationsAutoDetect
57120
58739
  });
57121
58740
  globalThis[keys.ACCESS_HTTP_SERVER] = accessHttpServer;
57122
- const pluginStateDir = path97.join(cfg.memoryDir, "state", "plugins", serviceId);
57123
- const togglePrimaryPath = path97.join(pluginStateDir, "session-toggles.json");
57124
- const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path97.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
58741
+ const pluginStateDir = path98.join(cfg.memoryDir, "state", "plugins", serviceId);
58742
+ const togglePrimaryPath = path98.join(pluginStateDir, "session-toggles.json");
58743
+ const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path98.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
57125
58744
  const sessionToggleStore = createFileToggleStore(togglePrimaryPath, {
57126
58745
  secondaryReadOnlyPath: toggleSecondaryPath
57127
58746
  });
@@ -57143,11 +58762,11 @@ var pluginDefinition = {
57143
58762
  }
57144
58763
  function resolveDreamJournalPath(runtimeWorkspaceDir) {
57145
58764
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
57146
- return path97.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path97.join(workspaceRoot, cfg.dreaming.journalPath);
58765
+ return path98.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path98.join(workspaceRoot, cfg.dreaming.journalPath);
57147
58766
  }
57148
58767
  function resolveHeartbeatJournalPath(runtimeWorkspaceDir) {
57149
58768
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
57150
- return path97.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path97.join(workspaceRoot, cfg.heartbeat.journalPath);
58769
+ return path98.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path98.join(workspaceRoot, cfg.heartbeat.journalPath);
57151
58770
  }
57152
58771
  function queueDreamSurfaceSync(runtimeWorkspaceDir) {
57153
58772
  if (!cfg.dreaming.enabled) return Promise.resolve();
@@ -57305,7 +58924,7 @@ Keep the reflection grounded in the evidence below.
57305
58924
  timeoutMs: cfg.activeRecallTimeoutMs,
57306
58925
  cacheTtlMs: cfg.activeRecallCacheTtlMs,
57307
58926
  persistTranscripts: cfg.activeRecallPersistTranscripts,
57308
- transcriptDir: path97.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path97.join(pluginStateDir, cfg.activeRecallTranscriptDir),
58927
+ transcriptDir: path98.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path98.join(pluginStateDir, cfg.activeRecallTranscriptDir),
57309
58928
  entityGraphDepth: cfg.activeRecallEntityGraphDepth,
57310
58929
  includeCausalTrajectories: cfg.activeRecallIncludeCausalTrajectories,
57311
58930
  includeDaySummary: cfg.activeRecallIncludeDaySummary,
@@ -58173,6 +59792,219 @@ Keep the reflection grounded in the evidence below.
58173
59792
  const runtimeAgentId = typeof runtimeAgent?.id === "string" && runtimeAgent.id.length > 0 ? runtimeAgent.id : void 0;
58174
59793
  const capabilityAgentIds = runtimeAgentId ? [runtimeAgentId] : ["generalist"];
58175
59794
  const capabilityWorkspaceDir = (typeof runtimeAgent?.workspaceDir === "string" && runtimeAgent.workspaceDir.length > 0 ? runtimeAgent.workspaceDir : void 0) ?? orchestrator.config.workspaceDir ?? defaultWorkspaceDir();
59795
+ const remnicUsesQmd = (orchestrator.config.searchBackend ?? "qmd") === "qmd" && orchestrator.config.qmdEnabled !== false;
59796
+ const remnicQmdCommand = typeof orchestrator.config.qmdPath === "string" && orchestrator.config.qmdPath.trim().length > 0 ? orchestrator.config.qmdPath.trim() : "qmd";
59797
+ const readAllowedRoots = [
59798
+ orchestrator.config.memoryDir,
59799
+ capabilityWorkspaceDir ? path98.join(capabilityWorkspaceDir, "memory") : void 0
59800
+ ].filter((root) => typeof root === "string" && root.length > 0);
59801
+ const canonicalizeRootForContainment = async (rawPath) => {
59802
+ const resolved = path98.resolve(rawPath);
59803
+ try {
59804
+ return path98.normalize(await realpath5(resolved));
59805
+ } catch {
59806
+ return path98.normalize(resolved);
59807
+ }
59808
+ };
59809
+ const canonicalizeForRead = async (rawPath) => {
59810
+ const resolved = path98.resolve(rawPath);
59811
+ const real = await realpath5(resolved);
59812
+ return path98.normalize(real);
59813
+ };
59814
+ const readAllowedCanonicalRootsPromise = Promise.all(
59815
+ readAllowedRoots.map((root) => canonicalizeRootForContainment(root))
59816
+ );
59817
+ const isWithinAllowedRoot = async (candidatePath) => {
59818
+ let canonicalCandidatePath;
59819
+ try {
59820
+ canonicalCandidatePath = await canonicalizeForRead(candidatePath);
59821
+ } catch {
59822
+ return false;
59823
+ }
59824
+ const canonicalRoots = await readAllowedCanonicalRootsPromise;
59825
+ return canonicalRoots.some((root) => {
59826
+ const relative = path98.relative(root, canonicalCandidatePath);
59827
+ return relative === "" || !relative.startsWith("..") && !path98.isAbsolute(relative);
59828
+ });
59829
+ };
59830
+ const normalizeWorkspacePath = (rawPath) => {
59831
+ if (!rawPath || typeof rawPath !== "string") return "memory";
59832
+ const resolved = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : path98.resolve(capabilityWorkspaceDir, rawPath);
59833
+ const relative = path98.relative(capabilityWorkspaceDir, resolved);
59834
+ return relative && !relative.startsWith("..") && !path98.isAbsolute(relative) ? relative : rawPath;
59835
+ };
59836
+ const relativizeToMemoryRoot = (rawPath) => {
59837
+ if (!rawPath || typeof rawPath !== "string") return "memory";
59838
+ const resolved = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : path98.resolve(capabilityWorkspaceDir, rawPath);
59839
+ for (const root of readAllowedRoots) {
59840
+ const relative = path98.relative(root, resolved);
59841
+ if (relative !== "" && !relative.startsWith("..") && !path98.isAbsolute(relative)) {
59842
+ return relative;
59843
+ }
59844
+ }
59845
+ return normalizeWorkspacePath(rawPath);
59846
+ };
59847
+ const resolveReadablePath = async (requestedPath) => {
59848
+ const candidateAbsolutePaths = path98.isAbsolute(requestedPath) ? [path98.resolve(requestedPath)] : readAllowedRoots.map((root) => path98.resolve(root, requestedPath));
59849
+ let canonicalPath;
59850
+ let lastError;
59851
+ for (const absolutePath of candidateAbsolutePaths) {
59852
+ try {
59853
+ canonicalPath = await canonicalizeForRead(absolutePath);
59854
+ break;
59855
+ } catch (err) {
59856
+ lastError = err;
59857
+ }
59858
+ }
59859
+ if (canonicalPath === void 0) {
59860
+ throw new Error(
59861
+ `memory read rejected (path unresolvable): ${requestedPath}`
59862
+ );
59863
+ }
59864
+ const canonicalRoots = await readAllowedCanonicalRootsPromise;
59865
+ const contained = canonicalRoots.some((root) => {
59866
+ const relative = path98.relative(root, canonicalPath);
59867
+ return relative === "" || !relative.startsWith("..") && !path98.isAbsolute(relative);
59868
+ });
59869
+ if (!contained) {
59870
+ throw new Error(`memory read outside allowed roots: ${requestedPath}`);
59871
+ }
59872
+ if (!canonicalPath.toLowerCase().endsWith(".md")) {
59873
+ throw new Error(
59874
+ `memory read restricted to .md files: ${requestedPath}`
59875
+ );
59876
+ }
59877
+ return canonicalPath;
59878
+ };
59879
+ const remnicMemoryRuntime = {
59880
+ async getMemorySearchManager(_params) {
59881
+ return {
59882
+ manager: {
59883
+ async search(query, opts) {
59884
+ const namespace = typeof orchestrator.resolveSelfNamespace === "function" ? orchestrator.resolveSelfNamespace(opts?.sessionKey) : void 0;
59885
+ const resolvedMode = opts?.qmdSearchModeOverride === "vsearch" ? "vector" : opts?.qmdSearchModeOverride === "query" ? "search" : opts?.qmdSearchModeOverride ?? "search";
59886
+ const rawResults = await orchestrator.searchAcrossNamespaces({
59887
+ query,
59888
+ maxResults: opts?.maxResults,
59889
+ namespaces: namespace ? [namespace] : void 0,
59890
+ mode: resolvedMode
59891
+ });
59892
+ const isArtifactPath2 = (p) => /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(p);
59893
+ return rawResults.filter((result) => {
59894
+ const candidate = result;
59895
+ const p = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : "";
59896
+ return !isArtifactPath2(p);
59897
+ }).map((result, index) => {
59898
+ const candidate = result;
59899
+ const rawPath = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : `memory-${index + 1}`;
59900
+ const absolutePath = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : (() => {
59901
+ for (const root of readAllowedRoots) {
59902
+ const candidateAbs = path98.resolve(root, rawPath);
59903
+ const relative = path98.relative(root, candidateAbs);
59904
+ if (!relative.startsWith("..") && !path98.isAbsolute(relative)) {
59905
+ return candidateAbs;
59906
+ }
59907
+ }
59908
+ return path98.resolve(capabilityWorkspaceDir, rawPath);
59909
+ })();
59910
+ const normalizedPath = relativizeToMemoryRoot(rawPath);
59911
+ const startLine = typeof candidate.startLine === "number" && Number.isFinite(candidate.startLine) ? Math.max(1, Math.floor(candidate.startLine)) : 1;
59912
+ const endLine = typeof candidate.endLine === "number" && Number.isFinite(candidate.endLine) ? Math.max(startLine, Math.floor(candidate.endLine)) : startLine;
59913
+ return {
59914
+ path: absolutePath,
59915
+ startLine,
59916
+ endLine,
59917
+ score: typeof candidate.score === "number" && Number.isFinite(candidate.score) ? candidate.score : 0,
59918
+ snippet: typeof candidate.snippet === "string" ? candidate.snippet : typeof candidate.text === "string" ? candidate.text : "",
59919
+ source: normalizedPath.includes("sessions/") ? "sessions" : "memory",
59920
+ citation: normalizedPath
59921
+ };
59922
+ }).filter(
59923
+ (result) => typeof opts?.minScore === "number" && Number.isFinite(opts.minScore) ? result.score >= opts.minScore : true
59924
+ );
59925
+ },
59926
+ async readFile(params) {
59927
+ const requestedPath = normalizeWorkspacePath(params.relPath);
59928
+ const absolutePath = await resolveReadablePath(params.relPath);
59929
+ const text = await readFile52(absolutePath, "utf-8");
59930
+ const allLines = text.split(/\r?\n/);
59931
+ const from = typeof params.from === "number" ? Math.max(1, Math.floor(params.from)) : 1;
59932
+ const lines = typeof params.lines === "number" && Number.isFinite(params.lines) ? Math.max(1, Math.floor(params.lines)) : void 0;
59933
+ const startIndex = from - 1;
59934
+ const endIndex = typeof lines === "number" ? startIndex + lines : allLines.length;
59935
+ const slice = allLines.slice(startIndex, endIndex);
59936
+ const truncated = endIndex < allLines.length;
59937
+ return {
59938
+ text: slice.join("\n"),
59939
+ path: requestedPath,
59940
+ truncated: truncated || void 0,
59941
+ from,
59942
+ lines,
59943
+ nextFrom: truncated ? endIndex + 1 : void 0
59944
+ };
59945
+ },
59946
+ status() {
59947
+ const qmdAvailable = remnicUsesQmd && typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : !remnicUsesQmd;
59948
+ const qmdDebug = typeof orchestrator.qmd?.debugStatus === "function" ? orchestrator.qmd.debugStatus() : void 0;
59949
+ return {
59950
+ backend: remnicUsesQmd ? "qmd" : "builtin",
59951
+ provider: remnicUsesQmd ? "qmd" : "builtin",
59952
+ requestedProvider: remnicUsesQmd ? "qmd" : "builtin",
59953
+ model: remnicUsesQmd ? remnicQmdCommand : "builtin",
59954
+ dirty: false,
59955
+ workspaceDir: capabilityWorkspaceDir,
59956
+ dbPath: orchestrator.config.memoryDir,
59957
+ sources: ["memory"],
59958
+ sourceCounts: [],
59959
+ vector: remnicUsesQmd ? {
59960
+ enabled: true,
59961
+ available: qmdAvailable
59962
+ } : {
59963
+ enabled: false
59964
+ },
59965
+ fts: {
59966
+ enabled: true,
59967
+ available: qmdAvailable
59968
+ },
59969
+ custom: {
59970
+ remnic: {
59971
+ qmdAvailable,
59972
+ qmdDebug,
59973
+ memoryDir: orchestrator.config.memoryDir
59974
+ }
59975
+ }
59976
+ };
59977
+ },
59978
+ async sync(_params2) {
59979
+ if (remnicUsesQmd && typeof orchestrator.qmd?.update === "function") {
59980
+ await orchestrator.qmd.update();
59981
+ }
59982
+ },
59983
+ async probeEmbeddingAvailability() {
59984
+ if (!remnicUsesQmd) return { ok: true };
59985
+ const qmdAvailable = typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : false;
59986
+ if (qmdAvailable) return { ok: true };
59987
+ const qmdDebug = typeof orchestrator.qmd?.debugStatus === "function" ? orchestrator.qmd.debugStatus() : void 0;
59988
+ return {
59989
+ ok: false,
59990
+ error: qmdDebug ?? "Remnic QMD backend unavailable"
59991
+ };
59992
+ },
59993
+ async probeVectorAvailability() {
59994
+ if (!remnicUsesQmd) return false;
59995
+ return typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : false;
59996
+ },
59997
+ async close() {
59998
+ }
59999
+ }
60000
+ };
60001
+ },
60002
+ resolveMemoryBackendConfig(_params) {
60003
+ return remnicUsesQmd ? { backend: "qmd", qmd: { command: remnicQmdCommand } } : { backend: "builtin" };
60004
+ },
60005
+ async closeAllMemorySearchManagers() {
60006
+ }
60007
+ };
58176
60008
  const memoryCapability = {
58177
60009
  // Include the promptBuilder so runtimes that treat unified capability
58178
60010
  // registration as authoritative (SDK >=2026.4.5) continue to inject
@@ -58180,6 +60012,7 @@ Keep the reflection grounded in the evidence below.
58180
60012
  // Respect promptInjectionAllowed policy — omit promptBuilder if injection
58181
60013
  // is disabled, so the capability only provides publicArtifacts.
58182
60014
  ...promptInjectionAllowed ? { promptBuilder: capabilityPromptBuilder } : {},
60015
+ runtime: remnicMemoryRuntime,
58183
60016
  publicArtifacts: {
58184
60017
  listArtifacts: async (_params) => {
58185
60018
  try {
@@ -58197,7 +60030,7 @@ Keep the reflection grounded in the evidence below.
58197
60030
  };
58198
60031
  api.registerMemoryCapability(memoryCapability);
58199
60032
  const builderDesc = !promptInjectionAllowed ? " (promptBuilder omitted \u2014 injection disabled by policy)" : memoryPromptBuilder ? " and promptBuilder (from registerMemoryPromptSection)" : " and promptBuilder (capability-only fallback)";
58200
- log.info(`registered memory capability with publicArtifacts provider${builderDesc}`);
60033
+ log.info(`registered memory capability with runtime and publicArtifacts provider${builderDesc}`);
58201
60034
  }
58202
60035
  api.on(
58203
60036
  "agent_end",
@@ -58494,7 +60327,7 @@ Keep the reflection grounded in the evidence below.
58494
60327
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
58495
60328
  );
58496
60329
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
58497
- const signalPath = path97.join(
60330
+ const signalPath = path98.join(
58498
60331
  workspaceDir,
58499
60332
  `.compaction-reset-signal-${safeSessionKey}`
58500
60333
  );
@@ -58746,7 +60579,7 @@ Keep the reflection grounded in the evidence below.
58746
60579
  }
58747
60580
  async function ensureHourlySummaryCron(api2) {
58748
60581
  const jobId = "engram-hourly-summary";
58749
- const cronFilePath = path97.join(
60582
+ const cronFilePath = path98.join(
58750
60583
  os6.homedir(),
58751
60584
  ".openclaw",
58752
60585
  "cron",
@@ -58758,7 +60591,7 @@ Keep the reflection grounded in the evidence below.
58758
60591
  jobs: []
58759
60592
  };
58760
60593
  try {
58761
- const content = await readFile51(cronFilePath, "utf-8");
60594
+ const content = await readFile52(cronFilePath, "utf-8");
58762
60595
  jobsData = JSON.parse(content);
58763
60596
  } catch {
58764
60597
  }
@@ -58773,7 +60606,7 @@ Keep the reflection grounded in the evidence below.
58773
60606
  id: jobId,
58774
60607
  agentId: "generalist",
58775
60608
  model,
58776
- name: "Engram Hourly Summary",
60609
+ name: "Remnic Hourly Summary",
58777
60610
  enabled: true,
58778
60611
  createdAtMs: Date.now(),
58779
60612
  updatedAtMs: Date.now(),
@@ -58789,7 +60622,7 @@ Keep the reflection grounded in the evidence below.
58789
60622
  kind: "agentTurn",
58790
60623
  timeoutSeconds: 120,
58791
60624
  thinking: "off",
58792
- message: "You are OpenClaw automation.\n\nTask: Generate Engram hourly summaries.\n\nCall the tool `memory_summarize_hourly` with empty params.\n\nOutput policy:\n- If you generated summaries successfully: output exactly NO_REPLY.\n- If there is an error: output one concise line describing it.\n\nRules:\n- Do NOT send anything to Discord.\n- Never print secrets.\n"
60625
+ message: "You are OpenClaw automation.\n\nTask: Generate Remnic hourly summaries.\n\nCall the tool `memory_summarize_hourly` with empty params.\n\nOutput policy:\n- If you generated summaries successfully: output exactly NO_REPLY.\n- If there is an error: output one concise line describing it.\n\nRules:\n- Do NOT send anything to Discord.\n- Never print secrets.\n"
58793
60626
  },
58794
60627
  delivery: { mode: "none" },
58795
60628
  state: {}
@@ -58959,39 +60792,33 @@ Keep the reflection grounded in the evidence below.
58959
60792
  stop: async () => {
58960
60793
  if (!didCountStart) return;
58961
60794
  didCountStart = false;
60795
+ const initPromiseAtStopEntry = globalThis[keys.INIT_PROMISE];
60796
+ try {
60797
+ await orchestrator.deferredReady;
60798
+ } catch {
60799
+ }
58962
60800
  const remainingServices = Math.max(
58963
60801
  0,
58964
60802
  (globalThis[CLI_ACTIVE_SERVICE_COUNT] || 0) - 1
58965
60803
  );
58966
60804
  globalThis[CLI_ACTIVE_SERVICE_COUNT] = remainingServices;
58967
- try {
58968
- activeOpikExporter?.unsubscribe();
58969
- } catch (err) {
58970
- log.debug(`engram opik exporter unsubscribe failed: ${err}`);
58971
- }
58972
- activeOpikExporter = null;
58973
- try {
58974
- await accessHttpServer.stop();
58975
- } catch (err) {
58976
- log.debug(`engram access HTTP stop failed: ${err}`);
58977
- }
58978
- stopDreamWatcher?.();
58979
- stopDreamWatcher = null;
58980
- stopHeartbeatWatcher?.();
58981
- stopHeartbeatWatcher = null;
58982
- removeDreamingObserver?.();
58983
- removeDreamingObserver = null;
58984
- delete globalThis[keys.ACCESS_HTTP_SERVER];
58985
- delete globalThis[keys.ACCESS_SERVICE];
58986
- const currentInitPromise = globalThis[keys.INIT_PROMISE];
60805
+ const currentInitPromise = initPromiseAtStopEntry;
58987
60806
  let secondaryTookOver = false;
60807
+ if (!currentInitPromise && globalThis[keys.INIT_PROMISE]) {
60808
+ secondaryTookOver = true;
60809
+ }
58988
60810
  if (!currentInitPromise) {
58989
- globalThis[keys.REGISTERED_GUARD] = false;
58990
- if (remainingServices === 0) {
58991
- globalThis[CLI_REGISTERED_GUARD] = false;
58992
- globalThis[SESSION_COMMANDS_REGISTERED_GUARD] = false;
60811
+ if (!secondaryTookOver) {
60812
+ globalThis[keys.REGISTERED_GUARD] = false;
60813
+ if (remainingServices === 0) {
60814
+ globalThis[CLI_REGISTERED_GUARD] = false;
60815
+ globalThis[SESSION_COMMANDS_REGISTERED_GUARD] = false;
60816
+ }
58993
60817
  }
58994
60818
  } else {
60819
+ if (globalThis[keys.INIT_PROMISE] && globalThis[keys.INIT_PROMISE] !== currentInitPromise) {
60820
+ secondaryTookOver = true;
60821
+ }
58995
60822
  try {
58996
60823
  await currentInitPromise;
58997
60824
  } catch {
@@ -59001,6 +60828,27 @@ Keep the reflection grounded in the evidence below.
59001
60828
  secondaryTookOver = true;
59002
60829
  }
59003
60830
  }
60831
+ if (!secondaryTookOver) {
60832
+ try {
60833
+ activeOpikExporter?.unsubscribe();
60834
+ } catch (err) {
60835
+ log.debug(`engram opik exporter unsubscribe failed: ${err}`);
60836
+ }
60837
+ activeOpikExporter = null;
60838
+ try {
60839
+ await accessHttpServer.stop();
60840
+ } catch (err) {
60841
+ log.debug(`engram access HTTP stop failed: ${err}`);
60842
+ }
60843
+ stopDreamWatcher?.();
60844
+ stopDreamWatcher = null;
60845
+ stopHeartbeatWatcher?.();
60846
+ stopHeartbeatWatcher = null;
60847
+ removeDreamingObserver?.();
60848
+ removeDreamingObserver = null;
60849
+ delete globalThis[keys.ACCESS_HTTP_SERVER];
60850
+ delete globalThis[keys.ACCESS_SERVICE];
60851
+ }
59004
60852
  if (!secondaryTookOver) {
59005
60853
  globalThis[keys.HOOK_APIS] = /* @__PURE__ */ new WeakSet();
59006
60854
  }
@@ -59036,7 +60884,7 @@ function extractTextContent2(msg) {
59036
60884
  // src/bridge.ts
59037
60885
  import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
59038
60886
  import * as childProcess from "child_process";
59039
- import path98 from "path";
60887
+ import path99 from "path";
59040
60888
 
59041
60889
  // src/service-candidates.ts
59042
60890
  function firstSuccessfulResult(candidates, attempt) {
@@ -59063,17 +60911,17 @@ function readCompatEnv(primary, legacy) {
59063
60911
  function configPathCandidates() {
59064
60912
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
59065
60913
  return [
59066
- ...envPath ? [path98.resolve(envPath)] : [],
59067
- path98.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
59068
- path98.join(resolveHomeDir2(), ".config", "engram", "config.json"),
59069
- path98.join(process.cwd(), "remnic.config.json"),
59070
- path98.join(process.cwd(), "engram.config.json")
60914
+ ...envPath ? [path99.resolve(envPath)] : [],
60915
+ path99.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
60916
+ path99.join(resolveHomeDir2(), ".config", "engram", "config.json"),
60917
+ path99.join(process.cwd(), "remnic.config.json"),
60918
+ path99.join(process.cwd(), "engram.config.json")
59071
60919
  ];
59072
60920
  }
59073
60921
  function isDaemonRunning() {
59074
60922
  for (const pidFile of [
59075
- path98.join(resolveHomeDir2(), ".remnic", "server.pid"),
59076
- path98.join(resolveHomeDir2(), ".engram", "server.pid")
60923
+ path99.join(resolveHomeDir2(), ".remnic", "server.pid"),
60924
+ path99.join(resolveHomeDir2(), ".engram", "server.pid")
59077
60925
  ]) {
59078
60926
  try {
59079
60927
  const pid = parseInt(readFileSync7(pidFile, "utf8").trim(), 10);
@@ -59146,8 +60994,8 @@ function detectBridgeMode() {
59146
60994
  }
59147
60995
  function loadAnyToken() {
59148
60996
  const tokenPaths = [
59149
- path98.join(resolveHomeDir2(), ".remnic", "tokens.json"),
59150
- path98.join(resolveHomeDir2(), ".engram", "tokens.json")
60997
+ path99.join(resolveHomeDir2(), ".remnic", "tokens.json"),
60998
+ path99.join(resolveHomeDir2(), ".engram", "tokens.json")
59151
60999
  ];
59152
61000
  for (const tokensPath of tokenPaths) {
59153
61001
  if (!existsSync12(tokensPath)) continue;