@remnic/plugin-openclaw 1.0.4 → 1.0.6

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,17 +3,16 @@ import {
3
3
  SharedContextManager,
4
4
  defaultTierMigrationCycleBudget,
5
5
  external_exports
6
- } from "./chunk-J7VGZNH4.js";
6
+ } from "./chunk-GUKYM4XZ.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";
14
- import {
15
- OpenAI
16
- } from "./chunk-NJG4HPAL.js";
17
16
  import {
18
17
  GraphIndex,
19
18
  analyzeGraphHealth
@@ -27,16 +26,7 @@ import {
27
26
  parseConsolidationResponse,
28
27
  renderExtensionsFooter,
29
28
  resolveExtensionsRoot
30
- } from "./chunk-J2FCINY7.js";
31
- import {
32
- FallbackLlmClient,
33
- buildChatCompletionTokenLimit,
34
- extractJsonCandidates,
35
- mergeEnv,
36
- readEnvVar,
37
- resolveHomeDir,
38
- shouldAssumeOpenAiChatCompletions
39
- } from "./chunk-3SA5F4WT.js";
29
+ } from "./chunk-QHMR3D7U.js";
40
30
  import {
41
31
  ContentHashIndex,
42
32
  MEMORY_LIFECYCLE_EVENT_SORT_ORDER,
@@ -78,7 +68,7 @@ import {
78
68
  sortMemoryLifecycleEvents,
79
69
  stripCitationForTemplate,
80
70
  toMemoryPathRel
81
- } from "./chunk-5VTGFKKU.js";
71
+ } from "./chunk-KPMXWORS.js";
82
72
  import {
83
73
  BoxBuilder,
84
74
  assertIsoRecordedAt,
@@ -92,6 +82,15 @@ import {
92
82
  recordStoreDay,
93
83
  validateStringRecord
94
84
  } from "./chunk-YHH3SXKD.js";
85
+ import {
86
+ FallbackLlmClient,
87
+ buildChatCompletionTokenLimit,
88
+ extractJsonCandidates,
89
+ mergeEnv,
90
+ readEnvVar,
91
+ resolveHomeDir,
92
+ shouldAssumeOpenAiChatCompletions
93
+ } from "./chunk-3SA5F4WT.js";
95
94
  import {
96
95
  listJsonFiles,
97
96
  listNamedFiles,
@@ -207,6 +206,7 @@ async function buildExtensionsFooterForSummary(config) {
207
206
  }
208
207
 
209
208
  // ../../src/index.ts
209
+ import OpenAI2 from "openai";
210
210
  import { createRequire as createRequire3 } from "module";
211
211
 
212
212
  // ../remnic-core/src/config.ts
@@ -235,6 +235,18 @@ function coerceBool(value) {
235
235
  function coerceInstallExtension(value) {
236
236
  return coerceBool(value);
237
237
  }
238
+ function coerceNumber(value) {
239
+ if (typeof value === "number") {
240
+ return Number.isFinite(value) ? value : void 0;
241
+ }
242
+ if (typeof value === "string") {
243
+ const trimmed = value.trim();
244
+ if (trimmed.length === 0) return void 0;
245
+ const n = Number(trimmed);
246
+ return Number.isFinite(n) ? n : void 0;
247
+ }
248
+ return void 0;
249
+ }
238
250
 
239
251
  // ../remnic-core/src/config.ts
240
252
  var DEFAULT_MEMORY_DIR = path2.join(
@@ -329,7 +341,8 @@ var VALID_MEMORY_CATEGORIES = /* @__PURE__ */ new Set([
329
341
  "commitment",
330
342
  "moment",
331
343
  "skill",
332
- "rule"
344
+ "rule",
345
+ "procedure"
333
346
  ]);
334
347
  var DEFAULT_BEHAVIOR_LOOP_PROTECTED_PARAMS = [
335
348
  "maxMemoryTokens",
@@ -496,6 +509,19 @@ function parseConfig(raw) {
496
509
  ) ? rawCodexCompat.compactionFlushMode : "auto",
497
510
  fingerprintDedup: rawCodexCompat.fingerprintDedup !== false
498
511
  };
512
+ const rawProcedural = cfg.procedural && typeof cfg.procedural === "object" && !Array.isArray(cfg.procedural) ? cfg.procedural : {};
513
+ const proceduralMinRaw = typeof rawProcedural.minOccurrences === "number" && Number.isFinite(rawProcedural.minOccurrences) ? Math.floor(rawProcedural.minOccurrences) : 3;
514
+ const procedural = {
515
+ enabled: coerceBool(rawProcedural.enabled) === true,
516
+ /** 0 disables miner emission threshold (no clusters qualify). */
517
+ minOccurrences: Math.min(1e3, Math.max(0, proceduralMinRaw)),
518
+ successFloor: typeof rawProcedural.successFloor === "number" && Number.isFinite(rawProcedural.successFloor) && rawProcedural.successFloor >= 0 && rawProcedural.successFloor <= 1 ? rawProcedural.successFloor : 0.7,
519
+ autoPromoteOccurrences: typeof rawProcedural.autoPromoteOccurrences === "number" && Number.isFinite(rawProcedural.autoPromoteOccurrences) ? rawProcedural.autoPromoteOccurrences <= 0 ? 0 : Math.min(1e4, Math.max(1, Math.floor(rawProcedural.autoPromoteOccurrences))) : 8,
520
+ autoPromoteEnabled: coerceBool(rawProcedural.autoPromoteEnabled) === true,
521
+ lookbackDays: typeof rawProcedural.lookbackDays === "number" && Number.isFinite(rawProcedural.lookbackDays) ? Math.min(3650, Math.max(1, Math.floor(rawProcedural.lookbackDays))) : 30,
522
+ proceduralMiningCronAutoRegister: coerceBool(rawProcedural.proceduralMiningCronAutoRegister) === true,
523
+ recallMaxProcedures: typeof rawProcedural.recallMaxProcedures === "number" && Number.isFinite(rawProcedural.recallMaxProcedures) ? Math.min(10, Math.max(1, Math.floor(rawProcedural.recallMaxProcedures))) : 3
524
+ };
499
525
  const memoryDir = typeof cfg.memoryDir === "string" && cfg.memoryDir.length > 0 ? cfg.memoryDir : DEFAULT_MEMORY_DIR;
500
526
  const rawIdentityInjectionMode = cfg.identityInjectionMode;
501
527
  const identityInjectionMode = rawIdentityInjectionMode && VALID_IDENTITY_INJECTION_MODES.includes(rawIdentityInjectionMode) ? rawIdentityInjectionMode : "recovery_only";
@@ -683,6 +709,25 @@ function parseConfig(raw) {
683
709
  // On by default
684
710
  temporalSupersessionIncludeInRecall: cfg.temporalSupersessionIncludeInRecall === true,
685
711
  // Off by default
712
+ // Direct-answer retrieval tier (issue #518)
713
+ recallDirectAnswerEnabled: coerceBool(cfg.recallDirectAnswerEnabled) ?? false,
714
+ recallDirectAnswerTokenOverlapFloor: (() => {
715
+ const n = coerceNumber(cfg.recallDirectAnswerTokenOverlapFloor);
716
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.55;
717
+ })(),
718
+ recallDirectAnswerImportanceFloor: (() => {
719
+ const n = coerceNumber(cfg.recallDirectAnswerImportanceFloor);
720
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.7;
721
+ })(),
722
+ recallDirectAnswerAmbiguityMargin: (() => {
723
+ const n = coerceNumber(cfg.recallDirectAnswerAmbiguityMargin);
724
+ return n !== void 0 && n >= 0 && n <= 1 ? n : 0.15;
725
+ })(),
726
+ recallDirectAnswerEligibleTaxonomyBuckets: Array.isArray(
727
+ cfg.recallDirectAnswerEligibleTaxonomyBuckets
728
+ ) ? cfg.recallDirectAnswerEligibleTaxonomyBuckets.filter(
729
+ (v) => typeof v === "string" && v.length > 0
730
+ ) : ["decisions", "principles", "conventions", "runbooks", "entities"],
686
731
  // Memory Linking (Phase 3A)
687
732
  memoryLinkingEnabled: cfg.memoryLinkingEnabled === true,
688
733
  // Off by default initially
@@ -758,6 +803,7 @@ function parseConfig(raw) {
758
803
  activeRecallAttachRecallExplain: cfg.activeRecallAttachRecallExplain === true,
759
804
  activeRecallAllowChainedActiveMemory: cfg.activeRecallAllowChainedActiveMemory === true,
760
805
  dreaming,
806
+ procedural,
761
807
  heartbeat,
762
808
  slotBehavior,
763
809
  codexCompat,
@@ -827,7 +873,7 @@ function parseConfig(raw) {
827
873
  semanticConsolidationMinClusterSize: typeof cfg.semanticConsolidationMinClusterSize === "number" ? Math.max(2, Math.floor(cfg.semanticConsolidationMinClusterSize)) : 3,
828
874
  semanticConsolidationExcludeCategories: Array.isArray(cfg.semanticConsolidationExcludeCategories) ? cfg.semanticConsolidationExcludeCategories.filter(
829
875
  (c) => typeof c === "string" && c.length > 0
830
- ) : ["correction", "commitment"],
876
+ ) : ["correction", "commitment", "procedure"],
831
877
  semanticConsolidationIntervalHours: typeof cfg.semanticConsolidationIntervalHours === "number" ? Math.max(1, Math.floor(cfg.semanticConsolidationIntervalHours)) : 168,
832
878
  semanticConsolidationMaxPerRun: typeof cfg.semanticConsolidationMaxPerRun === "number" ? Math.max(0, Math.floor(cfg.semanticConsolidationMaxPerRun)) : 100,
833
879
  creationMemoryEnabled: cfg.creationMemoryEnabled === true,
@@ -1077,7 +1123,7 @@ function parseConfig(raw) {
1077
1123
  factArchivalAgeDays: typeof cfg.factArchivalAgeDays === "number" ? cfg.factArchivalAgeDays : 90,
1078
1124
  factArchivalMaxImportance: typeof cfg.factArchivalMaxImportance === "number" ? cfg.factArchivalMaxImportance : 0.3,
1079
1125
  factArchivalMaxAccessCount: typeof cfg.factArchivalMaxAccessCount === "number" ? cfg.factArchivalMaxAccessCount : 2,
1080
- factArchivalProtectedCategories: Array.isArray(cfg.factArchivalProtectedCategories) ? cfg.factArchivalProtectedCategories.filter((c) => typeof c === "string") : ["commitment", "preference", "decision", "principle"],
1126
+ factArchivalProtectedCategories: Array.isArray(cfg.factArchivalProtectedCategories) ? cfg.factArchivalProtectedCategories.filter((c) => typeof c === "string") : ["commitment", "preference", "decision", "principle", "procedure"],
1081
1127
  // v8.3 lifecycle policy engine (default off)
1082
1128
  lifecyclePolicyEnabled: cfg.lifecyclePolicyEnabled === true,
1083
1129
  lifecycleFilterStaleEnabled: cfg.lifecycleFilterStaleEnabled === true,
@@ -1086,7 +1132,7 @@ function parseConfig(raw) {
1086
1132
  lifecycleArchiveDecayThreshold: typeof cfg.lifecycleArchiveDecayThreshold === "number" ? Math.min(1, Math.max(0, cfg.lifecycleArchiveDecayThreshold)) : 0.85,
1087
1133
  lifecycleProtectedCategories: Array.isArray(cfg.lifecycleProtectedCategories) ? cfg.lifecycleProtectedCategories.filter(
1088
1134
  (c) => typeof c === "string" && VALID_MEMORY_CATEGORIES.has(c)
1089
- ) : ["decision", "principle", "commitment", "preference"],
1135
+ ) : ["decision", "principle", "commitment", "preference", "procedure"],
1090
1136
  lifecycleMetricsEnabled: typeof cfg.lifecycleMetricsEnabled === "boolean" ? cfg.lifecycleMetricsEnabled : cfg.lifecyclePolicyEnabled === true,
1091
1137
  // v8.3 proactive + policy learning (default off)
1092
1138
  proactiveExtractionEnabled: cfg.proactiveExtractionEnabled === true,
@@ -1358,6 +1404,11 @@ function buildDefaultRecallPipeline(cfg) {
1358
1404
  maxEntities: typeof cfg.knowledgeIndexMaxEntities === "number" ? Math.max(0, Math.floor(cfg.knowledgeIndexMaxEntities)) : 40
1359
1405
  },
1360
1406
  { id: "verbatim-artifacts", enabled: cfg.verbatimArtifactsEnabled === true },
1407
+ {
1408
+ id: "procedure-recall",
1409
+ enabled: typeof cfg.procedural === "object" && cfg.procedural !== null && !Array.isArray(cfg.procedural) && cfg.procedural.enabled === true,
1410
+ maxChars: 2400
1411
+ },
1361
1412
  { id: "memory-boxes", enabled: cfg.memoryBoxesEnabled === true },
1362
1413
  { id: "temporal-memory-tree", enabled: cfg.temporalMemoryTreeEnabled === true },
1363
1414
  { id: "lcm-compressed-history", enabled: cfg.lcmEnabled === true },
@@ -1483,7 +1534,7 @@ function detectSdkCapabilities(api) {
1483
1534
  // ../remnic-core/src/orchestrator.ts
1484
1535
  import path43 from "path";
1485
1536
  import os3 from "os";
1486
- import { createHash as createHash9 } from "crypto";
1537
+ import { createHash as createHash9, randomBytes } from "crypto";
1487
1538
  import { existsSync as existsSync8 } from "fs";
1488
1539
  import {
1489
1540
  mkdir as mkdir30,
@@ -2144,6 +2195,15 @@ var SmartBuffer = class {
2144
2195
  this.state = this.normalizeState(await this.storage.loadBuffer());
2145
2196
  this.loaded = true;
2146
2197
  }
2198
+ /**
2199
+ * Reset the buffer to an empty, usable state.
2200
+ * Called when the persisted buffer file is corrupt and load() fails,
2201
+ * so the buffer can still accept new turns for the rest of the session.
2202
+ */
2203
+ resetToEmpty() {
2204
+ this.state = { turns: [], lastExtractionAt: null, extractionCount: 0 };
2205
+ this.loaded = true;
2206
+ }
2147
2207
  async save() {
2148
2208
  await this.storage.saveBuffer(this.state);
2149
2209
  }
@@ -2627,6 +2687,9 @@ function buildRecursiveFallback(content, cfg) {
2627
2687
  };
2628
2688
  }
2629
2689
 
2690
+ // ../remnic-core/src/extraction.ts
2691
+ import OpenAI from "openai";
2692
+
2630
2693
  // ../remnic-core/src/delinearize.ts
2631
2694
  var PRONOUN_MAP = {
2632
2695
  "he": { types: ["person"], possessive: false, group: "masc" },
@@ -3709,14 +3772,40 @@ function parseMemoryActionEligibilityContext(value) {
3709
3772
  source: "unknown"
3710
3773
  };
3711
3774
  }
3775
+ var ProcedureStepExtractSchema = external_exports.object({
3776
+ order: external_exports.number(),
3777
+ intent: external_exports.string(),
3778
+ toolCall: external_exports.object({
3779
+ kind: external_exports.string(),
3780
+ signature: external_exports.string()
3781
+ }).optional().nullable(),
3782
+ expectedOutcome: external_exports.string().optional().nullable(),
3783
+ optional: external_exports.boolean().optional().nullable()
3784
+ });
3712
3785
  var ExtractedFactSchema = external_exports.object({
3713
- category: external_exports.enum(["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]),
3786
+ category: external_exports.enum([
3787
+ "fact",
3788
+ "preference",
3789
+ "correction",
3790
+ "entity",
3791
+ "decision",
3792
+ "relationship",
3793
+ "principle",
3794
+ "commitment",
3795
+ "moment",
3796
+ "skill",
3797
+ "rule",
3798
+ "procedure"
3799
+ ]),
3714
3800
  content: external_exports.string().describe("The memory content \u2014 a clear, standalone statement"),
3715
3801
  confidence: external_exports.number().min(0).max(1).describe("How confident are you this is correct (0-1)"),
3716
3802
  tags: external_exports.array(external_exports.string()).describe("Relevant tags for categorization"),
3717
3803
  entityRef: external_exports.string().optional().nullable().describe("If about an entity, its normalized name (e.g. person-jane-doe)"),
3718
3804
  promptedByQuestion: external_exports.string().optional().nullable().describe("Optional proactive follow-up question that surfaced this fact."),
3719
- 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"}).')
3805
+ 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"}).'),
3806
+ procedureSteps: external_exports.array(ProcedureStepExtractSchema).optional().nullable().describe(
3807
+ '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").'
3808
+ )
3720
3809
  });
3721
3810
  var EntityMentionSchema = external_exports.object({
3722
3811
  name: external_exports.string().describe("Normalized entity name (e.g. jane-doe, acme-corp, my-project)"),
@@ -4464,6 +4553,67 @@ function parallelEfficiency(group) {
4464
4553
  return Math.round(idealMs / group.wallMs * 100);
4465
4554
  }
4466
4555
 
4556
+ // ../remnic-core/src/procedural/procedure-types.ts
4557
+ function normalizeProcedureSteps(raw) {
4558
+ if (!Array.isArray(raw)) return [];
4559
+ const out = [];
4560
+ for (let i = 0; i < raw.length; i++) {
4561
+ const s = raw[i];
4562
+ if (!s || typeof s !== "object") continue;
4563
+ const o = s;
4564
+ const intent = typeof o.intent === "string" ? o.intent.trim() : "";
4565
+ if (!intent) continue;
4566
+ const orderRaw = o.order;
4567
+ const order = typeof orderRaw === "number" && Number.isFinite(orderRaw) ? Math.max(1, Math.floor(orderRaw)) : i + 1;
4568
+ let toolCall;
4569
+ const tc = o.toolCall;
4570
+ if (tc && typeof tc === "object" && !Array.isArray(tc)) {
4571
+ const t = tc;
4572
+ const kind = typeof t.kind === "string" ? t.kind.trim() : "";
4573
+ const signature = typeof t.signature === "string" ? t.signature.trim() : "";
4574
+ if (kind && signature) {
4575
+ toolCall = { kind, signature };
4576
+ }
4577
+ }
4578
+ const expectedOutcome = typeof o.expectedOutcome === "string" && o.expectedOutcome.trim() ? o.expectedOutcome.trim() : void 0;
4579
+ const optional = o.optional === true ? true : void 0;
4580
+ out.push({ order, intent, toolCall, expectedOutcome, optional });
4581
+ }
4582
+ return out;
4583
+ }
4584
+ function buildProcedurePersistBody(title, procedureSteps) {
4585
+ const head = typeof title === "string" ? title.trim() : "";
4586
+ const steps = normalizeProcedureSteps(procedureSteps);
4587
+ if (steps.length === 0) return head;
4588
+ return `${head}
4589
+
4590
+ ${buildProcedureMarkdownBody(steps)}`.trimEnd() + "\n";
4591
+ }
4592
+ function buildProcedureMarkdownBody(steps) {
4593
+ const sorted = [...steps].sort((a, b) => a.order - b.order);
4594
+ const lines = [];
4595
+ for (const step of sorted) {
4596
+ const n = Number.isFinite(step.order) ? Math.max(1, Math.floor(step.order)) : 1;
4597
+ lines.push(`## Step ${n}`);
4598
+ lines.push("");
4599
+ lines.push(step.intent.trim());
4600
+ if (step.toolCall?.kind && step.toolCall.signature) {
4601
+ lines.push("");
4602
+ lines.push(`- Tool: \`${step.toolCall.kind}\` \u2014 ${step.toolCall.signature}`);
4603
+ }
4604
+ if (step.expectedOutcome?.trim()) {
4605
+ lines.push("");
4606
+ lines.push(`- Expected: ${step.expectedOutcome.trim()}`);
4607
+ }
4608
+ if (step.optional === true) {
4609
+ lines.push("");
4610
+ lines.push("- Optional: true");
4611
+ }
4612
+ lines.push("");
4613
+ }
4614
+ return lines.join("\n").trimEnd() + "\n";
4615
+ }
4616
+
4467
4617
  // ../remnic-core/src/extraction.ts
4468
4618
  var PROACTIVE_MIN_CONFIDENCE = 0.8;
4469
4619
  function normalizeQuestion(question) {
@@ -4554,8 +4704,9 @@ var ExtractionEngine = class {
4554
4704
  return shouldAssumeOpenAiChatCompletions(this.config.openaiBaseUrl);
4555
4705
  }
4556
4706
  sanitizeExtractionResult(result, messageTimestamp) {
4707
+ const proceduralOn = this.config.procedural?.enabled === true;
4557
4708
  const ts = messageTimestamp ?? /* @__PURE__ */ new Date();
4558
- const facts = result.facts.map((fact) => {
4709
+ const facts = result.facts.filter((fact) => proceduralOn || fact.category !== "procedure").map((fact) => {
4559
4710
  const sanitized = sanitizeMemoryContent(fact.content);
4560
4711
  if (!sanitized.clean) {
4561
4712
  log.warn(`extraction fact sanitized; violations=${sanitized.violations.join(", ")}`);
@@ -4582,7 +4733,8 @@ var ExtractionEngine = class {
4582
4733
  promptedByQuestion: typeof f?.promptedByQuestion === "string" ? f.promptedByQuestion : void 0,
4583
4734
  structuredAttributes: f?.structuredAttributes && typeof f.structuredAttributes === "object" && !Array.isArray(f.structuredAttributes) ? Object.fromEntries(
4584
4735
  Object.entries(f.structuredAttributes).filter(([k, v]) => typeof k === "string" && typeof v === "string")
4585
- ) : void 0
4736
+ ) : void 0,
4737
+ procedureSteps: Array.isArray(f?.procedureSteps) ? normalizeProcedureSteps(f.procedureSteps) : void 0
4586
4738
  })).filter((f) => f.content.length > 0) : [];
4587
4739
  const questions = Array.isArray(parsed?.questions) ? parsed.questions.map((q) => {
4588
4740
  if (typeof q === "string") return { question: q, context: "", priority: 0.5 };
@@ -5425,7 +5577,9 @@ Respond with valid JSON matching this schema:
5425
5577
  "principle",
5426
5578
  "commitment",
5427
5579
  "moment",
5428
- "skill"
5580
+ "skill",
5581
+ "rule",
5582
+ "procedure"
5429
5583
  ]);
5430
5584
  const allowedEntityTypes = /* @__PURE__ */ new Set([
5431
5585
  "person",
@@ -5482,6 +5636,7 @@ Memory categories:
5482
5636
  - moment: Emotionally significant events or milestones (e.g., "first successful deployment of engram")
5483
5637
  - skill: Capabilities the user or agent has demonstrated (e.g., "user is proficient with Kubernetes")${this.config.causalRuleExtractionEnabled ? `
5484
5638
  - 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")` : ""}
5639
+ - 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.
5485
5640
 
5486
5641
  Rules:
5487
5642
  - Only extract genuinely NEW information worth remembering across sessions
@@ -6691,6 +6846,8 @@ var CATEGORY_BOOSTS = {
6691
6846
  // Durable rules/values
6692
6847
  rule: 0.11,
6693
6848
  // Causal IF→THEN rules
6849
+ procedure: 0.1,
6850
+ // Repeatable workflows (issue #519)
6694
6851
  preference: 0.1,
6695
6852
  // User preferences matter
6696
6853
  commitment: 0.1,
@@ -6996,6 +7153,18 @@ function enforceMaxCacheSize(cache) {
6996
7153
  }
6997
7154
  }
6998
7155
  var AUTO_APPROVE_CATEGORIES = /* @__PURE__ */ new Set(["correction", "principle"]);
7156
+ 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;
7157
+ function validateProcedureExtraction(input) {
7158
+ const steps = normalizeProcedureSteps(input.procedureSteps);
7159
+ if (steps.length < 2) {
7160
+ return { durable: false, reason: "Procedure requires at least two steps with intents" };
7161
+ }
7162
+ const combined = [input.content, ...steps.map((s) => s.intent)].join(" ").toLowerCase();
7163
+ if (!PROCEDURE_TRIGGER_RE.test(combined)) {
7164
+ return { durable: false, reason: "Procedure missing explicit trigger phrasing" };
7165
+ }
7166
+ return { durable: true, reason: "Procedure structure validated" };
7167
+ }
6999
7168
  async function judgeFactDurability(candidates, config, localLlm, fallbackLlm, cache) {
7000
7169
  const startMs = Date.now();
7001
7170
  const verdicts = /* @__PURE__ */ new Map();
@@ -7176,6 +7345,156 @@ function createVerdictCache() {
7176
7345
  return /* @__PURE__ */ new Map();
7177
7346
  }
7178
7347
 
7348
+ // ../remnic-core/src/intent.ts
7349
+ var GOAL_PATTERNS = [
7350
+ { re: /\b(debug(?:s|ged|ging)?|fix(?:es|ed|ing)?|error(?:s)?|incident(?:s)?|outage(?:s)?|failure(?:s)?)\b/i, goal: "stabilize" },
7351
+ { re: /\b(deploy(?:s|ed|ing)?|release(?:s|d|ing)?|ship(?:s|ped|ping)?|publish(?:es|ed|ing)?)\b/i, goal: "release" },
7352
+ { re: /\b(plan(?:s|ned|ning)?|roadmap(?:s)?|strateg(?:y|ies)|design(?:s|ed|ing)?)\b/i, goal: "plan" },
7353
+ { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|security|hardening)\b/i, goal: "review" },
7354
+ { re: /\b(sales|deal|customer|client|prospect)\b/i, goal: "close_deal" }
7355
+ ];
7356
+ var ACTION_PATTERNS = [
7357
+ { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|inspect(?:s|ed|ing)?|check(?:s|ed|ing)?)\b/i, action: "review" },
7358
+ { re: /\b(plan(?:s|ned|ning)?|design(?:s|ed|ing)?|brainstorm(?:s|ed|ing)?|spec(?:s)?)\b/i, action: "plan" },
7359
+ { 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" },
7360
+ { re: /\b(summariz(?:e|es|ed|ing)|recap(?:s|ped|ping)?|what happened|timeline)\b/i, action: "summarize" },
7361
+ { re: /\b(decid(?:e|es|ed|ing)|decision(?:s)?|cho(?:ose|oses|osing)|chose|chosen)\b/i, action: "decide" }
7362
+ ];
7363
+ var ENTITY_PATTERNS = [
7364
+ { re: /\b(pr|pull request|branch|repo|github|ci|workflow)\b/i, entityType: "repo" },
7365
+ { re: /\b(discord|slack|channel|gateway|agent)\b/i, entityType: "ops" },
7366
+ { re: /\b(customer|client|deal|lead|account)\b/i, entityType: "client" },
7367
+ { re: /\b(model|llm|qmd|embedding|retrieval|memory)\b/i, entityType: "ai" },
7368
+ { re: /\b(doc|readme|docs|changelog)\b/i, entityType: "docs" }
7369
+ ];
7370
+ var TASK_INITIATION_RE = /\b(ship(?:ping|ped)?|deploy(?:ing|ed)?|release|publish|open(?:ing)?\s+(?:a\s+)?(?:pr|pull\s+request)|merge(?:ing)?\s+(?:the\s+)?(?:pr|pull\s+request)|run\s+(?:the\s+)?tests?|start(?:ing)?\s+(?:work|on|the)|kick\s+off|implement(?:ing|ed)?|let's\s+|going\s+to\s+(?:ship|deploy|release|open|run|merge)|need\s+to\s+(?:ship|deploy|run|open|merge|test)|fix(?:ing|ed)?\s+(?:the\s+)?(?:bug|build)|patch(?:ing|ed)?|build(?:ing)?\s+(?:and\s+)?(?:ship|deploy))\b/i;
7371
+ function normalizeTextInput(input) {
7372
+ return typeof input === "string" ? input : "";
7373
+ }
7374
+ function inferIntentFromText(text) {
7375
+ const safeText = normalizeTextInput(text);
7376
+ const goal = GOAL_PATTERNS.find((p) => p.re.test(safeText))?.goal ?? "unknown";
7377
+ const actionType = ACTION_PATTERNS.find((p) => p.re.test(safeText))?.action ?? "unknown";
7378
+ const entityTypes = Array.from(
7379
+ new Set(ENTITY_PATTERNS.filter((p) => p.re.test(safeText)).map((p) => p.entityType))
7380
+ );
7381
+ const taskInitiation = TASK_INITIATION_RE.test(safeText);
7382
+ return {
7383
+ goal,
7384
+ actionType,
7385
+ entityTypes,
7386
+ taskInitiation
7387
+ };
7388
+ }
7389
+ function isTaskInitiationIntent(intent) {
7390
+ return intent.taskInitiation === true;
7391
+ }
7392
+ function intentCompatibilityScore(queryIntent, memoryIntent) {
7393
+ const queryHasSignal = queryIntent.goal !== "unknown" || queryIntent.actionType !== "unknown" || queryIntent.entityTypes.length > 0;
7394
+ const memoryHasSignal = memoryIntent.goal !== "unknown" || memoryIntent.actionType !== "unknown" || memoryIntent.entityTypes.length > 0;
7395
+ if (!queryHasSignal || !memoryHasSignal) return 0;
7396
+ let score = 0;
7397
+ if (queryIntent.goal !== "unknown" && memoryIntent.goal !== "unknown" && queryIntent.goal === memoryIntent.goal) {
7398
+ score += 0.5;
7399
+ }
7400
+ if (queryIntent.actionType !== "unknown" && memoryIntent.actionType !== "unknown" && queryIntent.actionType === memoryIntent.actionType) {
7401
+ score += 0.3;
7402
+ }
7403
+ const overlap = queryIntent.entityTypes.filter((et) => memoryIntent.entityTypes.includes(et)).length;
7404
+ if (overlap > 0) {
7405
+ const denom = Math.max(queryIntent.entityTypes.length, memoryIntent.entityTypes.length, 1);
7406
+ score += 0.2 * (overlap / denom);
7407
+ }
7408
+ return Math.max(0, Math.min(1, score));
7409
+ }
7410
+ function planRecallMode(prompt) {
7411
+ const p = normalizeTextInput(prompt).trim();
7412
+ let ackCandidate = p;
7413
+ while (ackCandidate.length > 0) {
7414
+ const ch = ackCandidate.charCodeAt(ackCandidate.length - 1);
7415
+ const isDigit = ch >= 48 && ch <= 57;
7416
+ const isUpper = ch >= 65 && ch <= 90;
7417
+ const isLower = ch >= 97 && ch <= 122;
7418
+ if (isDigit || isUpper || isLower) break;
7419
+ ackCandidate = ackCandidate.slice(0, -1);
7420
+ }
7421
+ ackCandidate = ackCandidate.trim();
7422
+ if (p.length === 0) return "no_recall";
7423
+ if (/\b(timeline|sequence|history|what happened|chain of events|root cause)\b/i.test(p)) {
7424
+ return "graph_mode";
7425
+ }
7426
+ if (p.length <= 18 && /^(ok|okay|kk|thanks|thx|got it|sounds good|yep|yes|nope|no|done|cool|works)$/i.test(ackCandidate)) {
7427
+ return "no_recall";
7428
+ }
7429
+ 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())) {
7430
+ return "full";
7431
+ }
7432
+ if (p.length <= 100 && /^(check|reload|restart|run|verify|show|status|sync|update|open|close|set|enable|disable|fix|patch)\b/i.test(p)) {
7433
+ return "minimal";
7434
+ }
7435
+ return "full";
7436
+ }
7437
+ function hasBroadGraphIntent(prompt) {
7438
+ const p = normalizeTextInput(prompt).trim().toLowerCase();
7439
+ if (!p) return false;
7440
+ 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(
7441
+ p
7442
+ );
7443
+ }
7444
+
7445
+ // ../remnic-core/src/procedural/procedure-recall.ts
7446
+ function tokenOverlapScore(prompt, memoryText) {
7447
+ const norm = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2);
7448
+ const promptTokens = new Set(norm(prompt));
7449
+ const memTokens = new Set(norm(memoryText));
7450
+ if (promptTokens.size === 0 || memTokens.size === 0) return 0;
7451
+ let inter = 0;
7452
+ for (const t of promptTokens) {
7453
+ if (memTokens.has(t)) inter++;
7454
+ }
7455
+ const union = /* @__PURE__ */ new Set([...promptTokens, ...memTokens]);
7456
+ return inter / Math.max(1, union.size);
7457
+ }
7458
+ function scoreProcedureForPrompt(m, prompt, queryIntent) {
7459
+ const memText = `${m.content}
7460
+ ${(m.frontmatter.tags ?? []).join(" ")}`;
7461
+ const jaccard = tokenOverlapScore(prompt, memText);
7462
+ const memIntent = inferIntentFromText(m.content.slice(0, 2e3));
7463
+ const intentScore = intentCompatibilityScore(queryIntent, memIntent);
7464
+ return jaccard * 0.55 + intentScore * 0.45;
7465
+ }
7466
+ async function buildProcedureRecallSection(storage, prompt, config) {
7467
+ if (config.procedural?.enabled !== true) return null;
7468
+ const trimmed = typeof prompt === "string" ? prompt.trim() : "";
7469
+ if (!trimmed) return null;
7470
+ const queryIntent = inferIntentFromText(trimmed);
7471
+ if (!isTaskInitiationIntent(queryIntent)) return null;
7472
+ const maxN = Math.min(
7473
+ 10,
7474
+ Math.max(
7475
+ 1,
7476
+ typeof config.procedural.recallMaxProcedures === "number" && Number.isFinite(config.procedural.recallMaxProcedures) ? Math.floor(config.procedural.recallMaxProcedures) : 3
7477
+ )
7478
+ );
7479
+ const all = await storage.readAllMemories();
7480
+ const scored = all.filter(
7481
+ (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"
7482
+ ).map((m) => ({ m, score: scoreProcedureForPrompt(m, trimmed, queryIntent) })).filter((x) => x.score > 0.04).sort((a, b) => b.score - a.score).slice(0, maxN);
7483
+ if (scored.length === 0) return null;
7484
+ const blocks = scored.map(({ m, score }) => {
7485
+ const id = m.frontmatter.id;
7486
+ const flat = m.content.replace(/\s+/g, " ").trim();
7487
+ const preview = flat.slice(0, 320);
7488
+ const suffix = flat.length > 320 ? "\u2026" : "";
7489
+ return `### ${id} (match ${score.toFixed(2)})
7490
+
7491
+ ${preview}${suffix}`;
7492
+ });
7493
+ return `## Relevant procedures
7494
+
7495
+ ${blocks.join("\n\n")}`;
7496
+ }
7497
+
7179
7498
  // ../remnic-core/src/reconstruct.ts
7180
7499
  function findUnresolvedEntityRefs(recalledSnippets, recalledEntityRefs, knownEntities) {
7181
7500
  const refSet = new Set(recalledEntityRefs.map((r) => r.toLowerCase()));
@@ -8713,6 +9032,26 @@ var QmdDaemonSession = class {
8713
9032
  this.buffer = "";
8714
9033
  }
8715
9034
  };
9035
+ var QMD_RESULT_LINE_RE = /^#([0-9a-fA-F]+)\s+(\d+)%\s+(.+)/;
9036
+ var QMD_PATH_TITLE_RE = /^(.+?\.[a-zA-Z]{2,10})\s+-\s+(.*)$/;
9037
+ function parseQmdMarkdownResultText(text, transport) {
9038
+ const results = [];
9039
+ for (const line of text.split("\n")) {
9040
+ const m = QMD_RESULT_LINE_RE.exec(line.trim());
9041
+ if (!m) continue;
9042
+ const rest = m[3];
9043
+ const pathTitleSplit = QMD_PATH_TITLE_RE.exec(rest);
9044
+ if (!pathTitleSplit) continue;
9045
+ results.push({
9046
+ docid: m[1],
9047
+ path: pathTitleSplit[1] ?? "unknown",
9048
+ snippet: "",
9049
+ score: parseInt(m[2], 10) / 100,
9050
+ transport
9051
+ });
9052
+ }
9053
+ return results;
9054
+ }
8716
9055
  function parseMcpSearchResult(result, transport = "daemon") {
8717
9056
  const resultObj = result;
8718
9057
  if (!resultObj) return [];
@@ -8745,6 +9084,15 @@ function parseMcpSearchResult(result, transport = "daemon") {
8745
9084
  const textResults = parsed?.results ?? parsed?.documents;
8746
9085
  if (Array.isArray(textResults)) pushDocs(textResults);
8747
9086
  } catch {
9087
+ const existingKeys = new Set(results.map((r) => `${r.docid.toLowerCase()}|${r.path}`));
9088
+ const parsed = parseQmdMarkdownResultText(item.text, transport);
9089
+ for (const p of parsed) {
9090
+ const key = `${p.docid.toLowerCase()}|${p.path}`;
9091
+ if (!existingKeys.has(key)) {
9092
+ results.push(p);
9093
+ existingKeys.add(key);
9094
+ }
9095
+ }
8748
9096
  }
8749
9097
  }
8750
9098
  }
@@ -8800,9 +9148,22 @@ var QmdClient = class _QmdClient {
8800
9148
  collection;
8801
9149
  maxResults;
8802
9150
  available = null;
8803
- lastUpdateFailAtMs = null;
9151
+ _lastUpdateFailAtMs = null;
8804
9152
  lastEmbedFailAtMs = null;
8805
9153
  lastUpdateRunAtMs = null;
9154
+ get lastUpdateFailedAtMs() {
9155
+ return this._lastUpdateFailAtMs;
9156
+ }
9157
+ get lastUpdateRanAtMs() {
9158
+ return this.lastUpdateRunAtMs;
9159
+ }
9160
+ resetUpdateThrottles() {
9161
+ this._lastUpdateFailAtMs = null;
9162
+ this.lastUpdateRunAtMs = null;
9163
+ const gs = getGlobalQmdState();
9164
+ gs.lastGlobalUpdateRunAtMs = null;
9165
+ gs.lastGlobalUpdateFailAtMs = null;
9166
+ }
8806
9167
  updateTimeoutMs;
8807
9168
  updateMinIntervalMs;
8808
9169
  slowLog;
@@ -9419,13 +9780,13 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9419
9780
  return [];
9420
9781
  }
9421
9782
  }
9422
- async update() {
9423
- await this.runUpdateForCollection(this.collection, { perCollectionThrottle: false });
9783
+ async update(signal) {
9784
+ await this.runUpdateForCollection(this.collection, { perCollectionThrottle: false }, signal);
9424
9785
  }
9425
9786
  async updateCollection(collection) {
9426
9787
  await this.runUpdateForCollection(collection, { perCollectionThrottle: true });
9427
9788
  }
9428
- async runUpdateForCollection(collection, options) {
9789
+ async runUpdateForCollection(collection, options, signal) {
9429
9790
  if (this.available === false) return;
9430
9791
  const name = collection.trim();
9431
9792
  if (!name) return;
@@ -9451,7 +9812,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9451
9812
  log.debug("QMD update: suppressed due to min-interval gate");
9452
9813
  return;
9453
9814
  }
9454
- if (this.lastUpdateFailAtMs && now - this.lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
9815
+ if (this._lastUpdateFailAtMs && now - this._lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
9455
9816
  log.debug("QMD update: suppressed due to recent failures (backoff)");
9456
9817
  return;
9457
9818
  }
@@ -9472,7 +9833,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9472
9833
  );
9473
9834
  }
9474
9835
  const startedAtMs = Date.now();
9475
- await this.runQmdCommand(["update", "-c", name], this.updateTimeoutMs);
9836
+ await this.runQmdCommand(["update", "-c", name], this.updateTimeoutMs, signal);
9476
9837
  const durationMs = Date.now() - startedAtMs;
9477
9838
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
9478
9839
  log.warn(`SLOW QMD update: durationMs=${durationMs}`);
@@ -9492,7 +9853,7 @@ ${stderr}`.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
9492
9853
  globalState.lastUpdateFailByCollectionMs[name] = at;
9493
9854
  globalState.lastGlobalUpdateFailAtMs = at;
9494
9855
  } else {
9495
- this.lastUpdateFailAtMs = at;
9856
+ this._lastUpdateFailAtMs = at;
9496
9857
  globalState.lastGlobalUpdateFailAtMs = at;
9497
9858
  }
9498
9859
  const msg = err instanceof Error ? err.message : String(err);
@@ -12621,6 +12982,7 @@ import { mkdir as mkdir8, readFile as readFile10, rename, rm as rm2, stat as sta
12621
12982
  import path15 from "path";
12622
12983
  var DAY_SUMMARY_CRON_ID = "engram-day-summary";
12623
12984
  var GOVERNANCE_CRON_ID = "engram-nightly-governance";
12985
+ var PROCEDURAL_MINING_CRON_ID = "engram-procedural-mining";
12624
12986
  async function acquireCronJobsLock(jobsPath) {
12625
12987
  const lockPath2 = `${jobsPath}.lock`;
12626
12988
  const start = Date.now();
@@ -12733,13 +13095,37 @@ async function ensureNightlyGovernanceCron(jobsPath, options) {
12733
13095
  delivery: { mode: "none" }
12734
13096
  }));
12735
13097
  }
13098
+ async function ensureProceduralMiningCron(jobsPath, options) {
13099
+ const scheduleExpr = typeof options.scheduleExpr === "string" && options.scheduleExpr.trim().length > 0 ? options.scheduleExpr.trim() : "17 3 * * *";
13100
+ const agentId = typeof options.agentId === "string" && options.agentId.trim().length > 0 ? options.agentId.trim() : "main";
13101
+ return ensureCronJob(jobsPath, PROCEDURAL_MINING_CRON_ID, () => ({
13102
+ id: PROCEDURAL_MINING_CRON_ID,
13103
+ agentId,
13104
+ name: "Remnic Procedural Mining (nightly)",
13105
+ enabled: true,
13106
+ schedule: {
13107
+ kind: "cron",
13108
+ expr: scheduleExpr,
13109
+ tz: options.timezone
13110
+ },
13111
+ sessionTarget: "isolated",
13112
+ wakeMode: "now",
13113
+ payload: {
13114
+ kind: "agentTurn",
13115
+ timeoutSeconds: 900,
13116
+ thinking: "off",
13117
+ 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."
13118
+ },
13119
+ delivery: { mode: "none" }
13120
+ }));
13121
+ }
12736
13122
 
12737
13123
  // ../remnic-core/src/lifecycle.ts
12738
13124
  var DEFAULT_POLICY = {
12739
13125
  promoteHeatThreshold: 0.55,
12740
13126
  staleDecayThreshold: 0.65,
12741
13127
  archiveDecayThreshold: 0.85,
12742
- protectedCategories: ["decision", "principle", "commitment", "preference"]
13128
+ protectedCategories: ["decision", "principle", "commitment", "preference", "procedure"]
12743
13129
  };
12744
13130
  function clamp01(value) {
12745
13131
  if (!Number.isFinite(value)) return 0;
@@ -18167,97 +18553,6 @@ async function readRecentEntityTranscriptEntries(transcriptEntriesPromise, recen
18167
18553
  }
18168
18554
  var entityRecentTranscriptLookbackHours = RECENT_TRANSCRIPT_LOOKBACK_HOURS;
18169
18555
 
18170
- // ../remnic-core/src/intent.ts
18171
- var GOAL_PATTERNS = [
18172
- { re: /\b(debug(?:s|ged|ging)?|fix(?:es|ed|ing)?|error(?:s)?|incident(?:s)?|outage(?:s)?|failure(?:s)?)\b/i, goal: "stabilize" },
18173
- { re: /\b(deploy(?:s|ed|ing)?|release(?:s|d|ing)?|ship(?:s|ped|ping)?|publish(?:es|ed|ing)?)\b/i, goal: "release" },
18174
- { re: /\b(plan(?:s|ned|ning)?|roadmap(?:s)?|strateg(?:y|ies)|design(?:s|ed|ing)?)\b/i, goal: "plan" },
18175
- { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|security|hardening)\b/i, goal: "review" },
18176
- { re: /\b(sales|deal|customer|client|prospect)\b/i, goal: "close_deal" }
18177
- ];
18178
- var ACTION_PATTERNS = [
18179
- { re: /\b(review(?:s|ed|ing)?|audit(?:s|ed|ing)?|inspect(?:s|ed|ing)?|check(?:s|ed|ing)?)\b/i, action: "review" },
18180
- { re: /\b(plan(?:s|ned|ning)?|design(?:s|ed|ing)?|brainstorm(?:s|ed|ing)?|spec(?:s)?)\b/i, action: "plan" },
18181
- { 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" },
18182
- { re: /\b(summariz(?:e|es|ed|ing)|recap(?:s|ped|ping)?|what happened|timeline)\b/i, action: "summarize" },
18183
- { re: /\b(decid(?:e|es|ed|ing)|decision(?:s)?|cho(?:ose|oses|osing)|chose|chosen)\b/i, action: "decide" }
18184
- ];
18185
- var ENTITY_PATTERNS = [
18186
- { re: /\b(pr|pull request|branch|repo|github|ci|workflow)\b/i, entityType: "repo" },
18187
- { re: /\b(discord|slack|channel|gateway|agent)\b/i, entityType: "ops" },
18188
- { re: /\b(customer|client|deal|lead|account)\b/i, entityType: "client" },
18189
- { re: /\b(model|llm|qmd|embedding|retrieval|memory)\b/i, entityType: "ai" },
18190
- { re: /\b(doc|readme|docs|changelog)\b/i, entityType: "docs" }
18191
- ];
18192
- function normalizeTextInput(input) {
18193
- return typeof input === "string" ? input : "";
18194
- }
18195
- function inferIntentFromText(text) {
18196
- const safeText = normalizeTextInput(text);
18197
- const goal = GOAL_PATTERNS.find((p) => p.re.test(safeText))?.goal ?? "unknown";
18198
- const actionType = ACTION_PATTERNS.find((p) => p.re.test(safeText))?.action ?? "unknown";
18199
- const entityTypes = Array.from(
18200
- new Set(ENTITY_PATTERNS.filter((p) => p.re.test(safeText)).map((p) => p.entityType))
18201
- );
18202
- return {
18203
- goal,
18204
- actionType,
18205
- entityTypes
18206
- };
18207
- }
18208
- function intentCompatibilityScore(queryIntent, memoryIntent) {
18209
- const queryHasSignal = queryIntent.goal !== "unknown" || queryIntent.actionType !== "unknown" || queryIntent.entityTypes.length > 0;
18210
- const memoryHasSignal = memoryIntent.goal !== "unknown" || memoryIntent.actionType !== "unknown" || memoryIntent.entityTypes.length > 0;
18211
- if (!queryHasSignal || !memoryHasSignal) return 0;
18212
- let score = 0;
18213
- if (queryIntent.goal !== "unknown" && memoryIntent.goal !== "unknown" && queryIntent.goal === memoryIntent.goal) {
18214
- score += 0.5;
18215
- }
18216
- if (queryIntent.actionType !== "unknown" && memoryIntent.actionType !== "unknown" && queryIntent.actionType === memoryIntent.actionType) {
18217
- score += 0.3;
18218
- }
18219
- const overlap = queryIntent.entityTypes.filter((et) => memoryIntent.entityTypes.includes(et)).length;
18220
- if (overlap > 0) {
18221
- const denom = Math.max(queryIntent.entityTypes.length, memoryIntent.entityTypes.length, 1);
18222
- score += 0.2 * (overlap / denom);
18223
- }
18224
- return Math.max(0, Math.min(1, score));
18225
- }
18226
- function planRecallMode(prompt) {
18227
- const p = normalizeTextInput(prompt).trim();
18228
- let ackCandidate = p;
18229
- while (ackCandidate.length > 0) {
18230
- const ch = ackCandidate.charCodeAt(ackCandidate.length - 1);
18231
- const isDigit = ch >= 48 && ch <= 57;
18232
- const isUpper = ch >= 65 && ch <= 90;
18233
- const isLower = ch >= 97 && ch <= 122;
18234
- if (isDigit || isUpper || isLower) break;
18235
- ackCandidate = ackCandidate.slice(0, -1);
18236
- }
18237
- ackCandidate = ackCandidate.trim();
18238
- if (p.length === 0) return "no_recall";
18239
- if (/\b(timeline|sequence|history|what happened|chain of events|root cause)\b/i.test(p)) {
18240
- return "graph_mode";
18241
- }
18242
- if (p.length <= 18 && /^(ok|okay|kk|thanks|thx|got it|sounds good|yep|yes|nope|no|done|cool|works)$/i.test(ackCandidate)) {
18243
- return "no_recall";
18244
- }
18245
- 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())) {
18246
- return "full";
18247
- }
18248
- if (p.length <= 100 && /^(check|reload|restart|run|verify|show|status|sync|update|open|close|set|enable|disable|fix|patch)\b/i.test(p)) {
18249
- return "minimal";
18250
- }
18251
- return "full";
18252
- }
18253
- function hasBroadGraphIntent(prompt) {
18254
- const p = normalizeTextInput(prompt).trim().toLowerCase();
18255
- if (!p) return false;
18256
- 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(
18257
- p
18258
- );
18259
- }
18260
-
18261
18556
  // ../remnic-core/src/recall-query-policy.ts
18262
18557
  var DEFAULT_STOPWORDS = /* @__PURE__ */ new Set([
18263
18558
  "the",
@@ -18552,11 +18847,11 @@ function computeCompressionGuidelineCandidate(events, options = {}) {
18552
18847
  notes
18553
18848
  };
18554
18849
  }
18555
- const successRate = summary.outcomes.applied / summary.total;
18850
+ const successRate2 = summary.outcomes.applied / summary.total;
18556
18851
  const failureRate = summary.outcomes.failed / summary.total;
18557
18852
  const qualitySeen = summary.quality.good + summary.quality.poor;
18558
18853
  const qualitySignal = qualitySeen > 0 ? (summary.quality.good - summary.quality.poor) / qualitySeen : 0;
18559
- const rawDelta = clamp((successRate - failureRate) * 0.12 + qualitySignal * 0.06, -MAX_DELTA, MAX_DELTA);
18854
+ const rawDelta = clamp((successRate2 - failureRate) * 0.12 + qualitySignal * 0.06, -MAX_DELTA, MAX_DELTA);
18560
18855
  const delta = roundDelta(rawDelta);
18561
18856
  const direction = directionForDelta(delta);
18562
18857
  if (direction === "decrease" && summary.outcomes.failed > summary.outcomes.applied) {
@@ -20872,14 +21167,75 @@ async function getWorkProductLedgerStatus(options) {
20872
21167
  };
20873
21168
  }
20874
21169
 
21170
+ // ../remnic-core/src/utils/iso-timestamp.ts
21171
+ var ISO_UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
21172
+ var ISO_OFFSET_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
21173
+ function validateDateComponents(isoString) {
21174
+ const match = isoString.match(
21175
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/
21176
+ );
21177
+ if (!match) return false;
21178
+ const [, yStr, mStr, dStr, hStr, minStr, sStr] = match;
21179
+ const y = Number(yStr);
21180
+ const m = Number(mStr);
21181
+ const d = Number(dStr);
21182
+ const h = Number(hStr);
21183
+ const min = Number(minStr);
21184
+ const s = Number(sStr);
21185
+ if (m < 1 || m > 12) return false;
21186
+ if (d < 1 || d > 31) return false;
21187
+ if (h > 23 || min > 59 || s > 59) return false;
21188
+ const daysInMonth = new Date(y, m, 0).getDate();
21189
+ if (d > daysInMonth) return false;
21190
+ return true;
21191
+ }
21192
+ function validateOffset(isoString) {
21193
+ const offsetMatch = isoString.match(/([+-])(\d{2}):(\d{2})$/);
21194
+ if (!offsetMatch) return true;
21195
+ const oh = Number(offsetMatch[2]);
21196
+ const om = Number(offsetMatch[3]);
21197
+ if (oh > 14 || om > 59) return false;
21198
+ if (oh === 14 && om > 0) return false;
21199
+ return true;
21200
+ }
21201
+ function normalizeUtcForComparison(value) {
21202
+ const fracMatch = value.match(/\.(\d+)Z$/);
21203
+ if (fracMatch) {
21204
+ const ms = (fracMatch[1] + "000").slice(0, 3);
21205
+ return value.replace(/\.\d+Z$/, `.${ms}Z`);
21206
+ }
21207
+ return value.replace(/Z$/, ".000Z");
21208
+ }
21209
+ function parseIsoUtcTimestamp(value) {
21210
+ if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_RE.test(value)) {
21211
+ return null;
21212
+ }
21213
+ const ts = Date.parse(value);
21214
+ if (!Number.isFinite(ts)) return null;
21215
+ if (!validateDateComponents(value)) return null;
21216
+ const roundTrip = new Date(ts).toISOString();
21217
+ if (roundTrip !== normalizeUtcForComparison(value)) return null;
21218
+ return ts;
21219
+ }
21220
+ function parseIsoOffsetTimestamp(value) {
21221
+ if (typeof value !== "string" || !ISO_OFFSET_TIMESTAMP_RE.test(value)) {
21222
+ return null;
21223
+ }
21224
+ const ts = Date.parse(value);
21225
+ if (!Number.isFinite(ts)) return null;
21226
+ if (!validateDateComponents(value)) return null;
21227
+ if (!validateOffset(value)) return null;
21228
+ if (value.endsWith("Z")) {
21229
+ const roundTrip = new Date(ts).toISOString();
21230
+ if (roundTrip !== normalizeUtcForComparison(value)) return null;
21231
+ }
21232
+ return ts;
21233
+ }
21234
+
20875
21235
  // ../remnic-core/src/replay/types.ts
20876
21236
  var VALID_SOURCES = /* @__PURE__ */ new Set(["openclaw", "claude", "chatgpt"]);
20877
21237
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
20878
- var ISO_UTC_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
20879
21238
  var REPLAY_UNKNOWN_SESSION_KEY = "replay:unknown";
20880
- function normalizeIsoForComparison(value) {
20881
- return value.includes(".") ? value : value.replace("Z", ".000Z");
20882
- }
20883
21239
  function isReplaySource(value) {
20884
21240
  return typeof value === "string" && VALID_SOURCES.has(value);
20885
21241
  }
@@ -20892,12 +21248,7 @@ function normalizeReplaySessionKey(value) {
20892
21248
  return trimmed.length > 0 ? trimmed : REPLAY_UNKNOWN_SESSION_KEY;
20893
21249
  }
20894
21250
  function parseIsoTimestamp(value) {
20895
- if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_RE.test(value)) return null;
20896
- const ts = Date.parse(value);
20897
- if (!Number.isFinite(ts)) return null;
20898
- const roundTrip = new Date(ts).toISOString();
20899
- if (roundTrip !== normalizeIsoForComparison(value)) return null;
20900
- return ts;
21251
+ return parseIsoUtcTimestamp(value);
20901
21252
  }
20902
21253
  function validateReplayTurn(turn, index) {
20903
21254
  const issues = [];
@@ -24960,7 +25311,8 @@ var DEFAULT_CATEGORIES = [
24960
25311
  "commitment",
24961
25312
  "moment",
24962
25313
  "skill",
24963
- "rule"
25314
+ "rule",
25315
+ "procedure"
24964
25316
  ];
24965
25317
  function normalizeNamespace(namespace) {
24966
25318
  return namespace.trim();
@@ -25065,7 +25417,8 @@ var INLINE_ALLOWED_CATEGORIES = /* @__PURE__ */ new Set([
25065
25417
  "commitment",
25066
25418
  "moment",
25067
25419
  "skill",
25068
- "rule"
25420
+ "rule",
25421
+ "procedure"
25069
25422
  ]);
25070
25423
  var SECRET_PATTERNS = [
25071
25424
  /\bsk-[A-Za-z0-9]{16,}\b/,
@@ -25664,15 +26017,24 @@ var NamespaceSearchRouter = class {
25664
26017
  );
25665
26018
  return mergeNamespaceSearchResults(resultsByNamespace, maxResults);
25666
26019
  }
26020
+ /**
26021
+ * Update all namespace backends.
26022
+ * Returns the number of backends for which an update was attempted
26023
+ * (i.e., available and collection present). Callers can treat 0 as a
26024
+ * signal that no backend was eligible — useful for success-verification in
26025
+ * startup-sync when namespacesEnabled is true.
26026
+ */
25667
26027
  async updateNamespaces(namespaces) {
25668
26028
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
25669
- await Promise.all(
26029
+ const results = await Promise.all(
25670
26030
  unique.map(async (namespace) => {
25671
26031
  const record = await this.backendRecordFor(namespace);
25672
- if (!record.available || record.collectionState === "missing") return;
26032
+ if (!record.available || record.collectionState === "missing") return 0;
25673
26033
  await record.backend.update();
26034
+ return 1;
25674
26035
  })
25675
26036
  );
26037
+ return results.reduce((sum, v) => sum + v, 0);
25676
26038
  }
25677
26039
  async embedNamespaces(namespaces) {
25678
26040
  const unique = Array.from(new Set(namespaces.map((value) => value.trim()).filter(Boolean)));
@@ -25688,6 +26050,10 @@ var NamespaceSearchRouter = class {
25688
26050
  const record = await this.backendRecordFor(namespace);
25689
26051
  return record.collectionState;
25690
26052
  }
26053
+ /** Clear cached backend records so the next access re-probes availability. */
26054
+ clearCache() {
26055
+ this.cache.clear();
26056
+ }
25691
26057
  async backendRecordFor(namespace) {
25692
26058
  const key = namespace.trim() || this.config.defaultNamespace;
25693
26059
  const existing = this.cache.get(key);
@@ -27107,7 +27473,8 @@ function parseMemoryIntentSnapshot(value) {
27107
27473
  actionType: typeof candidate.actionType === "string" ? candidate.actionType : "unknown",
27108
27474
  entityTypes: Array.isArray(candidate.entityTypes) ? candidate.entityTypes.filter(
27109
27475
  (item) => typeof item === "string"
27110
- ) : []
27476
+ ) : [],
27477
+ taskInitiation: candidate.taskInitiation === true
27111
27478
  };
27112
27479
  }
27113
27480
  function buildQmdIntentHint(intent) {
@@ -27121,6 +27488,9 @@ function buildQmdIntentHint(intent) {
27121
27488
  if (intent.entityTypes.length > 0) {
27122
27489
  parts.push(`entities:${intent.entityTypes.join(",")}`);
27123
27490
  }
27491
+ if (intent.taskInitiation === true) {
27492
+ parts.push("task_initiation");
27493
+ }
27124
27494
  return parts.length > 0 ? parts.join(" ") : void 0;
27125
27495
  }
27126
27496
  function parseQmdRecallResults(value) {
@@ -27296,6 +27666,41 @@ var Orchestrator = class _Orchestrator {
27296
27666
  // Initialization gate: recall() awaits this before proceeding
27297
27667
  initPromise = null;
27298
27668
  resolveInit = null;
27669
+ /**
27670
+ * Resolves when deferred initialization (QMD probe, warmup, caches, cron)
27671
+ * completes. CLI and http-serve callers that need `qmd.isAvailable()` to
27672
+ * reflect reality should `await orchestrator.deferredReady` after
27673
+ * `initialize()`. Gateway callers can ignore it — recall() degrades
27674
+ * gracefully when QMD isn't ready yet.
27675
+ *
27676
+ * Also resolves (without error) when `initialize()` throws before reaching
27677
+ * the deferred-init phase, so callers never hang on a permanently-pending
27678
+ * promise.
27679
+ *
27680
+ * Host adapters that need to tie deferred init to their stop() lifecycle
27681
+ * should `await orchestrator.deferredReady` before proceeding with teardown
27682
+ * to prevent background QMD/warmup/cron tasks from racing with shutdown.
27683
+ */
27684
+ deferredReady = Promise.resolve();
27685
+ resolveDeferredReady = null;
27686
+ deferredInitAbort = null;
27687
+ /**
27688
+ * Whether the deferred init's QMD startup sync completed successfully.
27689
+ * When false after deferredReady resolves, the server retry loop should
27690
+ * attempt startupSearchSync() even if `qmd.isAvailable()` is true —
27691
+ * availability only means probe succeeded, not that the index is current.
27692
+ */
27693
+ deferredSyncSucceeded = false;
27694
+ /**
27695
+ * Abort deferred initialization so background QMD sync/warmup stops
27696
+ * promptly on shutdown. Safe to call multiple times or before init.
27697
+ */
27698
+ abortDeferredInit() {
27699
+ if (this.deferredInitAbort) {
27700
+ this.deferredInitAbort.abort();
27701
+ this.deferredInitAbort = null;
27702
+ }
27703
+ }
27299
27704
  /** Set per-session workspace for the next recall() call (compaction reset). @internal */
27300
27705
  setRecallWorkspaceOverride(sessionKey, dir) {
27301
27706
  this._recallWorkspaceOverrides.set(sessionKey, dir);
@@ -27686,100 +28091,173 @@ var Orchestrator = class _Orchestrator {
27686
28091
  return this.fastLlm;
27687
28092
  }
27688
28093
  async initialize() {
27689
- await migrateFromEngram({
27690
- quiet: true,
27691
- logger: (message) => log.info(message)
28094
+ this.deferredReady = new Promise((resolve) => {
28095
+ this.resolveDeferredReady = resolve;
27692
28096
  });
27693
- await this.storage.ensureDirectories();
27694
- await this.storage.loadAliases();
27695
- if (this.config.namespacesEnabled) {
27696
- const namespaces = /* @__PURE__ */ new Set([
27697
- this.config.defaultNamespace,
27698
- this.config.sharedNamespace,
27699
- ...this.config.namespacePolicies.map((p) => p.name)
27700
- ]);
27701
- for (const ns of namespaces) {
27702
- const sm = await this.storageRouter.storageFor(ns);
27703
- await sm.ensureDirectories();
27704
- await sm.loadAliases().catch(() => void 0);
28097
+ try {
28098
+ await migrateFromEngram({
28099
+ quiet: true,
28100
+ logger: (message) => log.info(message)
28101
+ });
28102
+ await this.storage.ensureDirectories();
28103
+ await this.storage.loadAliases();
28104
+ if (this.config.namespacesEnabled) {
28105
+ const namespaces = /* @__PURE__ */ new Set([
28106
+ this.config.defaultNamespace,
28107
+ this.config.sharedNamespace,
28108
+ ...this.config.namespacePolicies.map((p) => p.name)
28109
+ ]);
28110
+ for (const ns of namespaces) {
28111
+ const sm = await this.storageRouter.storageFor(ns);
28112
+ await sm.ensureDirectories();
28113
+ await sm.loadAliases().catch(() => void 0);
28114
+ }
28115
+ }
28116
+ await this.relevance.load();
28117
+ await this.negatives.load();
28118
+ await this.lastRecall.load();
28119
+ await this.tierMigrationStatus.load();
28120
+ await this.sessionObserver.load();
28121
+ this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
28122
+ this.utilityRuntimeValues = await loadUtilityRuntimeValues({
28123
+ memoryDir: this.config.memoryDir,
28124
+ memoryUtilityLearningEnabled: this.config.memoryUtilityLearningEnabled,
28125
+ promotionByOutcomeEnabled: this.config.promotionByOutcomeEnabled
28126
+ });
28127
+ if (this.config.factDeduplicationEnabled) {
28128
+ const stateDir2 = path43.join(this.config.memoryDir, "state");
28129
+ this.contentHashIndex = new ContentHashIndex(stateDir2);
28130
+ await this.contentHashIndex.load();
28131
+ log.info(
28132
+ `content-hash dedup: loaded ${this.contentHashIndex.size} hashes`
28133
+ );
27705
28134
  }
27706
- }
27707
- await this.relevance.load();
27708
- await this.negatives.load();
27709
- await this.lastRecall.load();
27710
- await this.tierMigrationStatus.load();
27711
- await this.sessionObserver.load();
27712
- this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
27713
- this.utilityRuntimeValues = await loadUtilityRuntimeValues({
27714
- memoryDir: this.config.memoryDir,
27715
- memoryUtilityLearningEnabled: this.config.memoryUtilityLearningEnabled,
27716
- promotionByOutcomeEnabled: this.config.promotionByOutcomeEnabled
27717
- });
27718
- if (this.config.factDeduplicationEnabled) {
27719
- const stateDir2 = path43.join(this.config.memoryDir, "state");
27720
- this.contentHashIndex = new ContentHashIndex(stateDir2);
27721
- await this.contentHashIndex.load();
27722
- log.info(
27723
- `content-hash dedup: loaded ${this.contentHashIndex.size} hashes`
27724
- );
27725
- }
27726
- await this.transcript.initialize();
27727
- await this.summarizer.initialize();
27728
- if (this.sharedContext) {
27729
- await this.sharedContext.ensureStructure();
27730
- }
27731
- if (this.compounding) {
27732
- await this.compounding.ensureDirs();
27733
- }
27734
- if (this.resolveInit) {
27735
- this.resolveInit();
27736
- this.resolveInit = null;
27737
- log.info("init gate opened (essential state loaded)");
27738
- }
27739
- {
27740
- const available = await this.qmd.probe();
27741
- if (available) {
27742
- log.info(`Search backend: available ${this.qmd.debugStatus()}`);
27743
- const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
27744
- const states = await Promise.all(
27745
- namespaces.map(async (namespace) => ({
27746
- namespace,
27747
- state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(
27748
- namespace
27749
- ) : await this.qmd.ensureCollection(this.config.memoryDir)
27750
- }))
28135
+ await this.transcript.initialize();
28136
+ await this.summarizer.initialize();
28137
+ if (this.sharedContext) {
28138
+ await this.sharedContext.ensureStructure();
28139
+ }
28140
+ if (this.compounding) {
28141
+ await this.compounding.ensureDirs();
28142
+ }
28143
+ try {
28144
+ await this.buffer.load();
28145
+ } catch (bufErr) {
28146
+ log.error(
28147
+ `buffer.load() failed (init gate will still open): ${bufErr}`
27751
28148
  );
27752
- const defaultState2 = states.find(
27753
- (entry) => entry.namespace === this.config.defaultNamespace
27754
- )?.state ?? "unknown";
27755
- if (defaultState2 === "missing") {
27756
- this.qmd = new NoopSearchBackend();
27757
- log.warn(
27758
- "Search collection missing for Remnic memory store; disabling search retrieval for this runtime (fallback retrieval remains enabled)"
27759
- );
27760
- } else if (defaultState2 === "unknown") {
27761
- log.warn(
27762
- "Search collection check unavailable; keeping search retrieval enabled for fail-open behavior"
27763
- );
27764
- } else if (defaultState2 === "skipped") {
27765
- log.debug(
27766
- "Search collection check skipped (remote or daemon-only mode)"
27767
- );
28149
+ this.buffer.resetToEmpty();
28150
+ }
28151
+ if (this.config.compactionResetEnabled) {
28152
+ try {
28153
+ const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
28154
+ const files = await readdir14(wsDir).catch(() => []);
28155
+ for (const f of files) {
28156
+ if (!f.startsWith(".compaction-reset-signal-")) continue;
28157
+ const fp = path43.join(wsDir, f);
28158
+ const s = await stat10(fp).catch(() => null);
28159
+ if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
28160
+ await unlink7(fp).catch(() => {
28161
+ });
28162
+ log.debug(`initialize: removed stale compaction signal ${f}`);
28163
+ }
28164
+ }
28165
+ } catch (err) {
28166
+ log.debug("initialize: stale signal sweep failed:", err);
27768
28167
  }
27769
- for (const entry of states) {
27770
- if (entry.namespace === this.config.defaultNamespace) continue;
27771
- if (entry.state === "missing") {
28168
+ }
28169
+ try {
28170
+ const available = await this.qmd.probe();
28171
+ if (available) {
28172
+ log.info(`Search backend: available ${this.qmd.debugStatus()}`);
28173
+ const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
28174
+ const states = await Promise.all(
28175
+ namespaces.map(async (namespace) => ({
28176
+ namespace,
28177
+ state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(
28178
+ namespace
28179
+ ) : await this.qmd.ensureCollection(this.config.memoryDir)
28180
+ }))
28181
+ );
28182
+ const defaultState2 = states.find(
28183
+ (entry) => entry.namespace === this.config.defaultNamespace
28184
+ )?.state ?? "unknown";
28185
+ if (defaultState2 === "missing") {
28186
+ this.qmd = new NoopSearchBackend();
27772
28187
  log.warn(
27773
- `Search collection missing for namespace '${entry.namespace}'; namespace retrieval will fail open to non-search paths`
28188
+ "Search collection missing for Remnic memory store; disabling search retrieval for this runtime (fallback retrieval remains enabled)"
28189
+ );
28190
+ } else if (defaultState2 === "unknown") {
28191
+ log.warn(
28192
+ "Search collection check unavailable; keeping search retrieval enabled for fail-open behavior"
28193
+ );
28194
+ } else if (defaultState2 === "skipped") {
28195
+ log.debug(
28196
+ "Search collection check skipped (remote or daemon-only mode)"
27774
28197
  );
27775
28198
  }
28199
+ for (const entry of states) {
28200
+ if (entry.namespace === this.config.defaultNamespace) continue;
28201
+ if (entry.state === "missing") {
28202
+ log.warn(
28203
+ `Search collection missing for namespace '${entry.namespace}'; namespace retrieval will fail open to non-search paths`
28204
+ );
28205
+ }
28206
+ }
28207
+ } else if (this.qmd instanceof NoopSearchBackend) {
28208
+ log.debug(`Search backend: noop (search intentionally disabled)`);
28209
+ } else {
28210
+ log.warn(`Search backend: not available ${this.qmd.debugStatus()}`);
27776
28211
  }
27777
- } else if (this.qmd instanceof NoopSearchBackend) {
27778
- log.debug(`Search backend: noop (search intentionally disabled)`);
27779
- } else {
27780
- log.warn(`Search backend: not available ${this.qmd.debugStatus()}`);
28212
+ } catch (err) {
28213
+ log.error(`QMD probe/collection check failed (non-fatal): ${err}`);
28214
+ }
28215
+ if (this.resolveInit) {
28216
+ this.resolveInit();
28217
+ this.resolveInit = null;
28218
+ log.info("init gate opened (essential state + QMD state loaded)");
28219
+ }
28220
+ const resolveDeferred = this.resolveDeferredReady;
28221
+ this.resolveDeferredReady = null;
28222
+ this.deferredInitAbort = new AbortController();
28223
+ this.deferredInitialize(this.deferredInitAbort.signal).catch((err) => {
28224
+ log.error(`deferred initialization failed (non-fatal): ${err}`);
28225
+ }).finally(() => {
28226
+ resolveDeferred?.();
28227
+ });
28228
+ } catch (err) {
28229
+ if (this.resolveInit) {
28230
+ this.resolveInit();
28231
+ this.resolveInit = null;
28232
+ }
28233
+ if (this.resolveDeferredReady) {
28234
+ this.resolveDeferredReady();
28235
+ this.resolveDeferredReady = null;
27781
28236
  }
28237
+ throw err;
28238
+ }
28239
+ }
28240
+ async deferredInitialize(signal) {
28241
+ if (this.qmd.isAvailable() && this.config.qmdMaintenanceEnabled) {
28242
+ try {
28243
+ log.info("QMD startup sync: updating index to match current disk state");
28244
+ if (this.config.namespacesEnabled) {
28245
+ await this.namespaceSearchRouter.updateNamespaces(
28246
+ this.configuredNamespaces()
28247
+ );
28248
+ } else {
28249
+ await this.qmd.update();
28250
+ }
28251
+ log.info("QMD startup sync: complete");
28252
+ this.deferredSyncSucceeded = true;
28253
+ } catch (err) {
28254
+ log.warn(`QMD startup sync failed (non-fatal): ${err}`);
28255
+ }
28256
+ } else if (!this.qmd.isAvailable()) {
28257
+ } else {
28258
+ this.deferredSyncSucceeded = true;
27782
28259
  }
28260
+ if (signal.aborted) return;
27783
28261
  const warmupPromises = [];
27784
28262
  if (this.qmd.isAvailable()) {
27785
28263
  const warmupNs = this.config.defaultNamespace;
@@ -27804,68 +28282,173 @@ var Orchestrator = class _Orchestrator {
27804
28282
  );
27805
28283
  }
27806
28284
  await Promise.all(warmupPromises);
28285
+ if (signal.aborted) return;
28286
+ const cacheWarmups = [];
27807
28287
  if (this.config.knowledgeIndexEnabled) {
27808
- (async () => {
27809
- try {
27810
- const t0 = Date.now();
27811
- await this.storage.buildKnowledgeIndex(this.config);
27812
- log.info(`Knowledge Index warmup: complete in ${Date.now() - t0}ms`);
27813
- } catch (err) {
27814
- log.debug(`Knowledge Index warmup failed (non-fatal): ${err}`);
27815
- }
27816
- })().catch(() => {
27817
- });
28288
+ cacheWarmups.push(
28289
+ (async () => {
28290
+ try {
28291
+ const t0 = Date.now();
28292
+ await this.storage.buildKnowledgeIndex(this.config);
28293
+ log.info(`Knowledge Index warmup: complete in ${Date.now() - t0}ms`);
28294
+ } catch (err) {
28295
+ log.debug(`Knowledge Index warmup failed (non-fatal): ${err}`);
28296
+ }
28297
+ })()
28298
+ );
27818
28299
  }
27819
- this.storage.readAllMemories().catch(() => {
27820
- });
27821
- this.storage.readAllEntityFiles().catch(() => {
27822
- });
28300
+ cacheWarmups.push(this.storage.readAllMemories().then(() => {
28301
+ }).catch(() => {
28302
+ }));
28303
+ cacheWarmups.push(this.storage.readAllEntityFiles().then(() => {
28304
+ }).catch(() => {
28305
+ }));
28306
+ await Promise.all(cacheWarmups);
28307
+ if (signal.aborted) return;
27823
28308
  if (this.config.conversationIndexEnabled && this.conversationIndexBackend) {
27824
- const init = await this.conversationIndexBackend.initialize();
27825
- if (!init.enabled) {
28309
+ try {
28310
+ const init = await this.conversationIndexBackend.initialize();
28311
+ if (!init.enabled) {
28312
+ this.config.conversationIndexEnabled = false;
28313
+ }
28314
+ if (init.logLevel === "info") {
28315
+ log.info(init.message);
28316
+ } else if (init.logLevel === "warn") {
28317
+ log.warn(init.message);
28318
+ } else {
28319
+ log.debug(init.message);
28320
+ }
28321
+ } catch (err) {
28322
+ log.error(`Conversation index initialization failed (non-fatal): ${err}`);
27826
28323
  this.config.conversationIndexEnabled = false;
27827
28324
  }
27828
- if (init.logLevel === "info") {
27829
- log.info(init.message);
27830
- } else if (init.logLevel === "warn") {
27831
- log.warn(init.message);
27832
- } else {
27833
- log.debug(init.message);
27834
- }
27835
28325
  }
27836
- await this.buffer.load();
28326
+ if (signal.aborted) return;
27837
28327
  if (this.config.localLlmEnabled) {
27838
- await this.validateLocalLlmModel();
27839
- }
27840
- if (this.config.compactionResetEnabled) {
27841
28328
  try {
27842
- const wsDir = this.config.workspaceDir || defaultWorkspaceDir();
27843
- const files = await readdir14(wsDir).catch(() => []);
27844
- for (const f of files) {
27845
- if (!f.startsWith(".compaction-reset-signal-")) continue;
27846
- const fp = path43.join(wsDir, f);
27847
- const s = await stat10(fp).catch(() => null);
27848
- if (s && Date.now() - s.mtimeMs >= COMPACTION_SIGNAL_MAX_AGE_MS) {
27849
- await unlink7(fp).catch(() => {
27850
- });
27851
- log.debug(`initialize: removed stale compaction signal ${f}`);
27852
- }
27853
- }
28329
+ await this.validateLocalLlmModel();
27854
28330
  } catch (err) {
27855
- log.debug("initialize: stale signal sweep failed:", err);
28331
+ log.error(`Local LLM validation failed (non-fatal): ${err}`);
27856
28332
  }
27857
28333
  }
27858
- log.info("orchestrator initialized (full)");
28334
+ if (signal.aborted) return;
27859
28335
  if (this.config.daySummaryEnabled) {
27860
- this.autoRegisterDaySummaryCron().catch((err) => {
28336
+ try {
28337
+ await this.autoRegisterDaySummaryCron();
28338
+ } catch (err) {
27861
28339
  log.debug(`day-summary cron auto-register failed (non-fatal): ${err}`);
27862
- });
28340
+ }
27863
28341
  }
27864
28342
  if (this.config.nightlyGovernanceCronAutoRegister) {
27865
- this.autoRegisterNightlyGovernanceCron().catch((err) => {
28343
+ try {
28344
+ await this.autoRegisterNightlyGovernanceCron();
28345
+ } catch (err) {
27866
28346
  log.debug(`nightly governance cron auto-register failed (non-fatal): ${err}`);
27867
- });
28347
+ }
28348
+ }
28349
+ if (this.config.procedural?.proceduralMiningCronAutoRegister) {
28350
+ try {
28351
+ await this.autoRegisterProceduralMiningCron();
28352
+ } catch (err) {
28353
+ log.debug(`procedural mining cron auto-register failed (non-fatal): ${err}`);
28354
+ }
28355
+ }
28356
+ log.info("orchestrator initialized (full \u2014 deferred steps complete)");
28357
+ }
28358
+ /**
28359
+ * Namespace-aware startup search sync. Re-probes QMD, ensures collections
28360
+ * (namespace-aware when namespacesEnabled), runs update, and warms up search.
28361
+ * Designed for server retry paths that run after the deferred init completes
28362
+ * when QMD was not available during initial startup.
28363
+ *
28364
+ * Accepts an optional AbortSignal so callers can interrupt the sync during
28365
+ * shutdown. The signal is checked between phases and forwarded into the QMD
28366
+ * update and warmup search calls so a long-running `qmd update` subprocess
28367
+ * is killed promptly rather than left in flight after `httpServer.stop()`.
28368
+ *
28369
+ * Returns true if the sync succeeded (QMD now available), false otherwise.
28370
+ */
28371
+ async startupSearchSync(signal) {
28372
+ if (signal?.aborted) return false;
28373
+ const available = await this.qmd.probe();
28374
+ if (!available) return false;
28375
+ if (signal?.aborted) {
28376
+ log.debug("startupSearchSync: aborted after probe");
28377
+ return false;
28378
+ }
28379
+ log.info(`startupSearchSync: backend now available ${this.qmd.debugStatus()}`);
28380
+ if (this.config.namespacesEnabled) {
28381
+ this.namespaceSearchRouter.clearCache();
28382
+ }
28383
+ const namespaces = this.config.namespacesEnabled ? this.configuredNamespaces() : [this.config.defaultNamespace];
28384
+ const states = await Promise.all(
28385
+ namespaces.map(async (namespace) => ({
28386
+ namespace,
28387
+ state: this.config.namespacesEnabled ? await this.namespaceSearchRouter.ensureNamespaceCollection(namespace) : await this.qmd.ensureCollection(this.config.memoryDir)
28388
+ }))
28389
+ );
28390
+ if (signal?.aborted) {
28391
+ log.debug("startupSearchSync: aborted after ensureCollection");
28392
+ return false;
28393
+ }
28394
+ const defaultState2 = states.find((e) => e.namespace === this.config.defaultNamespace)?.state ?? "unknown";
28395
+ if (defaultState2 === "missing") {
28396
+ if ("available" in this.qmd) {
28397
+ this.qmd.available = false;
28398
+ }
28399
+ this.qmd = new NoopSearchBackend();
28400
+ log.warn("startupSearchSync: search collection missing; disabling search (fallback retrieval remains enabled)");
28401
+ return false;
28402
+ }
28403
+ if (this.config.qmdMaintenanceEnabled) {
28404
+ try {
28405
+ const failTsBefore = "lastUpdateFailedAtMs" in this.qmd ? this.qmd.lastUpdateFailedAtMs : null;
28406
+ const hasRunTs = "lastUpdateRanAtMs" in this.qmd;
28407
+ if ("resetUpdateThrottles" in this.qmd) {
28408
+ this.qmd.resetUpdateThrottles();
28409
+ }
28410
+ log.info("startupSearchSync: updating index to match current disk state");
28411
+ let namespacesUpdated = 0;
28412
+ if (this.config.namespacesEnabled) {
28413
+ namespacesUpdated = await this.namespaceSearchRouter.updateNamespaces(namespaces);
28414
+ } else {
28415
+ await this.qmd.update(signal);
28416
+ }
28417
+ if (signal?.aborted) {
28418
+ log.debug("startupSearchSync: aborted after update");
28419
+ return false;
28420
+ }
28421
+ const failTsAfter = "lastUpdateFailedAtMs" in this.qmd ? this.qmd.lastUpdateFailedAtMs : null;
28422
+ const runTsAfter = hasRunTs ? this.qmd.lastUpdateRanAtMs : null;
28423
+ if (failTsAfter !== null && failTsAfter !== failTsBefore) {
28424
+ log.warn("startupSearchSync: update silently failed (detected via fail timestamp)");
28425
+ return false;
28426
+ }
28427
+ if (this.config.namespacesEnabled) {
28428
+ if (namespacesUpdated === 0) {
28429
+ log.warn("startupSearchSync: no namespace backends were eligible for update (all unavailable or collections missing)");
28430
+ return false;
28431
+ }
28432
+ log.info(`startupSearchSync: namespace updates succeeded (${namespacesUpdated}/${namespaces.length} namespaces updated)`);
28433
+ } else if (hasRunTs && runTsAfter === null) {
28434
+ log.warn("startupSearchSync: update was throttled/skipped (run timestamp is null after reset + update)");
28435
+ return false;
28436
+ }
28437
+ log.info("startupSearchSync: sync complete");
28438
+ } catch (err) {
28439
+ log.warn(`startupSearchSync: update failed: ${err}`);
28440
+ return false;
28441
+ }
28442
+ }
28443
+ if (!signal?.aborted) {
28444
+ try {
28445
+ await this.qmd.search("warmup", this.config.defaultNamespace, 1, void 0, { signal });
28446
+ log.info("startupSearchSync: warmup complete");
28447
+ } catch (err) {
28448
+ log.debug(`startupSearchSync: warmup search failed (non-fatal): ${err}`);
28449
+ }
27868
28450
  }
28451
+ return true;
27869
28452
  }
27870
28453
  /**
27871
28454
  * Auto-register the engram-day-summary cron job in OpenClaw if it doesn't exist.
@@ -27917,6 +28500,26 @@ var Orchestrator = class _Orchestrator {
27917
28500
  log.debug(`nightly governance cron auto-register error: ${err}`);
27918
28501
  }
27919
28502
  }
28503
+ async autoRegisterProceduralMiningCron() {
28504
+ const home = resolveHomeDir();
28505
+ const jobsPath = path43.join(home, ".openclaw", "cron", "jobs.json");
28506
+ try {
28507
+ if (!existsSync8(jobsPath)) {
28508
+ log.debug("procedural mining cron: jobs.json not found, skipping auto-register");
28509
+ return;
28510
+ }
28511
+ const created = await ensureProceduralMiningCron(jobsPath, {
28512
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
28513
+ });
28514
+ if (created.created) {
28515
+ log.info(`procedural mining cron auto-registered (${created.jobId})`);
28516
+ } else {
28517
+ log.debug("procedural mining cron already exists, skipping auto-register");
28518
+ }
28519
+ } catch (err) {
28520
+ log.debug(`procedural mining cron auto-register error: ${err}`);
28521
+ }
28522
+ }
27920
28523
  async applyBehaviorRuntimePolicy(state) {
27921
28524
  const result = await this.policyRuntime.applyFromBehaviorState(state);
27922
28525
  this.runtimePolicyValues = await this.policyRuntime.loadRuntimeValues();
@@ -30380,7 +30983,7 @@ ${trimmedBody}`;
30380
30983
  return null;
30381
30984
  }
30382
30985
  try {
30383
- const { retrieveCausalChains } = await import("./causal-retrieval-5UPIKZ4I.js");
30986
+ const { retrieveCausalChains } = await import("./causal-retrieval-3BKBXVXD.js");
30384
30987
  const section = await retrieveCausalChains({
30385
30988
  memoryDir: this.config.memoryDir,
30386
30989
  causalTrajectoryStoreDir: this.config.causalTrajectoryStoreDir,
@@ -31316,6 +31919,22 @@ ${formatted}`;
31316
31919
  });
31317
31920
  return section;
31318
31921
  })();
31922
+ const procedureRecallPromise = (async () => {
31923
+ if (this.config.procedural?.enabled !== true) return null;
31924
+ if (!this.isRecallSectionEnabled("procedure-recall", true)) return null;
31925
+ try {
31926
+ return await buildProcedureRecallSection(
31927
+ this.storage,
31928
+ retrievalQuery,
31929
+ this.config
31930
+ );
31931
+ } catch (err) {
31932
+ log.debug(
31933
+ `procedure-recall: failed open: ${err instanceof Error ? err.message : String(err)}`
31934
+ );
31935
+ return null;
31936
+ }
31937
+ })();
31319
31938
  const compoundingPromise = observeEnrichmentPromise(
31320
31939
  (async () => {
31321
31940
  const t0 = Date.now();
@@ -31380,6 +31999,7 @@ ${formatted}`;
31380
31999
  causalTrajectorySection,
31381
32000
  cmcCausalChainsSection,
31382
32001
  calibrationSection,
32002
+ procedureRecallSection,
31383
32003
  trustZoneSection,
31384
32004
  verifiedRecallSection,
31385
32005
  verifiedRulesSection,
@@ -31401,6 +32021,7 @@ ${formatted}`;
31401
32021
  ["causalTraj", causalTrajectoryPromise],
31402
32022
  ["cmc", cmcRetrievalPromise],
31403
32023
  ["calibration", calibrationPromise],
32024
+ ["procedureRecall", procedureRecallPromise],
31404
32025
  ["trustZone", trustZonePromise],
31405
32026
  ["verifiedRecall", verifiedRecallPromise],
31406
32027
  ["verifiedRules", verifiedRulesPromise],
@@ -31506,6 +32127,13 @@ ${profile}`
31506
32127
  calibrationSection
31507
32128
  );
31508
32129
  }
32130
+ if (procedureRecallSection) {
32131
+ this.appendRecallSection(
32132
+ sectionBuckets,
32133
+ "procedure-recall",
32134
+ procedureRecallSection
32135
+ );
32136
+ }
31509
32137
  if (identityContinuity) {
31510
32138
  this.appendRecallSection(
31511
32139
  sectionBuckets,
@@ -32550,6 +33178,87 @@ _Context: ${topQuestion.context}_`
32550
33178
  }
32551
33179
  }
32552
33180
  }
33181
+ /**
33182
+ * Return the namespace that `ingestBulkImportBatch` writes into (#460).
33183
+ *
33184
+ * Exposed so host CLIs can snapshot the same storage root that extraction
33185
+ * actually writes to, avoiding the "CLI counts files at namespace A while
33186
+ * writes land in namespace B" footgun that a naïve
33187
+ * `config.defaultNamespace` snapshot could hit when a namespace policy
33188
+ * named `"default"` also exists.
33189
+ *
33190
+ * Today bulk-import is pinned to `config.defaultNamespace`; future
33191
+ * per-invocation namespace routing would thread an explicit target here
33192
+ * and through `ingestBulkImportBatch`.
33193
+ */
33194
+ bulkImportWriteNamespace() {
33195
+ return this.config.defaultNamespace;
33196
+ }
33197
+ /**
33198
+ * Ingest a batch of bulk-import turns (#460). Like ingestReplayBatch, this
33199
+ * normalizes user/assistant turns into the extraction buffer and awaits
33200
+ * settlement, but it intentionally bypasses the captureMode="explicit"
33201
+ * gate because bulk-import is itself an explicit user action — the user
33202
+ * ran `bulk-import --source <name> --file ...` and would be surprised to
33203
+ * see the command silently no-op when capture is otherwise restricted.
33204
+ *
33205
+ * Turns with role="other" are skipped (not supported by the extraction
33206
+ * pipeline).
33207
+ *
33208
+ * Two design decisions worth calling out:
33209
+ *
33210
+ * - **sessionKey is truthy and per-batch-unique.**
33211
+ * `ThreadingManager.shouldStartNewThread` only applies the session-key
33212
+ * boundary check when `turn.sessionKey` is truthy (threading.ts:82);
33213
+ * with an empty string, imported turns could attach to the current
33214
+ * live thread or merge across unrelated import batches. A unique
33215
+ * `bulk-import:batch:<timestamp>-<rand>` key forces a fresh thread per
33216
+ * batch without matching common prefix/map rules in
33217
+ * `principalFromSessionKeyRules`. (Catch-all regex rules could still
33218
+ * remap the principal, but that only affects metadata provenance —
33219
+ * see the next point for why write routing is unaffected.)
33220
+ *
33221
+ * - **writeNamespaceOverride pins the storage target.**
33222
+ * We pass `writeNamespaceOverride: this.bulkImportWriteNamespace()` to
33223
+ * `queueBufferedExtraction`, which tells `runExtraction` to skip
33224
+ * `defaultNamespaceForPrincipal` and write directly into the
33225
+ * orchestrator's declared bulk-import write namespace. This keeps
33226
+ * writes deterministic even when namespace policies named `"default"`
33227
+ * exist alongside a different `config.defaultNamespace`, and also
33228
+ * guards against regex-catch-all principal rules steering bulk-import
33229
+ * into an unexpected tenant.
33230
+ *
33231
+ * Per-invocation namespace routing (letting callers target a namespace
33232
+ * other than `bulkImportWriteNamespace()`) is a separate feature tracked
33233
+ * as a follow-up — the hook is the `writeNamespaceOverride` option, but
33234
+ * the CLI surface does not yet expose a `--namespace` flag.
33235
+ */
33236
+ async ingestBulkImportBatch(turns, options = {}) {
33237
+ if (!Array.isArray(turns) || turns.length === 0) return;
33238
+ const sessionKey = `bulk-import:batch:${Date.now().toString(36)}-` + randomBytes(6).toString("hex");
33239
+ const sessionTurns = [];
33240
+ for (const turn of turns) {
33241
+ if (turn.role !== "user" && turn.role !== "assistant") continue;
33242
+ sessionTurns.push({
33243
+ role: turn.role,
33244
+ content: turn.content,
33245
+ timestamp: turn.timestamp,
33246
+ sessionKey
33247
+ });
33248
+ }
33249
+ if (sessionTurns.length === 0) return;
33250
+ await new Promise((resolve, reject) => {
33251
+ void this.queueBufferedExtraction(sessionTurns, "trigger_mode", {
33252
+ skipDedupeCheck: true,
33253
+ clearBufferAfterExtraction: false,
33254
+ skipCharThreshold: true,
33255
+ bufferKey: sessionKey,
33256
+ extractionDeadlineMs: options.deadlineMs,
33257
+ writeNamespaceOverride: this.bulkImportWriteNamespace(),
33258
+ onTaskSettled: (err) => err ? reject(err) : resolve()
33259
+ }).catch(reject);
33260
+ });
33261
+ }
32553
33262
  async observeSessionHeartbeat(sessionKey, options = {}) {
32554
33263
  if (this.config.sessionObserverEnabled !== true) return;
32555
33264
  if (!sessionKey || sessionKey.length === 0) return;
@@ -32616,7 +33325,8 @@ _Context: ${topQuestion.context}_`
32616
33325
  skipCharThreshold: options.skipCharThreshold ?? false,
32617
33326
  deadlineMs: options.extractionDeadlineMs,
32618
33327
  bufferKey,
32619
- abortSignal: options.abortSignal
33328
+ abortSignal: options.abortSignal,
33329
+ writeNamespaceOverride: options.writeNamespaceOverride
32620
33330
  });
32621
33331
  options.onTaskSettled?.();
32622
33332
  } catch (err) {
@@ -32742,7 +33452,7 @@ ${normalized}`).digest("hex");
32742
33452
  return;
32743
33453
  }
32744
33454
  const principal = resolvePrincipal(sessionKey, this.config);
32745
- const selfNamespace = defaultNamespaceForPrincipal(principal, this.config);
33455
+ const selfNamespace = typeof options.writeNamespaceOverride === "string" && options.writeNamespaceOverride.length > 0 ? options.writeNamespaceOverride : defaultNamespaceForPrincipal(principal, this.config);
32746
33456
  const storage = await this.storageRouter.storageFor(selfNamespace);
32747
33457
  const shouldPersistProcessedFingerprint = normalizedTurns.some(
32748
33458
  (turn) => turn.persistProcessedFingerprint === true
@@ -33426,6 +34136,9 @@ ${normalized}`).digest("hex");
33426
34136
  continue;
33427
34137
  }
33428
34138
  const judgeCategory = preRoutedCategories[fi] ?? f.category;
34139
+ if (judgeCategory === "procedure") {
34140
+ continue;
34141
+ }
33429
34142
  const tags = Array.isArray(f.tags) ? f.tags : [];
33430
34143
  const imp = scoreImportance(
33431
34144
  f.content,
@@ -33520,6 +34233,10 @@ ${normalized}`).digest("hex");
33520
34233
  writeCategory,
33521
34234
  fact.tags
33522
34235
  );
34236
+ if (writeCategory === "procedure" && this.config.procedural?.enabled !== true) {
34237
+ log.debug("persistExtraction: skip procedure memory (procedural.enabled is false)");
34238
+ continue;
34239
+ }
33523
34240
  if (!isAboveImportanceThreshold(
33524
34241
  importance.level,
33525
34242
  this.config.extractionMinImportanceLevel
@@ -33548,6 +34265,24 @@ ${normalized}`).digest("hex");
33548
34265
  }
33549
34266
  }
33550
34267
  }
34268
+ if (writeCategory === "procedure") {
34269
+ const procGate = validateProcedureExtraction({
34270
+ content: fact.content,
34271
+ procedureSteps: fact.procedureSteps
34272
+ });
34273
+ if (!procGate.durable) {
34274
+ if (this.config.extractionJudgeShadow) {
34275
+ log.info(
34276
+ `extraction-procedure-gate[shadow]: would reject "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34277
+ );
34278
+ } else {
34279
+ log.debug(
34280
+ `extraction-procedure-gate: rejected "${fact.content.slice(0, 60)}\u2026" reason="${procGate.reason}"`
34281
+ );
34282
+ continue;
34283
+ }
34284
+ }
34285
+ }
33551
34286
  let pendingSemanticSkip = null;
33552
34287
  if (this.config.semanticDedupEnabled) {
33553
34288
  let semanticDecision;
@@ -33627,7 +34362,7 @@ ${normalized}`).digest("hex");
33627
34362
  dedupedCount++;
33628
34363
  continue;
33629
34364
  }
33630
- if (this.config.chunkingEnabled) {
34365
+ if (this.config.chunkingEnabled && writeCategory !== "procedure") {
33631
34366
  let chunkResult;
33632
34367
  if (this.config.semanticChunkingEnabled) {
33633
34368
  try {
@@ -33822,8 +34557,9 @@ ${normalized}`).digest("hex");
33822
34557
  links.push(...suggestedLinks);
33823
34558
  }
33824
34559
  }
33825
- const memoryKind = this.config.episodeNoteModeEnabled ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory) : void 0;
33826
- const citedFactContent = applyInlineCitation(fact.content);
34560
+ const memoryKind = writeCategory === "procedure" ? void 0 : this.config.episodeNoteModeEnabled ? classifyMemoryKind(fact.content, fact.tags ?? [], writeCategory) : void 0;
34561
+ const rawPersistBody = writeCategory === "procedure" ? buildProcedurePersistBody(fact.content, fact.procedureSteps) : fact.content;
34562
+ const citedFactContent = applyInlineCitation(rawPersistBody);
33827
34563
  const memoryId = await targetStorage.writeMemory(
33828
34564
  writeCategory,
33829
34565
  citedFactContent,
@@ -33840,7 +34576,7 @@ ${normalized}`).digest("hex");
33840
34576
  intentEntityTypes: inferredIntent?.entityTypes,
33841
34577
  memoryKind,
33842
34578
  structuredAttributes: fact.structuredAttributes,
33843
- contentHashSource: fact.content
34579
+ contentHashSource: writeCategory === "fact" ? fact.content : void 0
33844
34580
  }
33845
34581
  );
33846
34582
  if (routedRuleId) {
@@ -39064,8 +39800,8 @@ Best for:
39064
39800
  ),
39065
39801
  category: Type.Optional(
39066
39802
  Type.String({
39067
- description: 'Category: "fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule" (default: "fact")',
39068
- enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]
39803
+ description: 'Category: "fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure" (default: "fact")',
39804
+ enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure"]
39069
39805
  })
39070
39806
  ),
39071
39807
  tags: Type.Optional(
@@ -39123,7 +39859,7 @@ Best for:
39123
39859
  category: Type.Optional(
39124
39860
  Type.String({
39125
39861
  description: "Memory category.",
39126
- enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule"]
39862
+ enum: ["fact", "preference", "correction", "entity", "decision", "relationship", "principle", "commitment", "moment", "skill", "rule", "procedure"]
39127
39863
  })
39128
39864
  ),
39129
39865
  tags: Type.Optional(
@@ -40192,8 +40928,8 @@ Returns: Performance trace data with timing breakdown`,
40192
40928
  }
40193
40929
 
40194
40930
  // ../remnic-core/src/cli.ts
40195
- import path71 from "path";
40196
- import { access as access5, readFile as readFile45, readdir as readdir25, unlink as unlink11 } from "fs/promises";
40931
+ import path72 from "path";
40932
+ import { access as access5, readFile as readFile46, readdir as readdir26, unlink as unlink11 } from "fs/promises";
40197
40933
  import { createHash as createHash14 } from "crypto";
40198
40934
 
40199
40935
  // ../remnic-core/src/transfer/export-json.ts
@@ -41070,8 +41806,8 @@ function gatherCandidates(input, warnings) {
41070
41806
  const record = rec;
41071
41807
  const content = typeof record.content === "string" ? record.content : null;
41072
41808
  if (!content) continue;
41073
- const path99 = typeof record.path === "string" ? record.path : "";
41074
- if (!path99.startsWith("transcripts/") && !path99.includes("/transcripts/")) continue;
41809
+ const path100 = typeof record.path === "string" ? record.path : "";
41810
+ if (!path100.startsWith("transcripts/") && !path100.includes("/transcripts/")) continue;
41075
41811
  rows.push(...parseJsonl(content, warnings));
41076
41812
  }
41077
41813
  return rows;
@@ -41126,6 +41862,158 @@ var openclawReplayNormalizer = {
41126
41862
  }
41127
41863
  };
41128
41864
 
41865
+ // ../remnic-core/src/bulk-import/types.ts
41866
+ var VALID_ROLES2 = /* @__PURE__ */ new Set(["user", "assistant", "other"]);
41867
+ function isImportRole(value) {
41868
+ return typeof value === "string" && VALID_ROLES2.has(value);
41869
+ }
41870
+ function parseIsoTimestamp2(value) {
41871
+ return parseIsoOffsetTimestamp(value);
41872
+ }
41873
+ function validateImportTurn(turn, index) {
41874
+ const issues = [];
41875
+ if (!turn || typeof turn !== "object") {
41876
+ issues.push({
41877
+ code: "turn.invalid",
41878
+ message: "Import turn must be an object.",
41879
+ index
41880
+ });
41881
+ return issues;
41882
+ }
41883
+ if (!isImportRole(turn.role)) {
41884
+ issues.push({
41885
+ code: "turn.role.invalid",
41886
+ message: `Import turn role must be 'user', 'assistant', or 'other', received '${String(turn.role)}'.`,
41887
+ index
41888
+ });
41889
+ }
41890
+ if (!turn.content || typeof turn.content !== "string" || turn.content.trim().length === 0) {
41891
+ issues.push({
41892
+ code: "turn.content.invalid",
41893
+ message: "Import turn content must be a non-empty string.",
41894
+ index
41895
+ });
41896
+ }
41897
+ if (!turn.timestamp || typeof turn.timestamp !== "string" || parseIsoTimestamp2(turn.timestamp) === null) {
41898
+ issues.push({
41899
+ code: "turn.timestamp.invalid",
41900
+ message: `Import turn timestamp must be a valid ISO timestamp, received '${String(turn.timestamp)}'.`,
41901
+ index
41902
+ });
41903
+ }
41904
+ return issues;
41905
+ }
41906
+
41907
+ // ../remnic-core/src/bulk-import/registry.ts
41908
+ var adapters = /* @__PURE__ */ new Map();
41909
+ function registerBulkImportSource(adapter) {
41910
+ if (!adapter || typeof adapter !== "object") {
41911
+ throw new Error("bulk-import adapter must be an object");
41912
+ }
41913
+ if (!adapter.name || typeof adapter.name !== "string" || adapter.name.trim().length === 0) {
41914
+ throw new Error("bulk-import adapter name must be a non-empty string");
41915
+ }
41916
+ if (typeof adapter.parse !== "function") {
41917
+ throw new Error(
41918
+ `bulk-import adapter '${adapter.name}' must have a parse function`
41919
+ );
41920
+ }
41921
+ const key = adapter.name.trim();
41922
+ if (adapters.has(key)) {
41923
+ throw new Error(
41924
+ `bulk-import source adapter '${key}' is already registered`
41925
+ );
41926
+ }
41927
+ const normalized = adapter.name === key ? adapter : { ...adapter, name: key };
41928
+ adapters.set(key, normalized);
41929
+ }
41930
+ function getBulkImportSource(name) {
41931
+ if (typeof name !== "string") return void 0;
41932
+ const key = name.trim();
41933
+ if (key.length === 0) return void 0;
41934
+ return adapters.get(key);
41935
+ }
41936
+ function listBulkImportSources() {
41937
+ return [...adapters.keys()];
41938
+ }
41939
+
41940
+ // ../remnic-core/src/bulk-import/pipeline.ts
41941
+ var DEFAULT_BATCH_SIZE = 20;
41942
+ var MIN_BATCH_SIZE = 1;
41943
+ var MAX_BATCH_SIZE = 1e3;
41944
+ function validateBatchSize(value) {
41945
+ if (value === void 0) return DEFAULT_BATCH_SIZE;
41946
+ if (typeof value !== "number" || !Number.isFinite(value)) {
41947
+ throw new Error(
41948
+ `batchSize must be a finite number, received ${String(value)}`
41949
+ );
41950
+ }
41951
+ if (!Number.isInteger(value)) {
41952
+ throw new Error(
41953
+ `batchSize must be an integer, received ${value}`
41954
+ );
41955
+ }
41956
+ if (value < MIN_BATCH_SIZE || value > MAX_BATCH_SIZE) {
41957
+ throw new Error(
41958
+ `batchSize must be between ${MIN_BATCH_SIZE} and ${MAX_BATCH_SIZE}, received ${value}`
41959
+ );
41960
+ }
41961
+ return value;
41962
+ }
41963
+ async function runBulkImportPipeline(source, options = {}, processBatch) {
41964
+ const batchSize = validateBatchSize(options.batchSize);
41965
+ const dryRun = options.dryRun === true;
41966
+ const result = {
41967
+ memoriesCreated: 0,
41968
+ duplicatesSkipped: 0,
41969
+ entitiesCreated: 0,
41970
+ turnsProcessed: 0,
41971
+ batchesProcessed: 0,
41972
+ errors: []
41973
+ };
41974
+ const turns = source.turns;
41975
+ if (!turns || turns.length === 0) {
41976
+ return result;
41977
+ }
41978
+ const validTurns = [];
41979
+ for (let i = 0; i < turns.length; i += 1) {
41980
+ const issues = validateImportTurn(turns[i], i);
41981
+ if (issues.length > 0) {
41982
+ const error = {
41983
+ batchIndex: -1,
41984
+ message: issues.map((iss) => iss.message).join("; ")
41985
+ };
41986
+ result.errors.push(error);
41987
+ } else {
41988
+ validTurns.push(turns[i]);
41989
+ }
41990
+ }
41991
+ if (dryRun) {
41992
+ result.turnsProcessed = validTurns.length;
41993
+ result.batchesProcessed = validTurns.length > 0 ? Math.ceil(validTurns.length / batchSize) : 0;
41994
+ return result;
41995
+ }
41996
+ let batchIndex = 0;
41997
+ for (let i = 0; i < validTurns.length; i += batchSize) {
41998
+ const batch = validTurns.slice(i, i + batchSize);
41999
+ try {
42000
+ const batchResult = await processBatch(batch);
42001
+ result.memoriesCreated += batchResult.memoriesCreated;
42002
+ result.duplicatesSkipped += batchResult.duplicatesSkipped;
42003
+ if (typeof batchResult.entitiesCreated === "number") {
42004
+ result.entitiesCreated += batchResult.entitiesCreated;
42005
+ }
42006
+ } catch (err) {
42007
+ const message = err instanceof Error ? err.message : String(err);
42008
+ result.errors.push({ batchIndex, message });
42009
+ }
42010
+ result.turnsProcessed += batch.length;
42011
+ result.batchesProcessed += 1;
42012
+ batchIndex += 1;
42013
+ }
42014
+ return result;
42015
+ }
42016
+
41129
42017
  // ../remnic-core/src/maintenance/archive-observations.ts
41130
42018
  import path55 from "path";
41131
42019
  import { mkdir as mkdir40, readdir as readdir19, readFile as readFile33, unlink as unlink8, writeFile as writeFile37 } from "fs/promises";
@@ -44474,6 +45362,98 @@ function sleep2(ms) {
44474
45362
  return new Promise((resolve) => setTimeout(resolve, ms));
44475
45363
  }
44476
45364
 
45365
+ // ../remnic-core/src/procedural/procedure-miner.ts
45366
+ function clusterKey(record) {
45367
+ const goal = record.goal.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 120);
45368
+ const refs = [...record.entityRefs ?? []].map((r) => r.trim().toLowerCase()).sort();
45369
+ return `${goal}|${refs.join(",")}`;
45370
+ }
45371
+ function successRate(group) {
45372
+ if (group.length === 0) return 0;
45373
+ const ok = group.filter((g) => g.outcomeKind === "success" || g.outcomeKind === "partial").length;
45374
+ return ok / group.length;
45375
+ }
45376
+ function pseudoStepsFromCluster(group) {
45377
+ const sentences = [];
45378
+ const pushUnique = (raw) => {
45379
+ const t = raw.trim();
45380
+ if (t.length < 8) return;
45381
+ if (!sentences.includes(t)) sentences.push(t);
45382
+ };
45383
+ for (const g of group) {
45384
+ const parts = [g.actionSummary, g.observationSummary, g.outcomeSummary].join(" ").split(/[.!?]\s+|;|\n+/).map((s) => s.trim()).filter((s) => s.length > 12);
45385
+ for (const p of parts) pushUnique(p);
45386
+ if (sentences.length >= 5) break;
45387
+ }
45388
+ if (sentences.length < 2 && group[0]) {
45389
+ pushUnique(`${group[0].goal.trim()} \u2014 confirm prerequisites and context.`);
45390
+ pushUnique("Execute the planned actions, then record the outcome.");
45391
+ }
45392
+ return sentences.slice(0, 6).map((intent, i) => ({
45393
+ order: i + 1,
45394
+ intent
45395
+ }));
45396
+ }
45397
+ async function hasExistingClusterWrite(storage, cluster) {
45398
+ const memories = await storage.readAllMemories();
45399
+ for (const m of memories) {
45400
+ if (m.frontmatter.category !== "procedure") continue;
45401
+ const c = m.frontmatter.structuredAttributes?.procedure_cluster;
45402
+ if (c === cluster) return true;
45403
+ }
45404
+ return false;
45405
+ }
45406
+ async function runProcedureMining(options) {
45407
+ const cfg = options.config.procedural;
45408
+ if (!cfg?.enabled) {
45409
+ return { clustersProcessed: 0, proceduresWritten: 0, skippedReason: "procedural_disabled" };
45410
+ }
45411
+ if (cfg.minOccurrences <= 0) {
45412
+ return { clustersProcessed: 0, proceduresWritten: 0, skippedReason: "minOccurrences_zero" };
45413
+ }
45414
+ const { trajectories } = await readCausalTrajectoryRecords({
45415
+ memoryDir: options.memoryDir
45416
+ });
45417
+ const recent = filterTrajectoriesByLookbackDays(trajectories, cfg.lookbackDays);
45418
+ const clusters = /* @__PURE__ */ new Map();
45419
+ for (const t of recent) {
45420
+ const key = clusterKey(t);
45421
+ const arr = clusters.get(key) ?? [];
45422
+ arr.push(t);
45423
+ clusters.set(key, arr);
45424
+ }
45425
+ let clustersProcessed = 0;
45426
+ let proceduresWritten = 0;
45427
+ for (const [key, group] of clusters) {
45428
+ if (group.length < cfg.minOccurrences) continue;
45429
+ const rate = successRate(group);
45430
+ if (rate < cfg.successFloor) continue;
45431
+ clustersProcessed += 1;
45432
+ if (await hasExistingClusterWrite(options.storage, key)) {
45433
+ log.debug(`procedure-miner: skip duplicate cluster key=${key.slice(0, 40)}\u2026`);
45434
+ continue;
45435
+ }
45436
+ const steps = normalizeProcedureSteps(pseudoStepsFromCluster(group));
45437
+ if (steps.length < 2) continue;
45438
+ const title = `When you work on goals like: ${group[0].goal.trim().slice(0, 140)}`;
45439
+ const body = buildProcedurePersistBody(title, steps);
45440
+ const promote = cfg.autoPromoteEnabled === true && group.length >= cfg.autoPromoteOccurrences && rate >= cfg.successFloor;
45441
+ await options.storage.writeMemory("procedure", body, {
45442
+ source: "procedure-miner",
45443
+ status: promote ? "active" : "pending_review",
45444
+ tags: ["procedure-miner", "causal-trajectory"],
45445
+ structuredAttributes: {
45446
+ procedure_cluster: key.slice(0, 500),
45447
+ trajectory_ids: group.map((g) => g.trajectoryId).join(",").slice(0, 1900),
45448
+ trajectory_count: String(group.length),
45449
+ success_rate: rate.toFixed(4)
45450
+ }
45451
+ });
45452
+ proceduresWritten += 1;
45453
+ }
45454
+ return { clustersProcessed, proceduresWritten };
45455
+ }
45456
+
44477
45457
  // ../remnic-core/src/briefing.ts
44478
45458
  import { readFile as readFile41 } from "fs/promises";
44479
45459
  import path67 from "path";
@@ -45113,10 +46093,10 @@ async function loadTodayCalendar(source, now) {
45113
46093
  }
45114
46094
  function buildOpenAiFollowupGenerator(cfg) {
45115
46095
  return async ({ sections, windowLabel, maxFollowups }) => {
45116
- const { OpenAI: OpenAI2 } = await import("./openai-53ZQ46RZ.js");
46096
+ const { OpenAI: OpenAI3 } = await import("openai");
45117
46097
  const clientOpts = { apiKey: cfg.apiKey };
45118
46098
  if (cfg.baseURL) clientOpts.baseURL = cfg.baseURL;
45119
- const client = new OpenAI2(clientOpts);
46099
+ const client = new OpenAI3(clientOpts);
45120
46100
  const prompt = buildFollowupPrompt(sections, windowLabel, maxFollowups);
45121
46101
  const response = await client.responses.create({
45122
46102
  model: cfg.model,
@@ -46185,6 +47165,25 @@ var EngramAccessService = class {
46185
47165
  reportPath: result.reportPath
46186
47166
  };
46187
47167
  }
47168
+ async procedureMiningRun(request, principal) {
47169
+ const resolvedNamespace = this.resolveWritableNamespace(
47170
+ request.namespace,
47171
+ void 0,
47172
+ request.authenticatedPrincipal ?? principal
47173
+ );
47174
+ const storage = await this.orchestrator.getStorage(resolvedNamespace);
47175
+ const result = await runProcedureMining({
47176
+ memoryDir: storage.dir,
47177
+ storage,
47178
+ config: this.orchestrator.config
47179
+ });
47180
+ return {
47181
+ namespace: resolvedNamespace,
47182
+ clustersProcessed: result.clustersProcessed,
47183
+ proceduresWritten: result.proceduresWritten,
47184
+ skippedReason: result.skippedReason
47185
+ };
47186
+ }
46188
47187
  async trustZoneStatus(namespace, principal) {
46189
47188
  const resolvedNamespace = this.resolveReadableNamespace(namespace, principal);
46190
47189
  const storage = await this.orchestrator.getStorage(resolvedNamespace);
@@ -47213,6 +48212,17 @@ var EngramMcpServer = class {
47213
48212
  additionalProperties: false
47214
48213
  }
47215
48214
  },
48215
+ {
48216
+ name: "engram.procedure_mining_run",
48217
+ description: "Run procedural memory mining from causal trajectories (issue #519). Respects procedural.enabled; writes under procedures/ when clusters qualify.",
48218
+ inputSchema: {
48219
+ type: "object",
48220
+ properties: {
48221
+ namespace: { type: "string" }
48222
+ },
48223
+ additionalProperties: false
48224
+ }
48225
+ },
47216
48226
  {
47217
48227
  name: "engram.memory_get",
47218
48228
  description: "Fetch one Remnic memory by id.",
@@ -48092,6 +49102,14 @@ ${body}`;
48092
49102
  batchSize: typeof args.batchSize === "number" && Number.isFinite(args.batchSize) ? args.batchSize : void 0,
48093
49103
  authenticatedPrincipal: effectivePrincipal
48094
49104
  }, effectivePrincipal);
49105
+ case "engram.procedure_mining_run":
49106
+ return this.service.procedureMiningRun(
49107
+ {
49108
+ namespace: typeof args.namespace === "string" ? args.namespace : void 0,
49109
+ authenticatedPrincipal: effectivePrincipal
49110
+ },
49111
+ effectivePrincipal
49112
+ );
48095
49113
  case "engram.memory_get":
48096
49114
  return this.service.memoryGet(
48097
49115
  typeof args.memoryId === "string" ? args.memoryId : "",
@@ -48457,7 +49475,8 @@ var categorySchema = external_exports.enum([
48457
49475
  "commitment",
48458
49476
  "moment",
48459
49477
  "skill",
48460
- "rule"
49478
+ "rule",
49479
+ "procedure"
48461
49480
  ]).optional();
48462
49481
  var confidenceSchema = external_exports.number().min(0).max(1).optional();
48463
49482
  var tagsSchema = external_exports.array(external_exports.string().max(256)).max(50).optional();
@@ -48656,8 +49675,8 @@ var HermesAdapter = class {
48656
49675
  // ../remnic-core/src/adapters/registry.ts
48657
49676
  var AdapterRegistry = class {
48658
49677
  adapters;
48659
- constructor(adapters) {
48660
- this.adapters = adapters ?? [
49678
+ constructor(adapters2) {
49679
+ this.adapters = adapters2 ?? [
48661
49680
  new HermesAdapter(),
48662
49681
  new ReplitAdapter(),
48663
49682
  new CodexAdapter(),
@@ -50129,6 +51148,10 @@ async function promoteSemanticRuleFromMemory(options) {
50129
51148
  return report;
50130
51149
  }
50131
51150
 
51151
+ // ../remnic-core/src/training-export/converter.ts
51152
+ import { lstat as lstat2, readdir as readdir25, readFile as readFile45, realpath as realpath3 } from "fs/promises";
51153
+ import path71 from "path";
51154
+
50132
51155
  // ../remnic-core/src/cli.ts
50133
51156
  function rankCandidateForKeep(a, b) {
50134
51157
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -50319,7 +51342,7 @@ async function runRepairMemoryProjectionCliCommand(options) {
50319
51342
  });
50320
51343
  }
50321
51344
  async function runMemoryTimelineCliCommand(options) {
50322
- const storage = new (await import("./storage-HW6SRQCK.js")).StorageManager(options.memoryDir);
51345
+ const storage = new (await import("./storage-BA6OBLMK.js")).StorageManager(options.memoryDir);
50323
51346
  return storage.getMemoryTimeline(options.memoryId, options.limit);
50324
51347
  }
50325
51348
  async function runMemoryGovernanceCliCommand(options) {
@@ -50347,7 +51370,7 @@ async function runMemoryGovernanceRestoreCliCommand(options) {
50347
51370
  });
50348
51371
  }
50349
51372
  async function runMemoryReviewDispositionCliCommand(options) {
50350
- const storage = new (await import("./storage-HW6SRQCK.js")).StorageManager(options.memoryDir);
51373
+ const storage = new (await import("./storage-BA6OBLMK.js")).StorageManager(options.memoryDir);
50351
51374
  const memory = await storage.getMemoryById(options.memoryId);
50352
51375
  if (!memory) throw new Error(`memory not found: ${options.memoryId}`);
50353
51376
  const updated = await storage.writeMemoryFrontmatter(memory, {
@@ -50513,7 +51536,7 @@ async function runSemanticRulePromoteCliCommand(options) {
50513
51536
  });
50514
51537
  }
50515
51538
  async function runCompoundingPromoteCliCommand(options) {
50516
- const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-BEV4BHEH.js");
51539
+ const { CompoundingEngine: CompoundingEngine2 } = await import("./engine-BU6GNUJ5.js");
50517
51540
  const config = parseConfig({
50518
51541
  memoryDir: options.memoryDir,
50519
51542
  qmdEnabled: false,
@@ -50930,7 +51953,7 @@ function policyVersionForValues(values, config) {
50930
51953
  return createHash14("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
50931
51954
  }
50932
51955
  async function readRuntimePolicySnapshot2(config, fileName) {
50933
- const filePath = path71.join(config.memoryDir, "state", fileName);
51956
+ const filePath = path72.join(config.memoryDir, "state", fileName);
50934
51957
  const snapshot = await readRuntimePolicySnapshot(filePath, {
50935
51958
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
50936
51959
  });
@@ -51486,7 +52509,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
51486
52509
  }
51487
52510
  async function runReplayCliCommand(orchestrator, options) {
51488
52511
  const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
51489
- const inputRaw = await readFile45(options.inputPath, "utf-8");
52512
+ const inputRaw = await readFile46(options.inputPath, "utf-8");
51490
52513
  const registry = buildReplayNormalizerRegistry([
51491
52514
  openclawReplayNormalizer,
51492
52515
  claudeReplayNormalizer,
@@ -51548,10 +52571,101 @@ async function runReplayCliCommand(orchestrator, options) {
51548
52571
  }
51549
52572
  return summary;
51550
52573
  }
52574
+ async function ensureBuiltInBulkImportAdapters() {
52575
+ if (!getBulkImportSource("weclone")) {
52576
+ const wecloneSpecifier = "@remnic/import-weclone";
52577
+ try {
52578
+ const mod = await import(wecloneSpecifier);
52579
+ if (mod.wecloneImportAdapter) {
52580
+ try {
52581
+ registerBulkImportSource(mod.wecloneImportAdapter);
52582
+ } catch {
52583
+ }
52584
+ }
52585
+ } catch {
52586
+ }
52587
+ }
52588
+ }
52589
+ async function runBulkImportCliCommand(opts) {
52590
+ await ensureBuiltInBulkImportAdapters();
52591
+ const adapter = getBulkImportSource(opts.source);
52592
+ if (!adapter) {
52593
+ const registered = listBulkImportSources();
52594
+ const list = registered.length > 0 ? registered.map((n) => `'${n}'`).join(", ") : "(none registered)";
52595
+ throw new Error(
52596
+ `Unknown bulk-import source '${opts.source}'. Valid sources: ${list}`
52597
+ );
52598
+ }
52599
+ if (opts.dryRun !== true && typeof opts.ingestBatch !== "function") {
52600
+ throw new Error(
52601
+ "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."
52602
+ );
52603
+ }
52604
+ const inputRaw = await readFile46(opts.file, "utf-8");
52605
+ let inputParsed;
52606
+ try {
52607
+ inputParsed = JSON.parse(inputRaw);
52608
+ } catch (err) {
52609
+ throw new Error(
52610
+ `Failed to parse import file as JSON: ${err.message}`
52611
+ );
52612
+ }
52613
+ if (typeof inputParsed !== "object" || inputParsed === null) {
52614
+ throw new Error(
52615
+ "Import file must contain a JSON object or array, got " + (inputParsed === null ? "null" : typeof inputParsed)
52616
+ );
52617
+ }
52618
+ const parsed = await adapter.parse(inputParsed, {
52619
+ strict: opts.strict === true,
52620
+ platform: opts.platform
52621
+ });
52622
+ const processBatch = opts.ingestBatch ?? (async () => {
52623
+ throw new Error(
52624
+ "Bulk import persistence is not wired: no ingestBatch callback was provided by the host CLI."
52625
+ );
52626
+ });
52627
+ const result = await runBulkImportPipeline(
52628
+ parsed,
52629
+ {
52630
+ batchSize: opts.batchSize,
52631
+ dryRun: opts.dryRun,
52632
+ dedup: true,
52633
+ trustLevel: "import"
52634
+ },
52635
+ processBatch
52636
+ );
52637
+ const out = opts.stdout;
52638
+ out.write(`Bulk import complete (source: ${opts.source})
52639
+ `);
52640
+ out.write(` Turns processed: ${result.turnsProcessed}
52641
+ `);
52642
+ out.write(` Batches processed: ${result.batchesProcessed}
52643
+ `);
52644
+ out.write(` Memories created: ${result.memoriesCreated}
52645
+ `);
52646
+ out.write(` Duplicates skipped: ${result.duplicatesSkipped}
52647
+ `);
52648
+ if (result.errors.length > 0) {
52649
+ out.write(` Errors: ${result.errors.length}
52650
+ `);
52651
+ if (opts.verbose) {
52652
+ for (const err of result.errors) {
52653
+ opts.stderr.write(
52654
+ ` [batch ${err.batchIndex}] ${err.message}
52655
+ `
52656
+ );
52657
+ }
52658
+ }
52659
+ }
52660
+ if (opts.dryRun) {
52661
+ out.write(" (dry run \u2014 no memories were stored)\n");
52662
+ }
52663
+ return result;
52664
+ }
51551
52665
  async function getPluginVersion() {
51552
52666
  try {
51553
52667
  const pkgPath = new URL("../package.json", import.meta.url);
51554
- const raw = await readFile45(pkgPath, "utf-8");
52668
+ const raw = await readFile46(pkgPath, "utf-8");
51555
52669
  const parsed = JSON.parse(raw);
51556
52670
  return parsed.version ?? "unknown";
51557
52671
  } catch {
@@ -51575,59 +52689,71 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace, options) {
51575
52689
  }
51576
52690
  return orchestrator.config.memoryDir;
51577
52691
  }
51578
- const candidate = path71.join(orchestrator.config.memoryDir, "namespaces", ns);
52692
+ const candidate = path72.join(orchestrator.config.memoryDir, "namespaces", ns);
51579
52693
  if (ns === orchestrator.config.defaultNamespace) {
51580
52694
  return await exists3(candidate) ? candidate : orchestrator.config.memoryDir;
51581
52695
  }
51582
52696
  return candidate;
51583
52697
  }
51584
- async function readAllMemoryFiles(memoryDir) {
51585
- const roots = [path71.join(memoryDir, "facts"), path71.join(memoryDir, "corrections")];
51586
- const out = [];
52698
+ async function walkMemoryMarkdownFiles(memoryDir, visit) {
52699
+ const roots = [path72.join(memoryDir, "facts"), path72.join(memoryDir, "corrections")];
51587
52700
  const walk = async (dir) => {
51588
52701
  let entries;
51589
52702
  try {
51590
- entries = await readdir25(dir, { withFileTypes: true });
52703
+ entries = await readdir26(dir, { withFileTypes: true });
51591
52704
  } catch {
51592
52705
  return;
51593
52706
  }
51594
52707
  for (const entry of entries) {
51595
52708
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
51596
- const fullPath = path71.join(dir, entryName);
52709
+ const fullPath = path72.join(dir, entryName);
51597
52710
  if (entry.isDirectory()) {
51598
52711
  await walk(fullPath);
51599
52712
  continue;
51600
52713
  }
51601
52714
  if (!entry.isFile() || !entryName.endsWith(".md")) continue;
51602
- try {
51603
- const raw = await readFile45(fullPath, "utf-8");
51604
- const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
51605
- if (!parsed) continue;
51606
- const fmRaw = parsed[1];
51607
- const body = parsed[2] ?? "";
51608
- const get = (key) => {
51609
- const match = fmRaw.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
51610
- return match ? match[1].trim() : "";
51611
- };
51612
- const confidenceRaw = get("confidence");
51613
- const confidence = confidenceRaw.length > 0 ? Number(confidenceRaw) : void 0;
51614
- out.push({
51615
- path: fullPath,
51616
- content: body,
51617
- frontmatter: {
51618
- id: get("id") || void 0,
51619
- confidence: Number.isFinite(confidence) ? confidence : void 0,
51620
- updated: get("updated") || void 0,
51621
- created: get("created") || void 0
51622
- }
51623
- });
51624
- } catch {
51625
- }
52715
+ await visit(fullPath);
51626
52716
  }
51627
52717
  };
51628
52718
  for (const root of roots) {
51629
52719
  await walk(root);
51630
52720
  }
52721
+ }
52722
+ async function listMemoryMarkdownFilePaths(memoryDir) {
52723
+ const paths = [];
52724
+ await walkMemoryMarkdownFiles(memoryDir, (fullPath) => {
52725
+ paths.push(fullPath);
52726
+ });
52727
+ return paths;
52728
+ }
52729
+ async function readAllMemoryFiles(memoryDir) {
52730
+ const out = [];
52731
+ await walkMemoryMarkdownFiles(memoryDir, async (fullPath) => {
52732
+ try {
52733
+ const raw = await readFile46(fullPath, "utf-8");
52734
+ const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
52735
+ if (!parsed) return;
52736
+ const fmRaw = parsed[1];
52737
+ const body = parsed[2] ?? "";
52738
+ const get = (key) => {
52739
+ const match = fmRaw.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
52740
+ return match ? match[1].trim() : "";
52741
+ };
52742
+ const confidenceRaw = get("confidence");
52743
+ const confidence = confidenceRaw.length > 0 ? Number(confidenceRaw) : void 0;
52744
+ out.push({
52745
+ path: fullPath,
52746
+ content: body,
52747
+ frontmatter: {
52748
+ id: get("id") || void 0,
52749
+ confidence: Number.isFinite(confidence) ? confidence : void 0,
52750
+ updated: get("updated") || void 0,
52751
+ created: get("created") || void 0
52752
+ }
52753
+ });
52754
+ } catch {
52755
+ }
52756
+ });
51631
52757
  return out;
51632
52758
  }
51633
52759
  function formatContinuityIncidentCli(incident) {
@@ -51936,7 +53062,7 @@ function registerCli(api, orchestrator) {
51936
53062
  if (plan.moved.length > 0) {
51937
53063
  console.log("\nEntries:");
51938
53064
  for (const move of plan.moved) {
51939
- console.log(`- ${path71.basename(move.from)}`);
53065
+ console.log(`- ${path72.basename(move.from)}`);
51940
53066
  }
51941
53067
  }
51942
53068
  if (dryRun) {
@@ -52134,6 +53260,64 @@ function registerCli(api, orchestrator) {
52134
53260
  }
52135
53261
  console.log("OK");
52136
53262
  });
53263
+ cmd.command("bulk-import").description(
53264
+ "Bulk-import chat history via a registered source adapter (e.g. --source weclone)."
53265
+ ).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) => {
53266
+ const options = args[0] ?? {};
53267
+ const sourceRaw = typeof options.source === "string" ? options.source.trim() : "";
53268
+ const filePathRaw = typeof options.file === "string" ? options.file.trim() : "";
53269
+ if (sourceRaw.length === 0) {
53270
+ console.log("Missing --source. Example: openclaw engram bulk-import --source weclone --file /tmp/export.json");
53271
+ return;
53272
+ }
53273
+ if (filePathRaw.length === 0) {
53274
+ console.log("Missing --file. Example: openclaw engram bulk-import --source weclone --file /tmp/export.json");
53275
+ return;
53276
+ }
53277
+ const batchSizeRaw = parseInt(String(options.batchSize ?? "50"), 10);
53278
+ const batchSize = Number.isFinite(batchSizeRaw) && batchSizeRaw > 0 ? batchSizeRaw : 50;
53279
+ const platformRaw = typeof options.platform === "string" ? options.platform.trim() : "";
53280
+ const writeNamespace = orchestrator.bulkImportWriteNamespace();
53281
+ const writeStorage = await orchestrator.getStorageForNamespace(
53282
+ writeNamespace
53283
+ );
53284
+ const writeRoot = writeStorage.dir;
53285
+ const ingestBatch = async (turns) => {
53286
+ const before = new Set(await listMemoryMarkdownFilePaths(writeRoot));
53287
+ await orchestrator.ingestBulkImportBatch(turns, {});
53288
+ const after = await listMemoryMarkdownFilePaths(writeRoot);
53289
+ let memoriesCreated = 0;
53290
+ for (const p of after) {
53291
+ if (!before.has(p)) memoriesCreated += 1;
53292
+ }
53293
+ return { memoriesCreated, duplicatesSkipped: 0 };
53294
+ };
53295
+ try {
53296
+ const result = await runBulkImportCliCommand({
53297
+ memoryDir: writeRoot,
53298
+ source: sourceRaw,
53299
+ file: filePathRaw,
53300
+ platform: platformRaw.length > 0 ? platformRaw : void 0,
53301
+ batchSize,
53302
+ dryRun: options.dryRun === true,
53303
+ verbose: options.verbose === true,
53304
+ strict: options.strict === true,
53305
+ ingestBatch,
53306
+ stdout: process.stdout,
53307
+ stderr: process.stderr
53308
+ });
53309
+ if (result.errors.length > 0) {
53310
+ console.error(`Bulk import completed with ${result.errors.length} batch error(s).`);
53311
+ process.exitCode = 1;
53312
+ } else {
53313
+ console.log("OK");
53314
+ }
53315
+ } catch (err) {
53316
+ const message = err instanceof Error ? err.message : String(err);
53317
+ console.error(`Bulk import failed: ${message}`);
53318
+ process.exitCode = 1;
53319
+ }
53320
+ });
52137
53321
  cmd.command("benchmark-status").description("Show benchmark/evaluation harness status, benchmark packs, and latest run summary").action(async () => {
52138
53322
  const status = await runBenchmarkStatusCliCommand({
52139
53323
  memoryDir: orchestrator.config.memoryDir,
@@ -53586,7 +54770,7 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
53586
54770
  }
53587
54771
  });
53588
54772
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
53589
- const workspaceDir = path71.join(resolveHomeDir(), ".openclaw", "workspace");
54773
+ const workspaceDir = path72.join(resolveHomeDir(), ".openclaw", "workspace");
53590
54774
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
53591
54775
  if (!identity) {
53592
54776
  console.log("No identity file found.");
@@ -53809,8 +54993,8 @@ Semantic consolidation complete. clusters=${result.clustersFound}, consolidated=
53809
54993
  const options = args[0] ?? {};
53810
54994
  const threadId = options.thread;
53811
54995
  const top = parseInt(options.top ?? "10", 10);
53812
- const memoryDir = path71.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
53813
- const threading = new ThreadingManager(path71.join(memoryDir, "threads"));
54996
+ const memoryDir = path72.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
54997
+ const threading = new ThreadingManager(path72.join(memoryDir, "threads"));
53814
54998
  if (threadId) {
53815
54999
  const thread = await threading.loadThread(threadId);
53816
55000
  if (!thread) {
@@ -54286,19 +55470,19 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
54286
55470
  }
54287
55471
 
54288
55472
  // ../../src/index.ts
54289
- import { readFile as readFile51, writeFile as writeFile46 } from "fs/promises";
55473
+ import { readFile as readFile52, realpath as realpath5, writeFile as writeFile46 } from "fs/promises";
54290
55474
  import { readFileSync as readFileSync6 } from "fs";
54291
- import path97 from "path";
55475
+ import path98 from "path";
54292
55476
  import os6 from "os";
54293
55477
 
54294
55478
  // ../remnic-core/src/opik-exporter.ts
54295
- import { createHash as createHash15, randomBytes } from "crypto";
55479
+ import { createHash as createHash15, randomBytes as randomBytes2 } from "crypto";
54296
55480
  import { readFileSync as readFileSync4 } from "fs";
54297
- import path72 from "path";
55481
+ import path73 from "path";
54298
55482
  var OPIK_EXPORTER_SLOT = "__openclawOpikExporter";
54299
55483
  function readOpikOpenclawConfig(log2) {
54300
55484
  try {
54301
- const configPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH") || path72.join(resolveHomeDir(), ".openclaw", "openclaw.json");
55485
+ const configPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH") || path73.join(resolveHomeDir(), ".openclaw", "openclaw.json");
54302
55486
  const raw = JSON.parse(readFileSync4(configPath, "utf-8"));
54303
55487
  const entry = raw?.plugins?.entries?.["opik-openclaw"];
54304
55488
  if (!entry?.enabled || !entry?.config) return {};
@@ -54316,7 +55500,7 @@ function readOpikOpenclawConfig(log2) {
54316
55500
  }
54317
55501
  function uuidV7() {
54318
55502
  const now = Date.now();
54319
- const bytes = randomBytes(16);
55503
+ const bytes = randomBytes2(16);
54320
55504
  bytes[0] = now / 2 ** 40 & 255;
54321
55505
  bytes[1] = now / 2 ** 32 & 255;
54322
55506
  bytes[2] = now / 2 ** 24 & 255;
@@ -54660,8 +55844,8 @@ function cleanUserMessage(content) {
54660
55844
  }
54661
55845
 
54662
55846
  // src/public-artifacts.ts
54663
- import { readdir as readdir26, access as access6, stat as stat20, lstat as lstat2, realpath as realpath3 } from "fs/promises";
54664
- import path73 from "path";
55847
+ import { readdir as readdir27, access as access6, stat as stat20, lstat as lstat3, realpath as realpath4 } from "fs/promises";
55848
+ import path74 from "path";
54665
55849
  var PUBLIC_DIRS = [
54666
55850
  { dir: "facts", kind: "fact", contentType: "markdown" },
54667
55851
  { dir: "entities", kind: "entity", contentType: "markdown" },
@@ -54673,9 +55857,9 @@ var PUBLIC_FILES = [
54673
55857
  ];
54674
55858
  async function isContainedWithin(target, boundary) {
54675
55859
  try {
54676
- const resolvedTarget = await realpath3(target);
54677
- const resolvedBoundary = await realpath3(boundary);
54678
- return resolvedTarget === resolvedBoundary || resolvedTarget.startsWith(resolvedBoundary + path73.sep);
55860
+ const resolvedTarget = await realpath4(target);
55861
+ const resolvedBoundary = await realpath4(boundary);
55862
+ return resolvedTarget === resolvedBoundary || resolvedTarget.startsWith(resolvedBoundary + path74.sep);
54679
55863
  } catch {
54680
55864
  return false;
54681
55865
  }
@@ -54684,7 +55868,7 @@ async function listMarkdownFilesRecursive(rootDir, boundary, ancestorRealPaths)
54684
55868
  const boundaryDir = boundary ?? rootDir;
54685
55869
  let resolvedRoot;
54686
55870
  try {
54687
- resolvedRoot = await realpath3(rootDir);
55871
+ resolvedRoot = await realpath4(rootDir);
54688
55872
  } catch {
54689
55873
  return [];
54690
55874
  }
@@ -54693,18 +55877,18 @@ async function listMarkdownFilesRecursive(rootDir, boundary, ancestorRealPaths)
54693
55877
  nextAncestors.add(resolvedRoot);
54694
55878
  let entries;
54695
55879
  try {
54696
- entries = await readdir26(rootDir, { withFileTypes: true });
55880
+ entries = await readdir27(rootDir, { withFileTypes: true });
54697
55881
  } catch {
54698
55882
  return [];
54699
55883
  }
54700
55884
  entries.sort((a, b) => String(a.name).localeCompare(String(b.name)));
54701
55885
  const files = [];
54702
55886
  for (const entry of entries) {
54703
- const fullPath = path73.join(rootDir, String(entry.name));
55887
+ const fullPath = path74.join(rootDir, String(entry.name));
54704
55888
  let isDir = entry.isDirectory();
54705
55889
  let isFile = entry.isFile();
54706
55890
  try {
54707
- const linkStat = await lstat2(fullPath);
55891
+ const linkStat = await lstat3(fullPath);
54708
55892
  if (linkStat.isSymbolicLink()) {
54709
55893
  if (!await isContainedWithin(fullPath, boundaryDir)) {
54710
55894
  continue;
@@ -54738,21 +55922,21 @@ async function listRemnicPublicArtifacts(params) {
54738
55922
  const { memoryDir, workspaceDir, agentIds } = params;
54739
55923
  const artifacts = [];
54740
55924
  for (const spec of PUBLIC_DIRS) {
54741
- const dirPath = path73.join(memoryDir, spec.dir);
55925
+ const dirPath = path74.join(memoryDir, spec.dir);
54742
55926
  if (!await pathExists2(dirPath)) continue;
54743
55927
  if (!await isContainedWithin(dirPath, memoryDir)) continue;
54744
55928
  try {
54745
- const resolvedDir = await realpath3(dirPath);
54746
- const expectedParent = await realpath3(memoryDir);
54747
- const resolvedName = path73.basename(resolvedDir);
55929
+ const resolvedDir = await realpath4(dirPath);
55930
+ const expectedParent = await realpath4(memoryDir);
55931
+ const resolvedName = path74.basename(resolvedDir);
54748
55932
  if (resolvedName !== spec.dir) continue;
54749
- if (path73.dirname(resolvedDir) !== expectedParent) continue;
55933
+ if (path74.dirname(resolvedDir) !== expectedParent) continue;
54750
55934
  } catch {
54751
55935
  continue;
54752
55936
  }
54753
55937
  const files = await listMarkdownFilesRecursive(dirPath, dirPath);
54754
55938
  for (const absolutePath of files) {
54755
- const relativePath = path73.relative(memoryDir, absolutePath).replace(/\\/g, "/");
55939
+ const relativePath = path74.relative(memoryDir, absolutePath).replace(/\\/g, "/");
54756
55940
  artifacts.push({
54757
55941
  kind: spec.kind,
54758
55942
  workspaceDir,
@@ -54764,16 +55948,16 @@ async function listRemnicPublicArtifacts(params) {
54764
55948
  }
54765
55949
  }
54766
55950
  for (const spec of PUBLIC_FILES) {
54767
- const absolutePath = path73.join(memoryDir, spec.relativePath);
55951
+ const absolutePath = path74.join(memoryDir, spec.relativePath);
54768
55952
  if (!await pathExists2(absolutePath)) continue;
54769
55953
  if (!await isContainedWithin(absolutePath, memoryDir)) continue;
54770
55954
  try {
54771
- const linkStat = await lstat2(absolutePath);
55955
+ const linkStat = await lstat3(absolutePath);
54772
55956
  if (linkStat.isSymbolicLink()) {
54773
- const resolvedPath = await realpath3(absolutePath);
54774
- const expectedParent = await realpath3(memoryDir);
54775
- if (path73.dirname(resolvedPath) !== expectedParent) continue;
54776
- if (path73.basename(resolvedPath) !== path73.basename(spec.relativePath)) continue;
55957
+ const resolvedPath = await realpath4(absolutePath);
55958
+ const expectedParent = await realpath4(memoryDir);
55959
+ if (path74.dirname(resolvedPath) !== expectedParent) continue;
55960
+ if (path74.basename(resolvedPath) !== path74.basename(spec.relativePath)) continue;
54777
55961
  }
54778
55962
  } catch {
54779
55963
  continue;
@@ -54924,28 +56108,28 @@ var DEFAULT_MAX_BINARY_SIZE_BYTES = 50 * 1024 * 1024;
54924
56108
  // ../remnic-core/src/binary-lifecycle/backend.ts
54925
56109
  import fs3 from "fs";
54926
56110
  import fsp2 from "fs/promises";
54927
- import path74 from "path";
56111
+ import path75 from "path";
54928
56112
 
54929
56113
  // ../remnic-core/src/binary-lifecycle/scanner.ts
54930
56114
  import fsp3 from "fs/promises";
54931
- import path75 from "path";
56115
+ import path76 from "path";
54932
56116
 
54933
56117
  // ../remnic-core/src/binary-lifecycle/manifest.ts
54934
56118
  import fsp4 from "fs/promises";
54935
- import path76 from "path";
56119
+ import path77 from "path";
54936
56120
  import crypto3 from "crypto";
54937
56121
 
54938
56122
  // ../remnic-core/src/binary-lifecycle/pipeline.ts
54939
56123
  import fsp5 from "fs/promises";
54940
- import path77 from "path";
56124
+ import path78 from "path";
54941
56125
  import crypto4 from "crypto";
54942
56126
 
54943
56127
  // ../remnic-core/src/projection/index.ts
54944
56128
  import fs4 from "fs";
54945
- import path79 from "path";
56129
+ import path80 from "path";
54946
56130
 
54947
56131
  // ../remnic-core/src/utils/category-dir.ts
54948
- import path78 from "path";
56132
+ import path79 from "path";
54949
56133
  var CATEGORY_DIR_MAP = {
54950
56134
  correction: "corrections",
54951
56135
  question: "questions",
@@ -54956,7 +56140,8 @@ var CATEGORY_DIR_MAP = {
54956
56140
  principle: "principles",
54957
56141
  rule: "rules",
54958
56142
  skill: "skills",
54959
- relationship: "relationships"
56143
+ relationship: "relationships",
56144
+ procedure: "procedures"
54960
56145
  };
54961
56146
  var ALL_CATEGORY_DIRS = [
54962
56147
  "facts",
@@ -54969,30 +56154,30 @@ var ALL_CATEGORY_KEYS = [
54969
56154
 
54970
56155
  // ../remnic-core/src/onboarding/index.ts
54971
56156
  import fs5 from "fs";
54972
- import path80 from "path";
56157
+ import path81 from "path";
54973
56158
 
54974
56159
  // ../remnic-core/src/curation/index.ts
54975
56160
  import fs6 from "fs";
54976
- import path81 from "path";
56161
+ import path82 from "path";
54977
56162
  import crypto5 from "crypto";
54978
56163
 
54979
56164
  // ../remnic-core/src/dedup/index.ts
54980
56165
  import fs7 from "fs";
54981
- import path82 from "path";
56166
+ import path83 from "path";
54982
56167
  import crypto6 from "crypto";
54983
56168
 
54984
56169
  // ../remnic-core/src/review/index.ts
54985
56170
  import fs8 from "fs";
54986
- import path83 from "path";
56171
+ import path84 from "path";
54987
56172
 
54988
56173
  // ../remnic-core/src/sync/index.ts
54989
56174
  import fs9 from "fs";
54990
- import path84 from "path";
56175
+ import path85 from "path";
54991
56176
  import crypto7 from "crypto";
54992
56177
 
54993
56178
  // ../remnic-core/src/connectors/index.ts
54994
56179
  import fs11 from "fs";
54995
- import path87 from "path";
56180
+ import path88 from "path";
54996
56181
  import os4 from "os";
54997
56182
  import { spawnSync } from "child_process";
54998
56183
  import { createRequire as createRequire2 } from "module";
@@ -55000,31 +56185,31 @@ import { fileURLToPath as fileURLToPath4 } from "url";
55000
56185
 
55001
56186
  // ../remnic-core/src/tokens.ts
55002
56187
  import fs10 from "fs";
55003
- import path85 from "path";
55004
- import { randomBytes as randomBytes2 } from "crypto";
56188
+ import path86 from "path";
56189
+ import { randomBytes as randomBytes3 } from "crypto";
55005
56190
 
55006
56191
  // ../remnic-core/src/connectors/codex-marketplace.ts
55007
56192
  import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
55008
- import path86 from "path";
56193
+ import path87 from "path";
55009
56194
 
55010
56195
  // ../remnic-core/src/spaces/index.ts
55011
56196
  import fs12 from "fs";
55012
- import path88 from "path";
56197
+ import path89 from "path";
55013
56198
  import crypto8 from "crypto";
55014
56199
 
55015
56200
  // ../remnic-core/src/memory-extension/codex-publisher.ts
55016
56201
  import fs13 from "fs";
55017
56202
  import os5 from "os";
55018
- import path89 from "path";
56203
+ import path90 from "path";
55019
56204
 
55020
56205
  // ../remnic-core/src/taxonomy/taxonomy-loader.ts
55021
- import { readFile as readFile46, mkdir as mkdir49, writeFile as writeFile43 } from "fs/promises";
55022
- import path90 from "path";
56206
+ import { readFile as readFile47, mkdir as mkdir49, writeFile as writeFile43 } from "fs/promises";
56207
+ import path91 from "path";
55023
56208
 
55024
56209
  // ../remnic-core/src/enrichment/audit.ts
55025
- import { mkdir as mkdir50, readFile as readFile47, appendFile as appendFile4 } from "fs/promises";
56210
+ import { mkdir as mkdir50, readFile as readFile48, appendFile as appendFile4 } from "fs/promises";
55026
56211
  import { existsSync as existsSync11 } from "fs";
55027
- import path91 from "path";
56212
+ import path92 from "path";
55028
56213
 
55029
56214
  // src/openclaw-tools/shapes.ts
55030
56215
  var MemorySearchInputSchema = Type.Object({
@@ -55438,7 +56623,7 @@ async function syncHeartbeatOutcomeLinks(params) {
55438
56623
  }
55439
56624
  return { created: 0, updated: 0, linked };
55440
56625
  }
55441
- function parseIsoTimestamp2(value) {
56626
+ function parseIsoTimestamp3(value) {
55442
56627
  if (!value) return null;
55443
56628
  const parsed = Date.parse(value);
55444
56629
  return Number.isFinite(parsed) ? parsed : null;
@@ -55447,7 +56632,7 @@ function planDreamEntryFromConsolidation(params) {
55447
56632
  const now = params.now ?? /* @__PURE__ */ new Date();
55448
56633
  const latestDreamAt = Math.max(
55449
56634
  -1,
55450
- ...params.existingDreams.map((entry) => parseIsoTimestamp2(entry.timestamp)).filter((value) => value !== null)
56635
+ ...params.existingDreams.map((entry) => parseIsoTimestamp3(entry.timestamp)).filter((value) => value !== null)
55451
56636
  );
55452
56637
  if (latestDreamAt > 0 && now.getTime() - latestDreamAt < params.minIntervalMinutes * 6e4) {
55453
56638
  return null;
@@ -55591,88 +56776,100 @@ function resolveSession(commandCtx) {
55591
56776
  };
55592
56777
  }
55593
56778
  function buildSessionCommandDescriptors(pluginId, runtime) {
56779
+ const subcommands = [
56780
+ {
56781
+ name: "off",
56782
+ description: "Disable Remnic recall for this session",
56783
+ args: [],
56784
+ handler: async (commandCtx = {}) => {
56785
+ const { sessionKey, agentId } = resolveSession(commandCtx);
56786
+ await runtime.toggles.setDisabled(sessionKey, agentId, true);
56787
+ return `Remnic recall disabled for session ${sessionKey}.`;
56788
+ }
56789
+ },
56790
+ {
56791
+ name: "on",
56792
+ description: "Re-enable Remnic recall for this session",
56793
+ args: [],
56794
+ handler: async (commandCtx = {}) => {
56795
+ const { sessionKey, agentId } = resolveSession(commandCtx);
56796
+ await runtime.toggles.setDisabled(sessionKey, agentId, false);
56797
+ return `Remnic recall re-enabled for session ${sessionKey}.`;
56798
+ }
56799
+ },
56800
+ {
56801
+ name: "status",
56802
+ description: "Show Remnic recall status and last injected summary",
56803
+ args: [],
56804
+ handler: async (commandCtx = {}) => {
56805
+ const { sessionKey, agentId } = resolveSession(commandCtx);
56806
+ const resolved = await runtime.toggles.resolve(sessionKey, agentId);
56807
+ const lastRecall = runtime.getLastRecall(sessionKey);
56808
+ const summaryText = runtime.getLastRecallSummary(sessionKey);
56809
+ const summary = summaryText && summaryText.length > 0 ? summaryText : lastRecall && lastRecall.memoryIds.length > 0 ? `${lastRecall.memoryIds.length} memory item(s), latency ${lastRecall.latencyMs ?? "?"}ms` : "NONE";
56810
+ return [
56811
+ `Remnic recall is ${resolved.disabled ? "disabled" : "enabled"} for session ${sessionKey}.`,
56812
+ `Source: ${describeToggleSource(resolved.source)}.`,
56813
+ `Last recall: ${summary}.`
56814
+ ].join(" ");
56815
+ }
56816
+ },
56817
+ {
56818
+ name: "clear",
56819
+ description: "Clear the session override and use global config again",
56820
+ args: [],
56821
+ handler: async (commandCtx = {}) => {
56822
+ const { sessionKey, agentId } = resolveSession(commandCtx);
56823
+ await runtime.toggles.clear(sessionKey, agentId);
56824
+ return `Cleared the Remnic session override for ${sessionKey}.`;
56825
+ }
56826
+ },
56827
+ {
56828
+ name: "stats",
56829
+ description: "Show Remnic extraction and recall stats for this session",
56830
+ args: [],
56831
+ handler: async (commandCtx = {}) => {
56832
+ const { sessionKey } = resolveSession(commandCtx);
56833
+ const lastRecall = runtime.getLastRecall(sessionKey);
56834
+ if (!lastRecall) {
56835
+ return `No Remnic recall stats are available for session ${sessionKey} yet.`;
56836
+ }
56837
+ return [
56838
+ `Session ${sessionKey}.`,
56839
+ `Planner mode: ${lastRecall.plannerMode ?? "unknown"}.`,
56840
+ `Latency: ${lastRecall.latencyMs ?? "?"}ms.`,
56841
+ `Memories: ${lastRecall.memoryIds.length}.`
56842
+ ].join(" ");
56843
+ }
56844
+ },
56845
+ {
56846
+ name: "flush",
56847
+ description: "Force-flush the extraction buffer now",
56848
+ args: [],
56849
+ handler: async (commandCtx = {}) => {
56850
+ const { sessionKey } = resolveSession(commandCtx);
56851
+ await runtime.flushSession(sessionKey);
56852
+ return `Flushed the Remnic buffer for session ${sessionKey}.`;
56853
+ }
56854
+ }
56855
+ ];
56856
+ const subcommandNames = subcommands.map((entry) => entry.name).join(", ");
55594
56857
  return [
55595
56858
  {
55596
56859
  name: "remnic",
56860
+ description: `Remnic memory controls (${subcommandNames})`,
55597
56861
  category: "memory",
55598
56862
  pluginId,
55599
- subcommands: [
55600
- {
55601
- name: "off",
55602
- description: "Disable Remnic recall for this session",
55603
- args: [],
55604
- handler: async (commandCtx = {}) => {
55605
- const { sessionKey, agentId } = resolveSession(commandCtx);
55606
- await runtime.toggles.setDisabled(sessionKey, agentId, true);
55607
- return `Remnic recall disabled for session ${sessionKey}.`;
55608
- }
55609
- },
55610
- {
55611
- name: "on",
55612
- description: "Re-enable Remnic recall for this session",
55613
- args: [],
55614
- handler: async (commandCtx = {}) => {
55615
- const { sessionKey, agentId } = resolveSession(commandCtx);
55616
- await runtime.toggles.setDisabled(sessionKey, agentId, false);
55617
- return `Remnic recall re-enabled for session ${sessionKey}.`;
55618
- }
55619
- },
55620
- {
55621
- name: "status",
55622
- description: "Show Remnic recall status and last injected summary",
55623
- args: [],
55624
- handler: async (commandCtx = {}) => {
55625
- const { sessionKey, agentId } = resolveSession(commandCtx);
55626
- const resolved = await runtime.toggles.resolve(sessionKey, agentId);
55627
- const lastRecall = runtime.getLastRecall(sessionKey);
55628
- const summaryText = runtime.getLastRecallSummary(sessionKey);
55629
- const summary = summaryText && summaryText.length > 0 ? summaryText : lastRecall && lastRecall.memoryIds.length > 0 ? `${lastRecall.memoryIds.length} memory item(s), latency ${lastRecall.latencyMs ?? "?"}ms` : "NONE";
55630
- return [
55631
- `Remnic recall is ${resolved.disabled ? "disabled" : "enabled"} for session ${sessionKey}.`,
55632
- `Source: ${describeToggleSource(resolved.source)}.`,
55633
- `Last recall: ${summary}.`
55634
- ].join(" ");
55635
- }
55636
- },
55637
- {
55638
- name: "clear",
55639
- description: "Clear the session override and use global config again",
55640
- args: [],
55641
- handler: async (commandCtx = {}) => {
55642
- const { sessionKey, agentId } = resolveSession(commandCtx);
55643
- await runtime.toggles.clear(sessionKey, agentId);
55644
- return `Cleared the Remnic session override for ${sessionKey}.`;
55645
- }
55646
- },
55647
- {
55648
- name: "stats",
55649
- description: "Show Remnic extraction and recall stats for this session",
55650
- args: [],
55651
- handler: async (commandCtx = {}) => {
55652
- const { sessionKey } = resolveSession(commandCtx);
55653
- const lastRecall = runtime.getLastRecall(sessionKey);
55654
- if (!lastRecall) {
55655
- return `No Remnic recall stats are available for session ${sessionKey} yet.`;
55656
- }
55657
- return [
55658
- `Session ${sessionKey}.`,
55659
- `Planner mode: ${lastRecall.plannerMode ?? "unknown"}.`,
55660
- `Latency: ${lastRecall.latencyMs ?? "?"}ms.`,
55661
- `Memories: ${lastRecall.memoryIds.length}.`
55662
- ].join(" ");
55663
- }
55664
- },
55665
- {
55666
- name: "flush",
55667
- description: "Force-flush the extraction buffer now",
55668
- args: [],
55669
- handler: async (commandCtx = {}) => {
55670
- const { sessionKey } = resolveSession(commandCtx);
55671
- await runtime.flushSession(sessionKey);
55672
- return `Flushed the Remnic buffer for session ${sessionKey}.`;
55673
- }
56863
+ acceptsArgs: true,
56864
+ subcommands,
56865
+ handler: async (commandCtx = {}) => {
56866
+ const requested = commandCtx.args?.[0]?.trim().toLowerCase() ?? "status";
56867
+ const match = subcommands.find((entry) => entry.name === requested);
56868
+ if (!match) {
56869
+ return `Unknown Remnic subcommand "${requested}". Try one of: ${subcommandNames}.`;
55674
56870
  }
55675
- ]
56871
+ return match.handler(commandCtx);
56872
+ }
55676
56873
  }
55677
56874
  ];
55678
56875
  }
@@ -55772,8 +56969,8 @@ function validateSlotSelection(ctx) {
55772
56969
  }
55773
56970
 
55774
56971
  // ../remnic-core/src/session-toggles.ts
55775
- import { mkdir as mkdir51, readFile as readFile48, writeFile as writeFile44 } from "fs/promises";
55776
- import path92 from "path";
56972
+ import { mkdir as mkdir51, readFile as readFile49, writeFile as writeFile44 } from "fs/promises";
56973
+ import path93 from "path";
55777
56974
  function encodeToggleKey(sessionKey, agentId) {
55778
56975
  return `${encodeURIComponent(sessionKey)}::${encodeURIComponent(agentId)}`;
55779
56976
  }
@@ -55787,7 +56984,7 @@ function decodeToggleKey(key) {
55787
56984
  }
55788
56985
  async function safeReadToggleFile(filePath) {
55789
56986
  try {
55790
- const raw = await readFile48(filePath, "utf8");
56987
+ const raw = await readFile49(filePath, "utf8");
55791
56988
  const parsed = JSON.parse(raw);
55792
56989
  if (!parsed || typeof parsed !== "object" || typeof parsed.entries !== "object") {
55793
56990
  return { version: 1, entries: {} };
@@ -55812,7 +57009,7 @@ function createFileToggleStore(filePath, options = {}) {
55812
57009
  await run;
55813
57010
  }
55814
57011
  async function writeToggleFile(next) {
55815
- await mkdir51(path92.dirname(filePath), { recursive: true });
57012
+ await mkdir51(path93.dirname(filePath), { recursive: true });
55816
57013
  await writeFile44(filePath, JSON.stringify(next, null, 2), "utf8");
55817
57014
  }
55818
57015
  async function readPrimary() {
@@ -55885,8 +57082,8 @@ function createFileToggleStore(filePath, options = {}) {
55885
57082
  }
55886
57083
 
55887
57084
  // ../remnic-core/src/recall-audit.ts
55888
- import { appendFile as appendFile5, mkdir as mkdir52, readdir as readdir27, rm as rm10 } from "fs/promises";
55889
- import path93 from "path";
57085
+ import { appendFile as appendFile5, mkdir as mkdir52, readdir as readdir28, rm as rm10 } from "fs/promises";
57086
+ import path94 from "path";
55890
57087
  function formatIsoDate(ts) {
55891
57088
  const normalized = new Date(ts);
55892
57089
  if (Number.isNaN(normalized.getTime())) {
@@ -55896,24 +57093,24 @@ function formatIsoDate(ts) {
55896
57093
  }
55897
57094
  function buildRecallAuditPath(rootDir, ts, sessionKey) {
55898
57095
  const safeSessionKey = encodeURIComponent(sessionKey);
55899
- return path93.join(rootDir, "transcripts", formatIsoDate(ts), `${safeSessionKey}.jsonl`);
57096
+ return path94.join(rootDir, "transcripts", formatIsoDate(ts), `${safeSessionKey}.jsonl`);
55900
57097
  }
55901
57098
  async function appendRecallAuditEntry(rootDir, entry) {
55902
57099
  const filePath = buildRecallAuditPath(rootDir, entry.ts, entry.sessionKey);
55903
- await mkdir52(path93.dirname(filePath), { recursive: true });
57100
+ await mkdir52(path94.dirname(filePath), { recursive: true });
55904
57101
  await appendFile5(filePath, `${JSON.stringify(entry)}
55905
57102
  `, "utf8");
55906
57103
  return filePath;
55907
57104
  }
55908
57105
  async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE__ */ new Date()) {
55909
- const transcriptsDir = path93.join(rootDir, "transcripts");
57106
+ const transcriptsDir = path94.join(rootDir, "transcripts");
55910
57107
  const removed = [];
55911
57108
  const cutoff = new Date(now);
55912
57109
  cutoff.setUTCHours(0, 0, 0, 0);
55913
57110
  cutoff.setUTCDate(cutoff.getUTCDate() - Math.max(1, Math.floor(retentionDays)));
55914
57111
  let entries;
55915
57112
  try {
55916
- entries = await readdir27(transcriptsDir, { withFileTypes: true });
57113
+ entries = await readdir28(transcriptsDir, { withFileTypes: true });
55917
57114
  } catch {
55918
57115
  return removed;
55919
57116
  }
@@ -55922,7 +57119,7 @@ async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE_
55922
57119
  if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.name)) continue;
55923
57120
  const day = /* @__PURE__ */ new Date(`${entry.name}T00:00:00.000Z`);
55924
57121
  if (Number.isNaN(day.getTime()) || day >= cutoff) continue;
55925
- const dirPath = path93.join(transcriptsDir, entry.name);
57122
+ const dirPath = path94.join(transcriptsDir, entry.name);
55926
57123
  await rm10(dirPath, { recursive: true, force: true });
55927
57124
  removed.push(dirPath);
55928
57125
  }
@@ -55931,7 +57128,7 @@ async function pruneRecallAuditEntries(rootDir, retentionDays, now = /* @__PURE_
55931
57128
 
55932
57129
  // ../remnic-core/src/active-recall.ts
55933
57130
  import { appendFile as appendFile6, mkdir as mkdir53 } from "fs/promises";
55934
- import path94 from "path";
57131
+ import path95 from "path";
55935
57132
  var ACTIVE_RECALL_CACHE_MAX_ENTRIES = 256;
55936
57133
  var NONE_SET = /* @__PURE__ */ new Set([
55937
57134
  "",
@@ -56074,14 +57271,14 @@ ${params.recallExplain}` : null,
56074
57271
  }
56075
57272
  async function appendActiveRecallTranscript(transcriptRoot, input, config, result, queryBundle) {
56076
57273
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
56077
- const filePath = path94.join(
57274
+ const filePath = path95.join(
56078
57275
  transcriptRoot,
56079
57276
  "agents",
56080
57277
  sanitizeTranscriptPathSegment(input.agentId),
56081
57278
  date,
56082
57279
  `${sanitizeTranscriptPathSegment(input.sessionKey)}.jsonl`
56083
57280
  );
56084
- await mkdir53(path94.dirname(filePath), { recursive: true });
57281
+ await mkdir53(path95.dirname(filePath), { recursive: true });
56085
57282
  await appendFile6(
56086
57283
  filePath,
56087
57284
  `${JSON.stringify({
@@ -56313,8 +57510,8 @@ import { resolvePrincipal as resolvePrincipal2 } from "@remnic/core";
56313
57510
  // ../remnic-core/src/surfaces/dreams.ts
56314
57511
  import { createHash as createHash16 } from "crypto";
56315
57512
  import { statSync, watch as watch2 } from "fs";
56316
- import { mkdir as mkdir54, readFile as readFile49, writeFile as writeFile45 } from "fs/promises";
56317
- import path95 from "path";
57513
+ import { mkdir as mkdir54, readFile as readFile50, writeFile as writeFile45 } from "fs/promises";
57514
+ import path96 from "path";
56318
57515
  var DIARY_START_MARKER = "<!-- openclaw:dreaming:diary:start -->";
56319
57516
  var DIARY_END_MARKER = "<!-- openclaw:dreaming:diary:end -->";
56320
57517
  function stableDreamId(params) {
@@ -56479,7 +57676,7 @@ function createDreamsSurface() {
56479
57676
  return {
56480
57677
  async read(filePath) {
56481
57678
  try {
56482
- const content = await readFile49(filePath, "utf8");
57679
+ const content = await readFile50(filePath, "utf8");
56483
57680
  return parseDreamEntries(content);
56484
57681
  } catch (error) {
56485
57682
  if (error.code === "ENOENT") {
@@ -56489,10 +57686,10 @@ function createDreamsSurface() {
56489
57686
  }
56490
57687
  },
56491
57688
  async append(filePath, entry) {
56492
- await mkdir54(path95.dirname(filePath), { recursive: true });
57689
+ await mkdir54(path96.dirname(filePath), { recursive: true });
56493
57690
  let content = "";
56494
57691
  try {
56495
- content = await readFile49(filePath, "utf8");
57692
+ content = await readFile50(filePath, "utf8");
56496
57693
  } catch (error) {
56497
57694
  if (error.code !== "ENOENT") throw error;
56498
57695
  }
@@ -56510,22 +57707,22 @@ ${ensured.slice(endIndex)}` : `${ensureDiary("")}${block}`;
56510
57707
  let fileWatcher = null;
56511
57708
  let parentWatcher = null;
56512
57709
  let timer = null;
56513
- const watchedName = path95.basename(filePath);
56514
- const watchedDir = path95.dirname(filePath);
57710
+ const watchedName = path96.basename(filePath);
57711
+ const watchedDir = path96.dirname(filePath);
56515
57712
  const resolveParentWatchTarget = () => {
56516
57713
  let candidateDir = watchedDir;
56517
57714
  while (true) {
56518
57715
  try {
56519
57716
  if (statSync(candidateDir).isDirectory()) {
56520
- const relative = path95.relative(candidateDir, watchedDir);
57717
+ const relative = path96.relative(candidateDir, watchedDir);
56521
57718
  return {
56522
57719
  dir: candidateDir,
56523
- expectedName: relative.length === 0 ? watchedName : relative.split(path95.sep)[0] ?? watchedName
57720
+ expectedName: relative.length === 0 ? watchedName : relative.split(path96.sep)[0] ?? watchedName
56524
57721
  };
56525
57722
  }
56526
57723
  } catch {
56527
57724
  }
56528
- const parentDir = path95.dirname(candidateDir);
57725
+ const parentDir = path96.dirname(candidateDir);
56529
57726
  if (parentDir === candidateDir) {
56530
57727
  return null;
56531
57728
  }
@@ -56590,8 +57787,8 @@ ${ensured.slice(endIndex)}` : `${ensureDiary("")}${block}`;
56590
57787
  // ../remnic-core/src/surfaces/heartbeat.ts
56591
57788
  import { createHash as createHash17 } from "crypto";
56592
57789
  import { statSync as statSync2, watch as watch3 } from "fs";
56593
- import { readFile as readFile50 } from "fs/promises";
56594
- import path96 from "path";
57790
+ import { readFile as readFile51 } from "fs/promises";
57791
+ import path97 from "path";
56595
57792
  function stableHeartbeatId(params) {
56596
57793
  const digest = createHash17("sha1").update(
56597
57794
  JSON.stringify({
@@ -56754,7 +57951,7 @@ function createHeartbeatSurface() {
56754
57951
  return {
56755
57952
  async read(filePath) {
56756
57953
  try {
56757
- const content = await readFile50(filePath, "utf8");
57954
+ const content = await readFile51(filePath, "utf8");
56758
57955
  return parseHeartbeatEntries(content);
56759
57956
  } catch (error) {
56760
57957
  if (error.code === "ENOENT") {
@@ -56767,22 +57964,22 @@ function createHeartbeatSurface() {
56767
57964
  let fileWatcher = null;
56768
57965
  let parentWatcher = null;
56769
57966
  let timer = null;
56770
- const watchedName = path96.basename(filePath);
56771
- const watchedDir = path96.dirname(filePath);
57967
+ const watchedName = path97.basename(filePath);
57968
+ const watchedDir = path97.dirname(filePath);
56772
57969
  const resolveParentWatchTarget = () => {
56773
57970
  let candidateDir = watchedDir;
56774
57971
  while (true) {
56775
57972
  try {
56776
57973
  if (statSync2(candidateDir).isDirectory()) {
56777
- const relative = path96.relative(candidateDir, watchedDir);
57974
+ const relative = path97.relative(candidateDir, watchedDir);
56778
57975
  return {
56779
57976
  dir: candidateDir,
56780
- expectedName: relative.length === 0 ? watchedName : relative.split(path96.sep)[0] ?? watchedName
57977
+ expectedName: relative.length === 0 ? watchedName : relative.split(path97.sep)[0] ?? watchedName
56781
57978
  };
56782
57979
  }
56783
57980
  } catch {
56784
57981
  }
56785
- const parentDir = path96.dirname(candidateDir);
57982
+ const parentDir = path97.dirname(candidateDir);
56786
57983
  if (parentDir === candidateDir) {
56787
57984
  return null;
56788
57985
  }
@@ -56868,7 +58065,7 @@ function loadPluginEntryFromFile(pluginId) {
56868
58065
  try {
56869
58066
  const explicitConfigPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH");
56870
58067
  const homeDir = resolveHomeDir();
56871
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path97.join(homeDir, ".openclaw", "openclaw.json");
58068
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path98.join(homeDir, ".openclaw", "openclaw.json");
56872
58069
  const content = readFileSync6(configPath, "utf-8");
56873
58070
  const config = JSON.parse(content);
56874
58071
  return resolveRemnicPluginEntry(config, pluginId);
@@ -56884,7 +58081,7 @@ function loadRawConfigFromFile() {
56884
58081
  try {
56885
58082
  const explicitConfigPath = readEnvVar("OPENCLAW_ENGRAM_CONFIG_PATH") || readEnvVar("OPENCLAW_CONFIG_PATH");
56886
58083
  const homeDir = resolveHomeDir();
56887
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path97.join(homeDir, ".openclaw", "openclaw.json");
58084
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path98.join(homeDir, ".openclaw", "openclaw.json");
56888
58085
  const content = readFileSync6(configPath, "utf-8");
56889
58086
  const config = JSON.parse(content);
56890
58087
  return config && typeof config === "object" ? config : void 0;
@@ -57118,15 +58315,15 @@ var pluginDefinition = {
57118
58315
  citationsAutoDetect: cfg.citationsAutoDetect
57119
58316
  });
57120
58317
  globalThis[keys.ACCESS_HTTP_SERVER] = accessHttpServer;
57121
- const pluginStateDir = path97.join(cfg.memoryDir, "state", "plugins", serviceId);
57122
- const togglePrimaryPath = path97.join(pluginStateDir, "session-toggles.json");
57123
- const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path97.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
58318
+ const pluginStateDir = path98.join(cfg.memoryDir, "state", "plugins", serviceId);
58319
+ const togglePrimaryPath = path98.join(pluginStateDir, "session-toggles.json");
58320
+ const toggleSecondaryPath = cfg.respectBundledActiveMemoryToggle ? path98.join(cfg.memoryDir, "state", "plugins", "active-memory", "session-toggles.json") : void 0;
57124
58321
  const sessionToggleStore = createFileToggleStore(togglePrimaryPath, {
57125
58322
  secondaryReadOnlyPath: toggleSecondaryPath
57126
58323
  });
57127
58324
  const dreamsSurface = createDreamsSurface();
57128
58325
  const heartbeatSurface = createHeartbeatSurface();
57129
- const dreamNarrativeClient = cfg.openaiApiKey ? new OpenAI({
58326
+ const dreamNarrativeClient = cfg.openaiApiKey ? new OpenAI2({
57130
58327
  apiKey: cfg.openaiApiKey,
57131
58328
  ...cfg.openaiBaseUrl ? { baseURL: cfg.openaiBaseUrl } : {}
57132
58329
  }) : null;
@@ -57142,11 +58339,11 @@ var pluginDefinition = {
57142
58339
  }
57143
58340
  function resolveDreamJournalPath(runtimeWorkspaceDir) {
57144
58341
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
57145
- return path97.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path97.join(workspaceRoot, cfg.dreaming.journalPath);
58342
+ return path98.isAbsolute(cfg.dreaming.journalPath) ? cfg.dreaming.journalPath : path98.join(workspaceRoot, cfg.dreaming.journalPath);
57146
58343
  }
57147
58344
  function resolveHeartbeatJournalPath(runtimeWorkspaceDir) {
57148
58345
  const workspaceRoot = resolveWorkspaceRoot(runtimeWorkspaceDir);
57149
- return path97.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path97.join(workspaceRoot, cfg.heartbeat.journalPath);
58346
+ return path98.isAbsolute(cfg.heartbeat.journalPath) ? cfg.heartbeat.journalPath : path98.join(workspaceRoot, cfg.heartbeat.journalPath);
57150
58347
  }
57151
58348
  function queueDreamSurfaceSync(runtimeWorkspaceDir) {
57152
58349
  if (!cfg.dreaming.enabled) return Promise.resolve();
@@ -57304,7 +58501,7 @@ Keep the reflection grounded in the evidence below.
57304
58501
  timeoutMs: cfg.activeRecallTimeoutMs,
57305
58502
  cacheTtlMs: cfg.activeRecallCacheTtlMs,
57306
58503
  persistTranscripts: cfg.activeRecallPersistTranscripts,
57307
- transcriptDir: path97.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path97.join(pluginStateDir, cfg.activeRecallTranscriptDir),
58504
+ transcriptDir: path98.isAbsolute(cfg.activeRecallTranscriptDir) ? cfg.activeRecallTranscriptDir : path98.join(pluginStateDir, cfg.activeRecallTranscriptDir),
57308
58505
  entityGraphDepth: cfg.activeRecallEntityGraphDepth,
57309
58506
  includeCausalTrajectories: cfg.activeRecallIncludeCausalTrajectories,
57310
58507
  includeDaySummary: cfg.activeRecallIncludeDaySummary,
@@ -58172,6 +59369,219 @@ Keep the reflection grounded in the evidence below.
58172
59369
  const runtimeAgentId = typeof runtimeAgent?.id === "string" && runtimeAgent.id.length > 0 ? runtimeAgent.id : void 0;
58173
59370
  const capabilityAgentIds = runtimeAgentId ? [runtimeAgentId] : ["generalist"];
58174
59371
  const capabilityWorkspaceDir = (typeof runtimeAgent?.workspaceDir === "string" && runtimeAgent.workspaceDir.length > 0 ? runtimeAgent.workspaceDir : void 0) ?? orchestrator.config.workspaceDir ?? defaultWorkspaceDir();
59372
+ const remnicUsesQmd = (orchestrator.config.searchBackend ?? "qmd") === "qmd" && orchestrator.config.qmdEnabled !== false;
59373
+ const remnicQmdCommand = typeof orchestrator.config.qmdPath === "string" && orchestrator.config.qmdPath.trim().length > 0 ? orchestrator.config.qmdPath.trim() : "qmd";
59374
+ const readAllowedRoots = [
59375
+ orchestrator.config.memoryDir,
59376
+ capabilityWorkspaceDir ? path98.join(capabilityWorkspaceDir, "memory") : void 0
59377
+ ].filter((root) => typeof root === "string" && root.length > 0);
59378
+ const canonicalizeRootForContainment = async (rawPath) => {
59379
+ const resolved = path98.resolve(rawPath);
59380
+ try {
59381
+ return path98.normalize(await realpath5(resolved));
59382
+ } catch {
59383
+ return path98.normalize(resolved);
59384
+ }
59385
+ };
59386
+ const canonicalizeForRead = async (rawPath) => {
59387
+ const resolved = path98.resolve(rawPath);
59388
+ const real = await realpath5(resolved);
59389
+ return path98.normalize(real);
59390
+ };
59391
+ const readAllowedCanonicalRootsPromise = Promise.all(
59392
+ readAllowedRoots.map((root) => canonicalizeRootForContainment(root))
59393
+ );
59394
+ const isWithinAllowedRoot = async (candidatePath) => {
59395
+ let canonicalCandidatePath;
59396
+ try {
59397
+ canonicalCandidatePath = await canonicalizeForRead(candidatePath);
59398
+ } catch {
59399
+ return false;
59400
+ }
59401
+ const canonicalRoots = await readAllowedCanonicalRootsPromise;
59402
+ return canonicalRoots.some((root) => {
59403
+ const relative = path98.relative(root, canonicalCandidatePath);
59404
+ return relative === "" || !relative.startsWith("..") && !path98.isAbsolute(relative);
59405
+ });
59406
+ };
59407
+ const normalizeWorkspacePath = (rawPath) => {
59408
+ if (!rawPath || typeof rawPath !== "string") return "memory";
59409
+ const resolved = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : path98.resolve(capabilityWorkspaceDir, rawPath);
59410
+ const relative = path98.relative(capabilityWorkspaceDir, resolved);
59411
+ return relative && !relative.startsWith("..") && !path98.isAbsolute(relative) ? relative : rawPath;
59412
+ };
59413
+ const relativizeToMemoryRoot = (rawPath) => {
59414
+ if (!rawPath || typeof rawPath !== "string") return "memory";
59415
+ const resolved = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : path98.resolve(capabilityWorkspaceDir, rawPath);
59416
+ for (const root of readAllowedRoots) {
59417
+ const relative = path98.relative(root, resolved);
59418
+ if (relative !== "" && !relative.startsWith("..") && !path98.isAbsolute(relative)) {
59419
+ return relative;
59420
+ }
59421
+ }
59422
+ return normalizeWorkspacePath(rawPath);
59423
+ };
59424
+ const resolveReadablePath = async (requestedPath) => {
59425
+ const candidateAbsolutePaths = path98.isAbsolute(requestedPath) ? [path98.resolve(requestedPath)] : readAllowedRoots.map((root) => path98.resolve(root, requestedPath));
59426
+ let canonicalPath;
59427
+ let lastError;
59428
+ for (const absolutePath of candidateAbsolutePaths) {
59429
+ try {
59430
+ canonicalPath = await canonicalizeForRead(absolutePath);
59431
+ break;
59432
+ } catch (err) {
59433
+ lastError = err;
59434
+ }
59435
+ }
59436
+ if (canonicalPath === void 0) {
59437
+ throw new Error(
59438
+ `memory read rejected (path unresolvable): ${requestedPath}`
59439
+ );
59440
+ }
59441
+ const canonicalRoots = await readAllowedCanonicalRootsPromise;
59442
+ const contained = canonicalRoots.some((root) => {
59443
+ const relative = path98.relative(root, canonicalPath);
59444
+ return relative === "" || !relative.startsWith("..") && !path98.isAbsolute(relative);
59445
+ });
59446
+ if (!contained) {
59447
+ throw new Error(`memory read outside allowed roots: ${requestedPath}`);
59448
+ }
59449
+ if (!canonicalPath.toLowerCase().endsWith(".md")) {
59450
+ throw new Error(
59451
+ `memory read restricted to .md files: ${requestedPath}`
59452
+ );
59453
+ }
59454
+ return canonicalPath;
59455
+ };
59456
+ const remnicMemoryRuntime = {
59457
+ async getMemorySearchManager(_params) {
59458
+ return {
59459
+ manager: {
59460
+ async search(query, opts) {
59461
+ const namespace = typeof orchestrator.resolveSelfNamespace === "function" ? orchestrator.resolveSelfNamespace(opts?.sessionKey) : void 0;
59462
+ const resolvedMode = opts?.qmdSearchModeOverride === "vsearch" ? "vector" : opts?.qmdSearchModeOverride === "query" ? "search" : opts?.qmdSearchModeOverride ?? "search";
59463
+ const rawResults = await orchestrator.searchAcrossNamespaces({
59464
+ query,
59465
+ maxResults: opts?.maxResults,
59466
+ namespaces: namespace ? [namespace] : void 0,
59467
+ mode: resolvedMode
59468
+ });
59469
+ const isArtifactPath2 = (p) => /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(p);
59470
+ return rawResults.filter((result) => {
59471
+ const candidate = result;
59472
+ const p = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : "";
59473
+ return !isArtifactPath2(p);
59474
+ }).map((result, index) => {
59475
+ const candidate = result;
59476
+ const rawPath = typeof candidate.path === "string" ? candidate.path : typeof candidate.id === "string" ? candidate.id : `memory-${index + 1}`;
59477
+ const absolutePath = path98.isAbsolute(rawPath) ? path98.resolve(rawPath) : (() => {
59478
+ for (const root of readAllowedRoots) {
59479
+ const candidateAbs = path98.resolve(root, rawPath);
59480
+ const relative = path98.relative(root, candidateAbs);
59481
+ if (!relative.startsWith("..") && !path98.isAbsolute(relative)) {
59482
+ return candidateAbs;
59483
+ }
59484
+ }
59485
+ return path98.resolve(capabilityWorkspaceDir, rawPath);
59486
+ })();
59487
+ const normalizedPath = relativizeToMemoryRoot(rawPath);
59488
+ const startLine = typeof candidate.startLine === "number" && Number.isFinite(candidate.startLine) ? Math.max(1, Math.floor(candidate.startLine)) : 1;
59489
+ const endLine = typeof candidate.endLine === "number" && Number.isFinite(candidate.endLine) ? Math.max(startLine, Math.floor(candidate.endLine)) : startLine;
59490
+ return {
59491
+ path: absolutePath,
59492
+ startLine,
59493
+ endLine,
59494
+ score: typeof candidate.score === "number" && Number.isFinite(candidate.score) ? candidate.score : 0,
59495
+ snippet: typeof candidate.snippet === "string" ? candidate.snippet : typeof candidate.text === "string" ? candidate.text : "",
59496
+ source: normalizedPath.includes("sessions/") ? "sessions" : "memory",
59497
+ citation: normalizedPath
59498
+ };
59499
+ }).filter(
59500
+ (result) => typeof opts?.minScore === "number" && Number.isFinite(opts.minScore) ? result.score >= opts.minScore : true
59501
+ );
59502
+ },
59503
+ async readFile(params) {
59504
+ const requestedPath = normalizeWorkspacePath(params.relPath);
59505
+ const absolutePath = await resolveReadablePath(params.relPath);
59506
+ const text = await readFile52(absolutePath, "utf-8");
59507
+ const allLines = text.split(/\r?\n/);
59508
+ const from = typeof params.from === "number" ? Math.max(1, Math.floor(params.from)) : 1;
59509
+ const lines = typeof params.lines === "number" && Number.isFinite(params.lines) ? Math.max(1, Math.floor(params.lines)) : void 0;
59510
+ const startIndex = from - 1;
59511
+ const endIndex = typeof lines === "number" ? startIndex + lines : allLines.length;
59512
+ const slice = allLines.slice(startIndex, endIndex);
59513
+ const truncated = endIndex < allLines.length;
59514
+ return {
59515
+ text: slice.join("\n"),
59516
+ path: requestedPath,
59517
+ truncated: truncated || void 0,
59518
+ from,
59519
+ lines,
59520
+ nextFrom: truncated ? endIndex + 1 : void 0
59521
+ };
59522
+ },
59523
+ status() {
59524
+ const qmdAvailable = remnicUsesQmd && typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : !remnicUsesQmd;
59525
+ const qmdDebug = typeof orchestrator.qmd?.debugStatus === "function" ? orchestrator.qmd.debugStatus() : void 0;
59526
+ return {
59527
+ backend: remnicUsesQmd ? "qmd" : "builtin",
59528
+ provider: remnicUsesQmd ? "qmd" : "builtin",
59529
+ requestedProvider: remnicUsesQmd ? "qmd" : "builtin",
59530
+ model: remnicUsesQmd ? remnicQmdCommand : "builtin",
59531
+ dirty: false,
59532
+ workspaceDir: capabilityWorkspaceDir,
59533
+ dbPath: orchestrator.config.memoryDir,
59534
+ sources: ["memory"],
59535
+ sourceCounts: [],
59536
+ vector: remnicUsesQmd ? {
59537
+ enabled: true,
59538
+ available: qmdAvailable
59539
+ } : {
59540
+ enabled: false
59541
+ },
59542
+ fts: {
59543
+ enabled: true,
59544
+ available: qmdAvailable
59545
+ },
59546
+ custom: {
59547
+ remnic: {
59548
+ qmdAvailable,
59549
+ qmdDebug,
59550
+ memoryDir: orchestrator.config.memoryDir
59551
+ }
59552
+ }
59553
+ };
59554
+ },
59555
+ async sync(_params2) {
59556
+ if (remnicUsesQmd && typeof orchestrator.qmd?.update === "function") {
59557
+ await orchestrator.qmd.update();
59558
+ }
59559
+ },
59560
+ async probeEmbeddingAvailability() {
59561
+ if (!remnicUsesQmd) return { ok: true };
59562
+ const qmdAvailable = typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : false;
59563
+ if (qmdAvailable) return { ok: true };
59564
+ const qmdDebug = typeof orchestrator.qmd?.debugStatus === "function" ? orchestrator.qmd.debugStatus() : void 0;
59565
+ return {
59566
+ ok: false,
59567
+ error: qmdDebug ?? "Remnic QMD backend unavailable"
59568
+ };
59569
+ },
59570
+ async probeVectorAvailability() {
59571
+ if (!remnicUsesQmd) return false;
59572
+ return typeof orchestrator.qmd?.isAvailable === "function" ? Boolean(orchestrator.qmd.isAvailable()) : false;
59573
+ },
59574
+ async close() {
59575
+ }
59576
+ }
59577
+ };
59578
+ },
59579
+ resolveMemoryBackendConfig(_params) {
59580
+ return remnicUsesQmd ? { backend: "qmd", qmd: { command: remnicQmdCommand } } : { backend: "builtin" };
59581
+ },
59582
+ async closeAllMemorySearchManagers() {
59583
+ }
59584
+ };
58175
59585
  const memoryCapability = {
58176
59586
  // Include the promptBuilder so runtimes that treat unified capability
58177
59587
  // registration as authoritative (SDK >=2026.4.5) continue to inject
@@ -58179,6 +59589,7 @@ Keep the reflection grounded in the evidence below.
58179
59589
  // Respect promptInjectionAllowed policy — omit promptBuilder if injection
58180
59590
  // is disabled, so the capability only provides publicArtifacts.
58181
59591
  ...promptInjectionAllowed ? { promptBuilder: capabilityPromptBuilder } : {},
59592
+ runtime: remnicMemoryRuntime,
58182
59593
  publicArtifacts: {
58183
59594
  listArtifacts: async (_params) => {
58184
59595
  try {
@@ -58196,7 +59607,7 @@ Keep the reflection grounded in the evidence below.
58196
59607
  };
58197
59608
  api.registerMemoryCapability(memoryCapability);
58198
59609
  const builderDesc = !promptInjectionAllowed ? " (promptBuilder omitted \u2014 injection disabled by policy)" : memoryPromptBuilder ? " and promptBuilder (from registerMemoryPromptSection)" : " and promptBuilder (capability-only fallback)";
58199
- log.info(`registered memory capability with publicArtifacts provider${builderDesc}`);
59610
+ log.info(`registered memory capability with runtime and publicArtifacts provider${builderDesc}`);
58200
59611
  }
58201
59612
  api.on(
58202
59613
  "agent_end",
@@ -58493,7 +59904,7 @@ Keep the reflection grounded in the evidence below.
58493
59904
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
58494
59905
  );
58495
59906
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
58496
- const signalPath = path97.join(
59907
+ const signalPath = path98.join(
58497
59908
  workspaceDir,
58498
59909
  `.compaction-reset-signal-${safeSessionKey}`
58499
59910
  );
@@ -58745,7 +60156,7 @@ Keep the reflection grounded in the evidence below.
58745
60156
  }
58746
60157
  async function ensureHourlySummaryCron(api2) {
58747
60158
  const jobId = "engram-hourly-summary";
58748
- const cronFilePath = path97.join(
60159
+ const cronFilePath = path98.join(
58749
60160
  os6.homedir(),
58750
60161
  ".openclaw",
58751
60162
  "cron",
@@ -58757,7 +60168,7 @@ Keep the reflection grounded in the evidence below.
58757
60168
  jobs: []
58758
60169
  };
58759
60170
  try {
58760
- const content = await readFile51(cronFilePath, "utf-8");
60171
+ const content = await readFile52(cronFilePath, "utf-8");
58761
60172
  jobsData = JSON.parse(content);
58762
60173
  } catch {
58763
60174
  }
@@ -58772,7 +60183,7 @@ Keep the reflection grounded in the evidence below.
58772
60183
  id: jobId,
58773
60184
  agentId: "generalist",
58774
60185
  model,
58775
- name: "Engram Hourly Summary",
60186
+ name: "Remnic Hourly Summary",
58776
60187
  enabled: true,
58777
60188
  createdAtMs: Date.now(),
58778
60189
  updatedAtMs: Date.now(),
@@ -58788,7 +60199,7 @@ Keep the reflection grounded in the evidence below.
58788
60199
  kind: "agentTurn",
58789
60200
  timeoutSeconds: 120,
58790
60201
  thinking: "off",
58791
- 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"
60202
+ 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"
58792
60203
  },
58793
60204
  delivery: { mode: "none" },
58794
60205
  state: {}
@@ -58958,39 +60369,33 @@ Keep the reflection grounded in the evidence below.
58958
60369
  stop: async () => {
58959
60370
  if (!didCountStart) return;
58960
60371
  didCountStart = false;
60372
+ const initPromiseAtStopEntry = globalThis[keys.INIT_PROMISE];
60373
+ try {
60374
+ await orchestrator.deferredReady;
60375
+ } catch {
60376
+ }
58961
60377
  const remainingServices = Math.max(
58962
60378
  0,
58963
60379
  (globalThis[CLI_ACTIVE_SERVICE_COUNT] || 0) - 1
58964
60380
  );
58965
60381
  globalThis[CLI_ACTIVE_SERVICE_COUNT] = remainingServices;
58966
- try {
58967
- activeOpikExporter?.unsubscribe();
58968
- } catch (err) {
58969
- log.debug(`engram opik exporter unsubscribe failed: ${err}`);
58970
- }
58971
- activeOpikExporter = null;
58972
- try {
58973
- await accessHttpServer.stop();
58974
- } catch (err) {
58975
- log.debug(`engram access HTTP stop failed: ${err}`);
58976
- }
58977
- stopDreamWatcher?.();
58978
- stopDreamWatcher = null;
58979
- stopHeartbeatWatcher?.();
58980
- stopHeartbeatWatcher = null;
58981
- removeDreamingObserver?.();
58982
- removeDreamingObserver = null;
58983
- delete globalThis[keys.ACCESS_HTTP_SERVER];
58984
- delete globalThis[keys.ACCESS_SERVICE];
58985
- const currentInitPromise = globalThis[keys.INIT_PROMISE];
60382
+ const currentInitPromise = initPromiseAtStopEntry;
58986
60383
  let secondaryTookOver = false;
60384
+ if (!currentInitPromise && globalThis[keys.INIT_PROMISE]) {
60385
+ secondaryTookOver = true;
60386
+ }
58987
60387
  if (!currentInitPromise) {
58988
- globalThis[keys.REGISTERED_GUARD] = false;
58989
- if (remainingServices === 0) {
58990
- globalThis[CLI_REGISTERED_GUARD] = false;
58991
- globalThis[SESSION_COMMANDS_REGISTERED_GUARD] = false;
60388
+ if (!secondaryTookOver) {
60389
+ globalThis[keys.REGISTERED_GUARD] = false;
60390
+ if (remainingServices === 0) {
60391
+ globalThis[CLI_REGISTERED_GUARD] = false;
60392
+ globalThis[SESSION_COMMANDS_REGISTERED_GUARD] = false;
60393
+ }
58992
60394
  }
58993
60395
  } else {
60396
+ if (globalThis[keys.INIT_PROMISE] && globalThis[keys.INIT_PROMISE] !== currentInitPromise) {
60397
+ secondaryTookOver = true;
60398
+ }
58994
60399
  try {
58995
60400
  await currentInitPromise;
58996
60401
  } catch {
@@ -59000,6 +60405,27 @@ Keep the reflection grounded in the evidence below.
59000
60405
  secondaryTookOver = true;
59001
60406
  }
59002
60407
  }
60408
+ if (!secondaryTookOver) {
60409
+ try {
60410
+ activeOpikExporter?.unsubscribe();
60411
+ } catch (err) {
60412
+ log.debug(`engram opik exporter unsubscribe failed: ${err}`);
60413
+ }
60414
+ activeOpikExporter = null;
60415
+ try {
60416
+ await accessHttpServer.stop();
60417
+ } catch (err) {
60418
+ log.debug(`engram access HTTP stop failed: ${err}`);
60419
+ }
60420
+ stopDreamWatcher?.();
60421
+ stopDreamWatcher = null;
60422
+ stopHeartbeatWatcher?.();
60423
+ stopHeartbeatWatcher = null;
60424
+ removeDreamingObserver?.();
60425
+ removeDreamingObserver = null;
60426
+ delete globalThis[keys.ACCESS_HTTP_SERVER];
60427
+ delete globalThis[keys.ACCESS_SERVICE];
60428
+ }
59003
60429
  if (!secondaryTookOver) {
59004
60430
  globalThis[keys.HOOK_APIS] = /* @__PURE__ */ new WeakSet();
59005
60431
  }
@@ -59035,7 +60461,7 @@ function extractTextContent2(msg) {
59035
60461
  // src/bridge.ts
59036
60462
  import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
59037
60463
  import * as childProcess from "child_process";
59038
- import path98 from "path";
60464
+ import path99 from "path";
59039
60465
 
59040
60466
  // src/service-candidates.ts
59041
60467
  function firstSuccessfulResult(candidates, attempt) {
@@ -59062,17 +60488,17 @@ function readCompatEnv(primary, legacy) {
59062
60488
  function configPathCandidates() {
59063
60489
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
59064
60490
  return [
59065
- ...envPath ? [path98.resolve(envPath)] : [],
59066
- path98.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
59067
- path98.join(resolveHomeDir2(), ".config", "engram", "config.json"),
59068
- path98.join(process.cwd(), "remnic.config.json"),
59069
- path98.join(process.cwd(), "engram.config.json")
60491
+ ...envPath ? [path99.resolve(envPath)] : [],
60492
+ path99.join(resolveHomeDir2(), ".config", "remnic", "config.json"),
60493
+ path99.join(resolveHomeDir2(), ".config", "engram", "config.json"),
60494
+ path99.join(process.cwd(), "remnic.config.json"),
60495
+ path99.join(process.cwd(), "engram.config.json")
59070
60496
  ];
59071
60497
  }
59072
60498
  function isDaemonRunning() {
59073
60499
  for (const pidFile of [
59074
- path98.join(resolveHomeDir2(), ".remnic", "server.pid"),
59075
- path98.join(resolveHomeDir2(), ".engram", "server.pid")
60500
+ path99.join(resolveHomeDir2(), ".remnic", "server.pid"),
60501
+ path99.join(resolveHomeDir2(), ".engram", "server.pid")
59076
60502
  ]) {
59077
60503
  try {
59078
60504
  const pid = parseInt(readFileSync7(pidFile, "utf8").trim(), 10);
@@ -59145,8 +60571,8 @@ function detectBridgeMode() {
59145
60571
  }
59146
60572
  function loadAnyToken() {
59147
60573
  const tokenPaths = [
59148
- path98.join(resolveHomeDir2(), ".remnic", "tokens.json"),
59149
- path98.join(resolveHomeDir2(), ".engram", "tokens.json")
60574
+ path99.join(resolveHomeDir2(), ".remnic", "tokens.json"),
60575
+ path99.join(resolveHomeDir2(), ".engram", "tokens.json")
59150
60576
  ];
59151
60577
  for (const tokensPath of tokenPaths) {
59152
60578
  if (!existsSync12(tokensPath)) continue;