@remnic/plugin-openclaw 1.0.12 → 1.0.13

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
@@ -1906,6 +1906,8 @@ function parseConfig(raw) {
1906
1906
  lcmRecallBudgetShare: typeof cfg.lcmRecallBudgetShare === "number" ? Math.max(0, Math.min(1, cfg.lcmRecallBudgetShare)) : 0.15,
1907
1907
  lcmDeterministicMaxTokens: typeof cfg.lcmDeterministicMaxTokens === "number" ? Math.max(64, Math.floor(cfg.lcmDeterministicMaxTokens)) : 512,
1908
1908
  lcmArchiveRetentionDays: typeof cfg.lcmArchiveRetentionDays === "number" ? Math.max(1, Math.floor(cfg.lcmArchiveRetentionDays)) : 90,
1909
+ messagePartsEnabled: coerceBooleanLike(cfg.messagePartsEnabled) === true,
1910
+ messagePartsRecallMaxResults: typeof cfg.messagePartsRecallMaxResults === "number" ? Math.max(0, Math.floor(cfg.messagePartsRecallMaxResults)) : 6,
1909
1911
  // v9.1 Parallel Specialized Retrieval
1910
1912
  parallelRetrievalEnabled: cfg.parallelRetrievalEnabled === true,
1911
1913
  parallelAgentWeights: (() => {
@@ -19349,7 +19351,9 @@ var RECALL_XRAY_SERVED_BY_VALUES = [
19349
19351
  "graph",
19350
19352
  "recent-scan",
19351
19353
  "procedural",
19352
- "review-context"
19354
+ "review-context",
19355
+ "lcm-file-parts",
19356
+ "lcm-tool-parts"
19353
19357
  ];
19354
19358
  function isRecallXrayServedBy(value) {
19355
19359
  return typeof value === "string" && RECALL_XRAY_SERVED_BY_VALUES.includes(value);
@@ -26180,7 +26184,7 @@ function validateReplayTurn(turn, index) {
26180
26184
  // ../remnic-core/src/lcm/schema.ts
26181
26185
  import path37 from "path";
26182
26186
  import { mkdir as mkdir26 } from "fs/promises";
26183
- var LCM_SCHEMA_VERSION = 1;
26187
+ var LCM_SCHEMA_VERSION = 2;
26184
26188
  function openLcmDatabase(memoryDir) {
26185
26189
  const dbPath = path37.join(memoryDir, "state", "lcm.sqlite");
26186
26190
  const db = openBetterSqlite3(dbPath);
@@ -26226,6 +26230,23 @@ function createTables(db) {
26226
26230
  CREATE INDEX IF NOT EXISTS idx_lcm_messages_session
26227
26231
  ON lcm_messages(session_id, turn_index);
26228
26232
 
26233
+ CREATE TABLE IF NOT EXISTS lcm_message_parts (
26234
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26235
+ message_id INTEGER NOT NULL REFERENCES lcm_messages(id) ON DELETE CASCADE,
26236
+ ordinal INTEGER NOT NULL,
26237
+ kind TEXT NOT NULL,
26238
+ payload TEXT NOT NULL,
26239
+ tool_name TEXT,
26240
+ file_path TEXT,
26241
+ created_at TEXT NOT NULL
26242
+ );
26243
+ CREATE INDEX IF NOT EXISTS idx_lcm_message_parts_msg
26244
+ ON lcm_message_parts(message_id, ordinal);
26245
+ CREATE INDEX IF NOT EXISTS idx_lcm_message_parts_tool
26246
+ ON lcm_message_parts(tool_name);
26247
+ CREATE INDEX IF NOT EXISTS idx_lcm_message_parts_file
26248
+ ON lcm_message_parts(file_path);
26249
+
26229
26250
  CREATE TABLE IF NOT EXISTS lcm_summary_nodes (
26230
26251
  id TEXT PRIMARY KEY,
26231
26252
  session_id TEXT NOT NULL,
@@ -26278,6 +26299,423 @@ function createTables(db) {
26278
26299
  );
26279
26300
  }
26280
26301
 
26302
+ // ../remnic-core/src/message-parts/index.ts
26303
+ var LCM_MESSAGE_PART_KINDS = [
26304
+ "text",
26305
+ "tool_call",
26306
+ "tool_result",
26307
+ "patch",
26308
+ "file_read",
26309
+ "file_write",
26310
+ "step_start",
26311
+ "step_finish",
26312
+ "snapshot",
26313
+ "retry"
26314
+ ];
26315
+ var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|token)/i;
26316
+ var MAX_PAYLOAD_STRING = 8e3;
26317
+ var MAX_FILE_SCAN_CHARS = 2e4;
26318
+ function isLcmMessagePartKind(value) {
26319
+ return typeof value === "string" && LCM_MESSAGE_PART_KINDS.includes(value);
26320
+ }
26321
+ function parseMessageParts(input, options = {}) {
26322
+ const explicit = normalizeExplicitParts(input);
26323
+ if (explicit.length > 0) return explicit;
26324
+ const format = options.sourceFormat ?? inferSourceFormat(input);
26325
+ switch (format) {
26326
+ case "openai":
26327
+ return withRenderedFallback(parseOpenAiMessageParts(input, options), options);
26328
+ case "anthropic":
26329
+ return withRenderedFallback(parseAnthropicMessageParts(input, options), options);
26330
+ case "openclaw":
26331
+ return withRenderedFallback(parseOpenClawMessageParts(input, options), options);
26332
+ case "lossless-claw":
26333
+ case "remnic":
26334
+ return withRenderedFallback(normalizeExplicitParts(input), options);
26335
+ default:
26336
+ return renderedFallbackParts(options);
26337
+ }
26338
+ }
26339
+ function normalizeExplicitParts(input) {
26340
+ const rawParts = pickArray(input, "parts") ?? pickArray(input, "message_parts");
26341
+ if (!rawParts) return [];
26342
+ const parts = [];
26343
+ rawParts.forEach((raw, index) => {
26344
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
26345
+ const obj = raw;
26346
+ const kind = normalizeKind(obj.kind ?? obj.type);
26347
+ if (!kind) return;
26348
+ const payload = obj.payload && typeof obj.payload === "object" && !Array.isArray(obj.payload) ? obj.payload : { value: sanitizePayload(obj) };
26349
+ const toolName = asNonEmptyString(obj.toolName ?? obj.tool_name ?? obj.name);
26350
+ const filePath = asNonEmptyString(obj.filePath ?? obj.file_path ?? obj.path);
26351
+ const ordinal = typeof obj.ordinal === "number" && Number.isInteger(obj.ordinal) ? Math.max(0, obj.ordinal) : index;
26352
+ parts.push({
26353
+ ordinal,
26354
+ kind,
26355
+ payload: sanitizePayload(payload),
26356
+ toolName,
26357
+ filePath,
26358
+ createdAt: asNonEmptyString(obj.createdAt ?? obj.created_at)
26359
+ });
26360
+ });
26361
+ return parts;
26362
+ }
26363
+ function parseOpenAiMessageParts(input, _options = {}) {
26364
+ const items = gatherOpenAiItems(input);
26365
+ const parts = [];
26366
+ for (const item of items) {
26367
+ const type = asNonEmptyString(item.type) ?? asNonEmptyString(item.kind);
26368
+ if (!type) continue;
26369
+ if (isOpenAiContentBlock(item)) {
26370
+ const text = asNonEmptyString(item.text ?? item.content);
26371
+ if (text) parts.push(makePart("text", { type, text }, { filePath: firstFilePath(text) }));
26372
+ continue;
26373
+ }
26374
+ if (type === "message") {
26375
+ for (const block of gatherContentBlocks(item.content)) {
26376
+ const text = asNonEmptyString(block.text ?? block.content);
26377
+ if (text) parts.push(makePart("text", { type, text }, { filePath: firstFilePath(text) }));
26378
+ }
26379
+ continue;
26380
+ }
26381
+ if (type === "function_call") {
26382
+ const toolName = asNonEmptyString(item.name ?? item.tool_name);
26383
+ const payload = {
26384
+ id: item.id ?? item.call_id,
26385
+ name: toolName,
26386
+ arguments: parseMaybeJson(item.arguments)
26387
+ };
26388
+ parts.push(classifyToolPart(toolName, payload));
26389
+ continue;
26390
+ }
26391
+ if (type === "function_call_output") {
26392
+ const output = asNonEmptyString(item.output) ?? JSON.stringify(sanitizePayload(item.output ?? item));
26393
+ parts.push(makePart("tool_result", { id: item.id ?? item.call_id, output }, {
26394
+ filePath: firstFilePath(output)
26395
+ }));
26396
+ continue;
26397
+ }
26398
+ if (type === "reasoning") {
26399
+ parts.push(makePart("step_start", { type, summary: sanitizePayload(item.summary ?? item) }));
26400
+ continue;
26401
+ }
26402
+ if (type === "retry") {
26403
+ parts.push(makePart("retry", { type, item: sanitizePayload(item) }));
26404
+ }
26405
+ }
26406
+ return withOrdinals(parts);
26407
+ }
26408
+ function parseAnthropicMessageParts(input, _options = {}) {
26409
+ const blocks = gatherContentBlocks(
26410
+ Array.isArray(input) ? input : input && typeof input === "object" ? input.content : input
26411
+ );
26412
+ const parts = [];
26413
+ for (const block of blocks) {
26414
+ const type = asNonEmptyString(block.type);
26415
+ if (type === "text") {
26416
+ const text = asNonEmptyString(block.text);
26417
+ if (text) parts.push(makePart("text", { type, text }, { filePath: firstFilePath(text) }));
26418
+ continue;
26419
+ }
26420
+ if (type === "tool_use") {
26421
+ const toolName = asNonEmptyString(block.name);
26422
+ parts.push(classifyToolPart(toolName, {
26423
+ id: block.id,
26424
+ name: toolName,
26425
+ input: sanitizePayload(block.input)
26426
+ }));
26427
+ continue;
26428
+ }
26429
+ if (type === "tool_result") {
26430
+ const content = block.content;
26431
+ const rendered = renderUnknownContent(content);
26432
+ parts.push(makePart("tool_result", { id: block.tool_use_id, content: sanitizePayload(content) }, {
26433
+ filePath: firstFilePath(rendered)
26434
+ }));
26435
+ continue;
26436
+ }
26437
+ if (type === "thinking") {
26438
+ parts.push(makePart("step_start", {
26439
+ type,
26440
+ thinking: truncateString(asNonEmptyString(block.thinking) ?? ""),
26441
+ signature: asNonEmptyString(block.signature)
26442
+ }));
26443
+ continue;
26444
+ }
26445
+ if (type === "redacted_thinking") {
26446
+ parts.push(makePart("step_finish", { type }));
26447
+ }
26448
+ }
26449
+ return withOrdinals(parts);
26450
+ }
26451
+ function parseOpenClawMessageParts(input, options = {}) {
26452
+ const explicit = normalizeExplicitParts(input);
26453
+ if (explicit.length > 0) return explicit;
26454
+ if (!input || typeof input !== "object") return [];
26455
+ const obj = input;
26456
+ const content = obj.content;
26457
+ if (Array.isArray(content)) {
26458
+ const hasOpenAiBlocks = content.some(isOpenAiContentBlock);
26459
+ if (hasOpenAiBlocks) return parseOpenAiMessageParts(content, options);
26460
+ const hasAnthropicBlocks = content.some(
26461
+ (block) => block && typeof block === "object" && typeof block.type === "string"
26462
+ );
26463
+ if (hasAnthropicBlocks) return parseAnthropicMessageParts({ content }, options);
26464
+ }
26465
+ const toolName = asNonEmptyString(obj.toolName ?? obj.tool_name ?? obj.name);
26466
+ if (toolName) {
26467
+ return withOrdinals([
26468
+ classifyToolPart(toolName, {
26469
+ name: toolName,
26470
+ input: sanitizePayload(obj.input ?? obj.arguments ?? obj.params),
26471
+ output: sanitizePayload(obj.output ?? obj.result)
26472
+ })
26473
+ ]);
26474
+ }
26475
+ const rendered = options.renderedContent ?? asNonEmptyString(obj.content);
26476
+ return rendered ? withOrdinals(partsFromRenderedText(rendered)) : [];
26477
+ }
26478
+ function partsFromRenderedText(text) {
26479
+ if (text.includes("*** Begin Patch")) {
26480
+ const paths2 = extractFilePaths(text);
26481
+ const patchPaths = extractPatchPaths(text);
26482
+ return withOrdinals((patchPaths.length > 0 ? patchPaths : paths2).map(
26483
+ (filePath) => makePart("patch", { text: truncateString(text) }, { filePath })
26484
+ ));
26485
+ }
26486
+ const paths = extractFilePaths(text);
26487
+ if (paths.length === 0) return [];
26488
+ return withOrdinals(paths.map(
26489
+ (filePath) => makePart("file_read", { text: truncateString(text) }, { filePath })
26490
+ ));
26491
+ }
26492
+ function inferSourceFormat(input) {
26493
+ if (input && typeof input === "object") {
26494
+ const obj = input;
26495
+ const explicit = asNonEmptyString(obj.sourceFormat ?? obj.source_format);
26496
+ if (explicit === "openai" || explicit === "anthropic" || explicit === "openclaw" || explicit === "lossless-claw" || explicit === "remnic") {
26497
+ return explicit;
26498
+ }
26499
+ if (Array.isArray(obj.output)) return "openai";
26500
+ if (isOpenAiResponseItem(obj)) return "openai";
26501
+ if (Array.isArray(obj.content) && obj.content.some(isOpenAiContentBlock)) return "openai";
26502
+ if (Array.isArray(obj.content)) return "anthropic";
26503
+ }
26504
+ if (Array.isArray(input)) {
26505
+ return input.some(
26506
+ (item) => isRecord3(item) && (isOpenAiResponseItem(item) || isOpenAiContentBlock(item))
26507
+ ) ? "openai" : "anthropic";
26508
+ }
26509
+ return void 0;
26510
+ }
26511
+ function isOpenAiResponseItem(obj) {
26512
+ const type = asNonEmptyString(obj.type ?? obj.kind);
26513
+ return type === "message" || type === "function_call" || type === "function_call_output" || type === "reasoning" || type === "retry";
26514
+ }
26515
+ function isOpenAiContentBlock(value) {
26516
+ if (!isRecord3(value)) return false;
26517
+ const type = asNonEmptyString(value.type);
26518
+ return type === "input_text" || type === "output_text" || type === "input_image" || type === "input_file" || type === "refusal";
26519
+ }
26520
+ function gatherOpenAiItems(input) {
26521
+ if (Array.isArray(input)) return input.filter(isRecord3);
26522
+ if (!isRecord3(input)) return [];
26523
+ if (Array.isArray(input.output)) return input.output.filter(isRecord3);
26524
+ if (Array.isArray(input.items)) return input.items.filter(isRecord3);
26525
+ return [input];
26526
+ }
26527
+ function gatherContentBlocks(input) {
26528
+ if (Array.isArray(input)) return input.filter(isRecord3);
26529
+ if (typeof input === "string") return [{ type: "text", text: input }];
26530
+ if (isRecord3(input)) return [input];
26531
+ return [];
26532
+ }
26533
+ function classifyToolPart(toolName, payload) {
26534
+ const normalized = (toolName ?? "").toLowerCase();
26535
+ const rendered = renderUnknownContent(payload);
26536
+ const filePath = firstFilePathFromObject(payload) ?? firstFilePath(rendered) ?? null;
26537
+ if (normalized.includes("apply_patch") || rendered.includes("*** Begin Patch")) {
26538
+ return makePart("patch", payload, { toolName, filePath: filePath ?? extractPatchPaths(rendered)[0] ?? null });
26539
+ }
26540
+ if (/(write|edit|multiedit|create|save)/i.test(normalized)) {
26541
+ return makePart("file_write", payload, { toolName, filePath });
26542
+ }
26543
+ if (/(read|grep|glob|search|list|ls)/i.test(normalized)) {
26544
+ return makePart("file_read", payload, { toolName, filePath });
26545
+ }
26546
+ return makePart("tool_call", payload, { toolName, filePath });
26547
+ }
26548
+ function makePart(kind, payload, options = {}) {
26549
+ return {
26550
+ kind,
26551
+ payload: sanitizePayload(payload),
26552
+ toolName: options.toolName ?? null,
26553
+ filePath: options.filePath ?? null
26554
+ };
26555
+ }
26556
+ function withOrdinals(parts) {
26557
+ return parts.map((part, ordinal) => ({ ...part, ordinal: part.ordinal ?? ordinal }));
26558
+ }
26559
+ function withRenderedFallback(parts, options) {
26560
+ return parts.length > 0 ? parts : renderedFallbackParts(options);
26561
+ }
26562
+ function renderedFallbackParts(options) {
26563
+ const rendered = asNonEmptyString(options.renderedContent);
26564
+ return rendered ? partsFromRenderedText(rendered) : [];
26565
+ }
26566
+ function normalizeKind(value) {
26567
+ if (isLcmMessagePartKind(value)) return value;
26568
+ if (value === "tool_use" || value === "function_call") return "tool_call";
26569
+ if (value === "function_call_output") return "tool_result";
26570
+ if (value === "thinking" || value === "reasoning") return "step_start";
26571
+ return null;
26572
+ }
26573
+ function pickArray(input, key) {
26574
+ if (!input || typeof input !== "object" || Array.isArray(input)) return null;
26575
+ const value = input[key];
26576
+ return Array.isArray(value) ? value : null;
26577
+ }
26578
+ function asNonEmptyString(value) {
26579
+ if (typeof value !== "string") return null;
26580
+ const trimmed = value.trim();
26581
+ return trimmed.length > 0 ? trimmed : null;
26582
+ }
26583
+ function isRecord3(value) {
26584
+ return !!value && typeof value === "object" && !Array.isArray(value);
26585
+ }
26586
+ function parseMaybeJson(value) {
26587
+ if (typeof value !== "string") return sanitizePayload(value);
26588
+ try {
26589
+ return sanitizePayload(JSON.parse(value));
26590
+ } catch {
26591
+ return truncateString(value);
26592
+ }
26593
+ }
26594
+ function sanitizePayload(value, depth = 0) {
26595
+ if (value === null || value === void 0) return value;
26596
+ if (typeof value === "string") return truncateString(value);
26597
+ if (typeof value === "number" || typeof value === "boolean") return value;
26598
+ if (Array.isArray(value)) {
26599
+ if (depth >= 4) return "[truncated]";
26600
+ return value.slice(0, 100).map((item) => sanitizePayload(item, depth + 1));
26601
+ }
26602
+ if (typeof value === "object") {
26603
+ if (depth >= 4) return "[truncated]";
26604
+ const out = {};
26605
+ for (const [key, child] of Object.entries(value)) {
26606
+ out[key] = SECRET_KEY_RE.test(key) ? "[redacted]" : sanitizePayload(child, depth + 1);
26607
+ }
26608
+ return out;
26609
+ }
26610
+ return String(value);
26611
+ }
26612
+ function truncateString(value) {
26613
+ return value.length > MAX_PAYLOAD_STRING ? `${value.slice(0, MAX_PAYLOAD_STRING)}...[truncated]` : value;
26614
+ }
26615
+ function renderUnknownContent(value) {
26616
+ if (typeof value === "string") return value;
26617
+ try {
26618
+ return JSON.stringify(value ?? "");
26619
+ } catch {
26620
+ return String(value ?? "");
26621
+ }
26622
+ }
26623
+ function firstFilePathFromObject(value) {
26624
+ if (!isRecord3(value)) return null;
26625
+ const keys = ["file_path", "filePath", "path", "filename", "cwd"];
26626
+ for (const key of keys) {
26627
+ const candidate = asNonEmptyString(value[key]);
26628
+ if (candidate) return candidate;
26629
+ }
26630
+ for (const child of Object.values(value)) {
26631
+ if (typeof child === "string") {
26632
+ const fromText = extractPatchPaths(child)[0] ?? firstFilePath(child);
26633
+ if (fromText) return fromText;
26634
+ }
26635
+ if (isRecord3(child)) {
26636
+ const nested = firstFilePathFromObject(child);
26637
+ if (nested) return nested;
26638
+ }
26639
+ }
26640
+ return null;
26641
+ }
26642
+ function firstFilePath(text) {
26643
+ return extractFilePaths(text)[0] ?? null;
26644
+ }
26645
+ function extractFilePaths(text) {
26646
+ const out = /* @__PURE__ */ new Set();
26647
+ let token = "";
26648
+ const scanLength = Math.min(text.length, MAX_FILE_SCAN_CHARS);
26649
+ for (let index = 0; index <= scanLength; index += 1) {
26650
+ const char = index < scanLength ? text[index] : " ";
26651
+ if (isFilePathTokenSeparator(char)) {
26652
+ addFilePathCandidate(out, token);
26653
+ token = "";
26654
+ continue;
26655
+ }
26656
+ token += char;
26657
+ if (token.length > 512) {
26658
+ addFilePathCandidate(out, token);
26659
+ token = "";
26660
+ }
26661
+ }
26662
+ return [...out].slice(0, 20);
26663
+ }
26664
+ function isFilePathTokenSeparator(char) {
26665
+ return char === " " || char === "\n" || char === "\r" || char === " " || char === '"' || char === "'" || char === "`" || char === "(" || char === ")" || char === "[" || char === "]" || char === "{" || char === "}" || char === "<" || char === ">" || char === ",";
26666
+ }
26667
+ function addFilePathCandidate(out, raw) {
26668
+ const candidate = trimFilePathPunctuation(raw);
26669
+ if (candidate.length === 0 || candidate.includes("://")) return;
26670
+ if (isLikelyFilePath(candidate)) out.add(candidate);
26671
+ }
26672
+ function trimFilePathPunctuation(raw) {
26673
+ let start = 0;
26674
+ let end = raw.length;
26675
+ while (start < end && isLeadingFilePathPunctuation(raw[start])) start += 1;
26676
+ while (end > start && isTrailingFilePathPunctuation(raw[end - 1])) end -= 1;
26677
+ return raw.slice(start, end);
26678
+ }
26679
+ function isLeadingFilePathPunctuation(char) {
26680
+ return char === ":" || char === ";" || char === "!" || char === "?" || char === "|" || char === "*" || char === "=";
26681
+ }
26682
+ function isTrailingFilePathPunctuation(char) {
26683
+ return char === "." || char === ":" || char === ";" || char === "!" || char === "?" || char === "|" || char === "*" || char === "=";
26684
+ }
26685
+ function isLikelyFilePath(value) {
26686
+ if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("~/")) {
26687
+ return hasValidFileExtension(value);
26688
+ }
26689
+ if (value.includes("/")) return hasValidFileExtension(value);
26690
+ return hasValidFileExtension(value);
26691
+ }
26692
+ function hasValidFileExtension(value) {
26693
+ const lastSlash = value.lastIndexOf("/");
26694
+ const basename3 = value.slice(lastSlash + 1);
26695
+ const dot = basename3.lastIndexOf(".");
26696
+ if (dot <= 0 || dot === basename3.length - 1) return false;
26697
+ const ext = basename3.slice(dot + 1);
26698
+ if (ext.length < 1 || ext.length > 12) return false;
26699
+ for (const char of ext) {
26700
+ if (!isFileExtensionChar(char)) return false;
26701
+ }
26702
+ return true;
26703
+ }
26704
+ function isFileExtensionChar(char) {
26705
+ const code = char.charCodeAt(0);
26706
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_" || char === "+" || char === "-";
26707
+ }
26708
+ function extractPatchPaths(text) {
26709
+ const out = /* @__PURE__ */ new Set();
26710
+ for (const line of text.split(/\r?\n/)) {
26711
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/);
26712
+ if (match?.[1]) out.add(match[1].trim());
26713
+ const move = line.match(/^\*\*\* Move to: (.+)$/);
26714
+ if (move?.[1]) out.add(move[1].trim());
26715
+ }
26716
+ return [...out].slice(0, 20);
26717
+ }
26718
+
26281
26719
  // ../remnic-core/src/lcm/archive.ts
26282
26720
  function estimateTokens3(text) {
26283
26721
  return Math.ceil(text.length / 4);
@@ -26288,7 +26726,7 @@ var LcmArchive = class {
26288
26726
  }
26289
26727
  db;
26290
26728
  /** Append a message to the archive. Returns the row id. */
26291
- appendMessage(sessionId, turnIndex, role, content, metadata) {
26729
+ appendMessage(sessionId, turnIndex, role, content, metadata, parts) {
26292
26730
  const tokenCount = estimateTokens3(content);
26293
26731
  const now = (/* @__PURE__ */ new Date()).toISOString();
26294
26732
  const metaJson = metadata ? JSON.stringify(metadata) : null;
@@ -26299,18 +26737,58 @@ var LcmArchive = class {
26299
26737
  const result = stmt.run(sessionId, turnIndex, role, content, tokenCount, now, metaJson);
26300
26738
  const rowId = Number(result.lastInsertRowid);
26301
26739
  this.db.prepare("INSERT INTO lcm_messages_fts (rowid, content) VALUES (?, ?)").run(rowId, content);
26740
+ if (parts && parts.length > 0) {
26741
+ this.insertMessageParts(rowId, parts, now);
26742
+ }
26302
26743
  return rowId;
26303
26744
  }
26304
26745
  /** Append multiple messages in a single transaction. */
26305
- appendMessages(sessionId, messages) {
26746
+ appendMessages(sessionId, messages, options = {}) {
26306
26747
  if (messages.length === 0) return;
26748
+ const captureMessageParts = options.messagePartsEnabled !== false;
26307
26749
  const txn = this.db.transaction(() => {
26308
26750
  for (const msg of messages) {
26309
- this.appendMessage(sessionId, msg.turnIndex, msg.role, msg.content, msg.metadata);
26751
+ const explicitParts = msg.parts && msg.parts.length > 0 ? msg.parts : void 0;
26752
+ const rawContent = msg.rawContent ?? msg.content;
26753
+ const parts = captureMessageParts ? explicitParts ?? parseMessageParts(rawContent, {
26754
+ sourceFormat: msg.sourceFormat,
26755
+ renderedContent: msg.content
26756
+ }) : void 0;
26757
+ this.appendMessage(
26758
+ sessionId,
26759
+ msg.turnIndex,
26760
+ msg.role,
26761
+ msg.content,
26762
+ msg.metadata,
26763
+ parts
26764
+ );
26310
26765
  }
26311
26766
  });
26312
26767
  txn();
26313
26768
  }
26769
+ insertMessageParts(messageId, parts, fallbackCreatedAt) {
26770
+ if (parts.length === 0) return;
26771
+ const stmt = this.db.prepare(`
26772
+ INSERT INTO lcm_message_parts (message_id, ordinal, kind, payload, tool_name, file_path, created_at)
26773
+ VALUES (?, ?, ?, ?, ?, ?, ?)
26774
+ `);
26775
+ for (let index = 0; index < parts.length; index += 1) {
26776
+ const part = parts[index];
26777
+ const rawPart = part;
26778
+ const toolName = part.toolName ?? asNullableString(rawPart.tool_name);
26779
+ const filePath = part.filePath ?? asNullableString(rawPart.file_path);
26780
+ const createdAt = part.createdAt ?? asNullableString(rawPart.created_at);
26781
+ stmt.run(
26782
+ messageId,
26783
+ part.ordinal ?? index,
26784
+ part.kind,
26785
+ JSON.stringify(part.payload ?? {}),
26786
+ toolName ?? null,
26787
+ filePath ?? null,
26788
+ createdAt ?? fallbackCreatedAt
26789
+ );
26790
+ }
26791
+ }
26314
26792
  /** Get the highest turn_index for a session, or -1 if none. */
26315
26793
  getMaxTurnIndex(sessionId) {
26316
26794
  const row = this.db.prepare("SELECT MAX(turn_index) as max_turn FROM lcm_messages WHERE session_id = ?").get(sessionId);
@@ -26432,6 +26910,55 @@ var LcmArchive = class {
26432
26910
  return [];
26433
26911
  }
26434
26912
  }
26913
+ searchStructuredParts(query, limit, sessionId) {
26914
+ const cappedLimit = Math.max(0, Math.min(20, Math.floor(limit)));
26915
+ if (cappedLimit === 0) return [];
26916
+ const fileTerms = extractStructuredFileTerms(query);
26917
+ const toolTerms = extractStructuredToolTerms(query);
26918
+ if (fileTerms.length === 0 && toolTerms.length === 0) return [];
26919
+ const matchWhere = [];
26920
+ const whereParams = [];
26921
+ for (const term of fileTerms) {
26922
+ matchWhere.push("(p.file_path = ? OR p.file_path LIKE ? ESCAPE '\\')");
26923
+ whereParams.push(term, `%${escapeLike(term)}%`);
26924
+ }
26925
+ for (const term of toolTerms) {
26926
+ matchWhere.push("p.tool_name LIKE ? ESCAPE '\\'");
26927
+ whereParams.push(`%${escapeLike(term)}%`);
26928
+ }
26929
+ const where = [`(${matchWhere.join(" OR ")})`];
26930
+ if (sessionId) {
26931
+ where.push("m.session_id = ?");
26932
+ whereParams.push(sessionId);
26933
+ }
26934
+ const exactFileScoreParams = [...fileTerms];
26935
+ const sqlParams = [...exactFileScoreParams, ...whereParams, cappedLimit];
26936
+ const rows = this.db.prepare(`
26937
+ SELECT
26938
+ p.id AS part_id,
26939
+ p.message_id AS message_id,
26940
+ m.turn_index AS turn_index,
26941
+ m.role AS role,
26942
+ m.content AS content,
26943
+ m.session_id AS session_id,
26944
+ p.kind AS kind,
26945
+ p.tool_name AS tool_name,
26946
+ p.file_path AS file_path,
26947
+ p.payload AS payload,
26948
+ CASE
26949
+ WHEN p.file_path IN (${fileTerms.map(() => "?").join(",") || "NULL"}) THEN 3
26950
+ WHEN p.file_path IS NOT NULL THEN 2
26951
+ WHEN p.tool_name IS NOT NULL THEN 1
26952
+ ELSE 0
26953
+ END AS score
26954
+ FROM lcm_message_parts p
26955
+ JOIN lcm_messages m ON m.id = p.message_id
26956
+ WHERE ${where.join(" AND ")}
26957
+ ORDER BY score DESC, m.turn_index DESC, p.ordinal ASC
26958
+ LIMIT ?
26959
+ `).all(...sqlParams);
26960
+ return rows;
26961
+ }
26435
26962
  /** Get total message count for a session. */
26436
26963
  getMessageCount(sessionId) {
26437
26964
  const row = this.db.prepare("SELECT COUNT(*) as cnt FROM lcm_messages WHERE session_id = ?").get(sessionId);
@@ -26583,6 +27110,72 @@ var STOPWORDS = /* @__PURE__ */ new Set([
26583
27110
  "we",
26584
27111
  "they"
26585
27112
  ]);
27113
+ function extractStructuredFileTerms(query) {
27114
+ const terms = /* @__PURE__ */ new Set();
27115
+ for (const raw of splitQueryTerms(query)) {
27116
+ const cleaned = trimStructuredQueryTerm(raw);
27117
+ if (cleaned.includes("/") || hasStructuredFileExtension(cleaned)) {
27118
+ terms.add(cleaned);
27119
+ const basename3 = cleaned.split("/").pop();
27120
+ if (basename3 && basename3 !== cleaned) terms.add(basename3);
27121
+ }
27122
+ }
27123
+ return [...terms].filter((term) => term.length > 1).slice(0, 12);
27124
+ }
27125
+ function splitQueryTerms(query) {
27126
+ const terms = [];
27127
+ let term = "";
27128
+ for (const char of query.slice(0, 2e4)) {
27129
+ if (char === " " || char === "\n" || char === "\r" || char === " ") {
27130
+ if (term.length > 0) terms.push(term);
27131
+ term = "";
27132
+ continue;
27133
+ }
27134
+ term += char;
27135
+ if (term.length > 512) {
27136
+ terms.push(term);
27137
+ term = "";
27138
+ }
27139
+ }
27140
+ if (term.length > 0) terms.push(term);
27141
+ return terms;
27142
+ }
27143
+ function trimStructuredQueryTerm(raw) {
27144
+ const leading = /* @__PURE__ */ new Set(["`", "'", '"', "(", "[", "{"]);
27145
+ const trailing = /* @__PURE__ */ new Set(["`", "'", '"', ",", ".", "?", "!", ":", ";", ")", "]", "}"]);
27146
+ let start = 0;
27147
+ let end = raw.length;
27148
+ while (start < end && leading.has(raw[start])) start += 1;
27149
+ while (end > start && trailing.has(raw[end - 1])) end -= 1;
27150
+ return raw.slice(start, end);
27151
+ }
27152
+ function hasStructuredFileExtension(value) {
27153
+ const slash = value.lastIndexOf("/");
27154
+ const basename3 = value.slice(slash + 1);
27155
+ const dot = basename3.lastIndexOf(".");
27156
+ if (dot <= 0 || dot === basename3.length - 1) return false;
27157
+ const ext = basename3.slice(dot + 1);
27158
+ if (ext.length < 1 || ext.length > 12) return false;
27159
+ for (const char of ext) {
27160
+ const code = char.charCodeAt(0);
27161
+ const valid = code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_" || char === "+" || char === "-";
27162
+ if (!valid) return false;
27163
+ }
27164
+ return true;
27165
+ }
27166
+ function extractStructuredToolTerms(query) {
27167
+ const lower = query.toLowerCase();
27168
+ if (!/\b(tool|command|invocation|called|used|ran|read|write|patch|edit|grep|search)\b/.test(lower)) {
27169
+ return [];
27170
+ }
27171
+ return query.replace(/[^\w.-]/g, " ").split(/\s+/).filter((term) => term.length > 2 && !STOPWORDS.has(term.toLowerCase())).slice(0, 8);
27172
+ }
27173
+ function escapeLike(value) {
27174
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
27175
+ }
27176
+ function asNullableString(value) {
27177
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
27178
+ }
26586
27179
  function sanitizeFtsQuery(raw) {
26587
27180
  const words = raw.replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 1 && !STOPWORDS.has(w.toLowerCase()));
26588
27181
  if (words.length === 0) {
@@ -27052,7 +27645,9 @@ function extractLcmConfig(cfg) {
27052
27645
  maxDepth: cfg.lcmMaxDepth ?? 5,
27053
27646
  deterministicMaxTokens: cfg.lcmDeterministicMaxTokens ?? 512,
27054
27647
  archiveRetentionDays: cfg.lcmArchiveRetentionDays ?? 90,
27055
- recallBudgetShare: cfg.lcmRecallBudgetShare ?? 0.15
27648
+ recallBudgetShare: cfg.lcmRecallBudgetShare ?? 0.15,
27649
+ messagePartsEnabled: cfg.messagePartsEnabled === true,
27650
+ messagePartsRecallMaxResults: typeof cfg.messagePartsRecallMaxResults === "number" ? Math.max(0, Math.floor(cfg.messagePartsRecallMaxResults)) : 6
27056
27651
  };
27057
27652
  }
27058
27653
  var LcmEngine = class {
@@ -27184,9 +27779,14 @@ var LcmEngine = class {
27184
27779
  const newMessages = messages.map((m, i) => ({
27185
27780
  turnIndex: currentMax + 1 + i,
27186
27781
  role: m.role,
27187
- content: m.content
27782
+ content: m.content,
27783
+ parts: this.config.messagePartsEnabled ? m.parts : void 0,
27784
+ rawContent: this.config.messagePartsEnabled ? m.rawContent : void 0,
27785
+ sourceFormat: this.config.messagePartsEnabled ? m.sourceFormat : void 0
27188
27786
  }));
27189
- this.archive.appendMessages(sessionId, newMessages);
27787
+ this.archive.appendMessages(sessionId, newMessages, {
27788
+ messagePartsEnabled: this.config.messagePartsEnabled
27789
+ });
27190
27790
  try {
27191
27791
  await this.summarizer.summarizeIncremental(sessionId);
27192
27792
  } catch (err) {
@@ -27264,6 +27864,28 @@ var LcmEngine = class {
27264
27864
  budgetChars: effectiveBudget
27265
27865
  });
27266
27866
  }
27867
+ async searchStructuredParts(sessionId, query, limit = this.config.messagePartsRecallMaxResults) {
27868
+ if (!this.config.enabled || !this.config.messagePartsEnabled) return [];
27869
+ await this.ensureInitialized();
27870
+ if (!this.archive) return [];
27871
+ return this.archive.searchStructuredParts(query, limit, sessionId);
27872
+ }
27873
+ formatStructuredRecall(matches, budgetChars) {
27874
+ if (matches.length === 0 || budgetChars <= 0) return "";
27875
+ const lines = [];
27876
+ let used = "## Structured Session Matches\n\n".length;
27877
+ for (const match of matches) {
27878
+ const label = match.file_path ? `${match.kind} ${match.file_path}` : match.tool_name ? `${match.kind} ${match.tool_name}` : match.kind;
27879
+ const excerpt = match.content.replace(/\s+/g, " ").slice(0, 220);
27880
+ const line = `- turn ${match.turn_index} (${match.role}): ${label} \u2014 ${excerpt}`;
27881
+ if (used + line.length + 1 > budgetChars) break;
27882
+ lines.push(line);
27883
+ used += line.length + 1;
27884
+ }
27885
+ return lines.length > 0 ? `## Structured Session Matches
27886
+
27887
+ ${lines.join("\n")}` : "";
27888
+ }
27267
27889
  /** Flush pending summaries before compaction (called from before_compaction hook). */
27268
27890
  async preCompactionFlush(sessionId) {
27269
27891
  if (!this.config.enabled) return;
@@ -35504,6 +36126,7 @@ ${r.snippet.trim()}
35504
36126
  let recalledMemoryIds = [];
35505
36127
  let recalledMemoryPaths = [];
35506
36128
  let xrayRecalledResults = [];
36129
+ const lcmStructuredXrayResults = [];
35507
36130
  const xrayBranchPoolSize = {
35508
36131
  hot_qmd: 0,
35509
36132
  hot_embedding: 0,
@@ -37427,6 +38050,32 @@ ${tmtNode.summary}`
37427
38050
  }
37428
38051
  if (this.lcmEngine?.enabled && recallMode !== "minimal" && recallMode !== "no_recall") {
37429
38052
  try {
38053
+ const structuredMatches = await this.lcmEngine.searchStructuredParts(
38054
+ sessionKey ?? "default",
38055
+ retrievalQuery
38056
+ );
38057
+ const structuredSection = this.lcmEngine.formatStructuredRecall(
38058
+ structuredMatches,
38059
+ Math.ceil(this.config.recallBudgetChars * 0.08)
38060
+ );
38061
+ if (structuredSection) {
38062
+ const structuredAppended = this.appendRecallSection(
38063
+ sectionBuckets,
38064
+ "lcm-message-parts",
38065
+ structuredSection
38066
+ );
38067
+ if (structuredAppended) {
38068
+ for (const match of structuredMatches) {
38069
+ lcmStructuredXrayResults.push({
38070
+ memoryId: `lcm-message-part-${match.part_id}`,
38071
+ path: `lcm://${match.session_id}/turn/${match.turn_index}/part/${match.part_id}`,
38072
+ servedBy: match.file_path ? "lcm-file-parts" : "lcm-tool-parts",
38073
+ scoreDecomposition: { final: match.score },
38074
+ admittedBy: ["lcm-message-parts"]
38075
+ });
38076
+ }
38077
+ }
38078
+ }
37430
38079
  const lcmSection = await this.lcmEngine.assembleRecall(
37431
38080
  sessionKey ?? "default",
37432
38081
  this.config.recallBudgetChars
@@ -38327,10 +38976,17 @@ _Context: ${topQuestion.context}_`
38327
38976
  admitted: recalledMemoryIds.length
38328
38977
  }
38329
38978
  ];
38979
+ if (lcmStructuredXrayResults.length > 0) {
38980
+ filters.push({
38981
+ name: "lcm-message-parts",
38982
+ considered: lcmStructuredXrayResults.length,
38983
+ admitted: lcmStructuredXrayResults.length
38984
+ });
38985
+ }
38330
38986
  this.lastXraySnapshot = buildXraySnapshot({
38331
38987
  query: retrievalQuery,
38332
38988
  tierExplain: null,
38333
- results,
38989
+ results: [...results, ...lcmStructuredXrayResults],
38334
38990
  filters,
38335
38991
  budget: {
38336
38992
  chars: this.getRecallBudgetChars(options.budgetCharsOverride),
@@ -38497,13 +39153,28 @@ _Context: ${topQuestion.context}_`
38497
39153
  role: turn.role,
38498
39154
  content: turn.content,
38499
39155
  timestamp: turn.timestamp,
38500
- sessionKey: key
39156
+ sessionKey: key,
39157
+ parts: turn.parts,
39158
+ rawContent: turn.rawContent,
39159
+ sourceFormat: turn.sourceFormat
38501
39160
  });
38502
39161
  bySession.set(key, list);
38503
39162
  }
38504
39163
  const replayTasks = [];
38505
39164
  for (const [key, sessionTurns] of bySession.entries()) {
38506
39165
  if (sessionTurns.length === 0) continue;
39166
+ if (options.archiveLcm !== false && this.lcmEngine?.enabled) {
39167
+ await this.lcmEngine.observeMessages(
39168
+ key,
39169
+ sessionTurns.map((turn) => ({
39170
+ role: turn.role,
39171
+ content: turn.content,
39172
+ parts: turn.parts,
39173
+ rawContent: turn.rawContent,
39174
+ sourceFormat: turn.sourceFormat
39175
+ }))
39176
+ );
39177
+ }
38507
39178
  replayTasks.push(
38508
39179
  new Promise((resolve2, reject) => {
38509
39180
  void this.queueBufferedExtraction(sessionTurns, "trigger_mode", {
@@ -38592,10 +39263,25 @@ _Context: ${topQuestion.context}_`
38592
39263
  role: turn.role,
38593
39264
  content: turn.content,
38594
39265
  timestamp: turn.timestamp,
38595
- sessionKey
39266
+ sessionKey,
39267
+ parts: turn.parts,
39268
+ rawContent: turn.rawContent,
39269
+ sourceFormat: turn.sourceFormat
38596
39270
  });
38597
39271
  }
38598
39272
  if (sessionTurns.length === 0) return;
39273
+ if (this.lcmEngine?.enabled) {
39274
+ await this.lcmEngine.observeMessages(
39275
+ sessionKey,
39276
+ sessionTurns.map((turn) => ({
39277
+ role: turn.role,
39278
+ content: turn.content,
39279
+ parts: turn.parts,
39280
+ rawContent: turn.rawContent,
39281
+ sourceFormat: turn.sourceFormat
39282
+ }))
39283
+ );
39284
+ }
38599
39285
  await new Promise((resolve2, reject) => {
38600
39286
  void this.queueBufferedExtraction(sessionTurns, "trigger_mode", {
38601
39287
  skipDedupeCheck: true,
@@ -43124,13 +43810,13 @@ function toolJsonResult(value, options) {
43124
43810
  function workLayerTextResult(text, options) {
43125
43811
  return toolResult2(wrapWorkLayerContext(text, { linkToMemory: options?.linkToMemory === true }));
43126
43812
  }
43127
- function asNonEmptyString(value) {
43813
+ function asNonEmptyString2(value) {
43128
43814
  if (typeof value !== "string") return void 0;
43129
43815
  const trimmed = value.trim();
43130
43816
  return trimmed.length > 0 ? trimmed : void 0;
43131
43817
  }
43132
43818
  function normalizeToolNamespace(value) {
43133
- return asNonEmptyString(value);
43819
+ return asNonEmptyString2(value);
43134
43820
  }
43135
43821
  function clampUnitInterval(value, fallback) {
43136
43822
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
@@ -43179,17 +43865,17 @@ var WORK_TASK_STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress", "blocke
43179
43865
  var WORK_TASK_PRIORITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
43180
43866
  var WORK_PROJECT_STATUSES = /* @__PURE__ */ new Set(["active", "on_hold", "completed", "archived"]);
43181
43867
  function asTaskStatus(value) {
43182
- const normalized = asNonEmptyString(value);
43868
+ const normalized = asNonEmptyString2(value);
43183
43869
  if (!normalized || !WORK_TASK_STATUSES.has(normalized)) return void 0;
43184
43870
  return normalized;
43185
43871
  }
43186
43872
  function asTaskPriority(value) {
43187
- const normalized = asNonEmptyString(value);
43873
+ const normalized = asNonEmptyString2(value);
43188
43874
  if (!normalized || !WORK_TASK_PRIORITIES.has(normalized)) return void 0;
43189
43875
  return normalized;
43190
43876
  }
43191
43877
  function asProjectStatus(value) {
43192
- const normalized = asNonEmptyString(value);
43878
+ const normalized = asNonEmptyString2(value);
43193
43879
  if (!normalized || !WORK_PROJECT_STATUSES.has(normalized)) return void 0;
43194
43880
  return normalized;
43195
43881
  }
@@ -43329,7 +44015,7 @@ function registerTools(api, orchestrator) {
43329
44015
  return createHash10("sha256").update(input).digest("hex").slice(0, 16);
43330
44016
  }
43331
44017
  function normalizeMemoryCategory(value, fallback) {
43332
- const normalized = asNonEmptyString(value);
44018
+ const normalized = asNonEmptyString2(value);
43333
44019
  if (!normalized) return fallback;
43334
44020
  if (!VALID_MEMORY_CATEGORIES.has(normalized)) return void 0;
43335
44021
  return normalized;
@@ -43338,8 +44024,8 @@ function registerTools(api, orchestrator) {
43338
44024
  return typeof value === "string" && actionTypeSet.has(value);
43339
44025
  }
43340
44026
  function buildActionInputSummary(action, params) {
43341
- const primary = asNonEmptyString(params.memoryId) ?? asNonEmptyString(params.category) ?? asNonEmptyString(params.linkTargetId);
43342
- const content = asNonEmptyString(params.content);
44027
+ const primary = asNonEmptyString2(params.memoryId) ?? asNonEmptyString2(params.category) ?? asNonEmptyString2(params.linkTargetId);
44028
+ const content = asNonEmptyString2(params.content);
43343
44029
  let summary = primary ? `${action} => ${primary}` : action;
43344
44030
  if (content) {
43345
44031
  summary += ` | ${content.slice(0, 120)}`;
@@ -44224,14 +44910,14 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
44224
44910
  return toolResult2(`Recorded context checkpoint telemetry in namespace=${ns}.`);
44225
44911
  }
44226
44912
  const validationErrors = [];
44227
- if (!asNonEmptyString(sessionKey)) validationErrors.push("sessionKey is required");
44913
+ if (!asNonEmptyString2(sessionKey)) validationErrors.push("sessionKey is required");
44228
44914
  if (!Array.isArray(turns) || turns.length === 0) validationErrors.push("turns must be a non-empty array");
44229
44915
  const baseEvent = {
44230
44916
  action: "summarize_node",
44231
44917
  namespace: ns,
44232
44918
  actor: "tool.context_checkpoint",
44233
44919
  subsystem: "tools.context_checkpoint",
44234
- sourceSessionKey: asNonEmptyString(sessionKey),
44920
+ sourceSessionKey: asNonEmptyString2(sessionKey),
44235
44921
  inputSummary: summary,
44236
44922
  checkpointTurnCount: Array.isArray(turns) ? turns.length : void 0,
44237
44923
  dryRun: dryRun === true,
@@ -44393,10 +45079,10 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
44393
45079
  return toolResult2(`Validation failed: invalid action ${String(action)}.`);
44394
45080
  }
44395
45081
  const validationErrors = [];
44396
- const contentValue = asNonEmptyString(content);
44397
- const memoryIdValue = asNonEmptyString(memoryId);
44398
- const linkTargetIdValue = asNonEmptyString(linkTargetId);
44399
- const linkTypeValue = asNonEmptyString(linkType);
45082
+ const contentValue = asNonEmptyString2(content);
45083
+ const memoryIdValue = asNonEmptyString2(memoryId);
45084
+ const linkTargetIdValue = asNonEmptyString2(linkTargetId);
45085
+ const linkTypeValue = asNonEmptyString2(linkType);
44400
45086
  const structuredActionRequest = execute === true || Object.prototype.hasOwnProperty.call(params, "content") || Object.prototype.hasOwnProperty.call(params, "category") || Object.prototype.hasOwnProperty.call(params, "linkTargetId") || Object.prototype.hasOwnProperty.call(params, "linkType") || Object.prototype.hasOwnProperty.call(params, "linkStrength") || Object.prototype.hasOwnProperty.call(params, "artifactType");
44401
45087
  const baseEvent = {
44402
45088
  action,
@@ -44405,7 +45091,7 @@ NOTE: You did not provide sessionKey; under concurrency this may not match your
44405
45091
  subsystem: "tools.memory_action_apply",
44406
45092
  reason,
44407
45093
  memoryId: memoryIdValue,
44408
- sourceSessionKey: asNonEmptyString(sessionKey),
45094
+ sourceSessionKey: asNonEmptyString2(sessionKey),
44409
45095
  inputSummary: buildActionInputSummary(action, params),
44410
45096
  promptHash: promptHashForTelemetry(sourcePrompt)
44411
45097
  };
@@ -45315,16 +46001,16 @@ Best for:
45315
46001
  description: typeof p.description === "string" ? p.description : void 0,
45316
46002
  status: asTaskStatus(p.status),
45317
46003
  priority: asTaskPriority(p.priority),
45318
- owner: asNonEmptyString(p.owner),
45319
- assignee: asNonEmptyString(p.assignee),
45320
- projectId: asNonEmptyString(p.projectId),
46004
+ owner: asNonEmptyString2(p.owner),
46005
+ assignee: asNonEmptyString2(p.assignee),
46006
+ projectId: asNonEmptyString2(p.projectId),
45321
46007
  tags: Array.isArray(p.tags) ? p.tags.filter((x) => typeof x === "string") : void 0,
45322
- dueAt: asNonEmptyString(p.dueAt)
46008
+ dueAt: asNonEmptyString2(p.dueAt)
45323
46009
  });
45324
46010
  return toolJsonResult({ action, task: created });
45325
46011
  }
45326
46012
  if (action === "get") {
45327
- const taskId = asNonEmptyString(p.id);
46013
+ const taskId = asNonEmptyString2(p.id);
45328
46014
  if (!taskId) {
45329
46015
  return workLayerTextResult("work_task.get requires `id`.");
45330
46016
  }
@@ -45334,14 +46020,14 @@ Best for:
45334
46020
  if (action === "list") {
45335
46021
  const tasks = await storage.listTasks({
45336
46022
  status: asTaskStatus(p.status),
45337
- owner: asNonEmptyString(p.owner),
45338
- assignee: asNonEmptyString(p.assignee),
45339
- projectId: asNonEmptyString(p.projectId)
46023
+ owner: asNonEmptyString2(p.owner),
46024
+ assignee: asNonEmptyString2(p.assignee),
46025
+ projectId: asNonEmptyString2(p.projectId)
45340
46026
  });
45341
46027
  return toolJsonResult({ action, count: tasks.length, tasks });
45342
46028
  }
45343
46029
  if (action === "update") {
45344
- const taskId = asNonEmptyString(p.id);
46030
+ const taskId = asNonEmptyString2(p.id);
45345
46031
  if (!taskId) {
45346
46032
  return workLayerTextResult("work_task.update requires `id`.");
45347
46033
  }
@@ -45365,8 +46051,8 @@ Best for:
45365
46051
  return toolJsonResult({ action, task: updated });
45366
46052
  }
45367
46053
  if (action === "transition") {
45368
- const taskId = asNonEmptyString(p.id);
45369
- const rawStatus = asNonEmptyString(p.status);
46054
+ const taskId = asNonEmptyString2(p.id);
46055
+ const rawStatus = asNonEmptyString2(p.status);
45370
46056
  if (!taskId || !rawStatus) {
45371
46057
  return workLayerTextResult("work_task.transition requires `id` and `status`.");
45372
46058
  }
@@ -45378,7 +46064,7 @@ Best for:
45378
46064
  return toolJsonResult({ action, task });
45379
46065
  }
45380
46066
  if (action === "delete") {
45381
- const taskId = asNonEmptyString(p.id);
46067
+ const taskId = asNonEmptyString2(p.id);
45382
46068
  if (!taskId) {
45383
46069
  return workLayerTextResult("work_task.delete requires `id`.");
45384
46070
  }
@@ -45426,13 +46112,13 @@ Best for:
45426
46112
  name: p.name,
45427
46113
  description: typeof p.description === "string" ? p.description : void 0,
45428
46114
  status: asProjectStatus(p.status),
45429
- owner: asNonEmptyString(p.owner),
46115
+ owner: asNonEmptyString2(p.owner),
45430
46116
  tags: Array.isArray(p.tags) ? p.tags.filter((x) => typeof x === "string") : void 0
45431
46117
  });
45432
46118
  return toolJsonResult({ action, project });
45433
46119
  }
45434
46120
  if (action === "get") {
45435
- const projectId = asNonEmptyString(p.id);
46121
+ const projectId = asNonEmptyString2(p.id);
45436
46122
  if (!projectId) {
45437
46123
  return workLayerTextResult("work_project.get requires `id`.");
45438
46124
  }
@@ -45444,7 +46130,7 @@ Best for:
45444
46130
  return toolJsonResult({ action, count: projects.length, projects });
45445
46131
  }
45446
46132
  if (action === "update") {
45447
- const projectId = asNonEmptyString(p.id);
46133
+ const projectId = asNonEmptyString2(p.id);
45448
46134
  if (!projectId) {
45449
46135
  return workLayerTextResult("work_project.update requires `id`.");
45450
46136
  }
@@ -45460,7 +46146,7 @@ Best for:
45460
46146
  return toolJsonResult({ action, project });
45461
46147
  }
45462
46148
  if (action === "delete") {
45463
- const projectId = asNonEmptyString(p.id);
46149
+ const projectId = asNonEmptyString2(p.id);
45464
46150
  if (!projectId) {
45465
46151
  return workLayerTextResult("work_project.delete requires `id`.");
45466
46152
  }
@@ -45468,8 +46154,8 @@ Best for:
45468
46154
  return toolJsonResult({ action, deleted });
45469
46155
  }
45470
46156
  if (action === "link_task") {
45471
- const taskId = asNonEmptyString(p.taskId);
45472
- const projectId = asNonEmptyString(p.projectId);
46157
+ const taskId = asNonEmptyString2(p.taskId);
46158
+ const projectId = asNonEmptyString2(p.projectId);
45473
46159
  if (!taskId || !projectId) {
45474
46160
  return workLayerTextResult("work_project.link_task requires `taskId` and `projectId`.");
45475
46161
  }
@@ -45505,7 +46191,7 @@ Best for:
45505
46191
  async execute(_toolCallId, params) {
45506
46192
  const p = params;
45507
46193
  const action = String(p.action ?? "");
45508
- const projectId = asNonEmptyString(p.projectId);
46194
+ const projectId = asNonEmptyString2(p.projectId);
45509
46195
  const linkToMemory = p.linkToMemory === true;
45510
46196
  try {
45511
46197
  await new WorkStorage(orchestrator.config.memoryDir).ensureDirectories();
@@ -45525,7 +46211,7 @@ Best for:
45525
46211
  const result = await importWorkBoardSnapshot({
45526
46212
  memoryDir: orchestrator.config.memoryDir,
45527
46213
  snapshot,
45528
- projectId: asNonEmptyString(p.projectId)
46214
+ projectId: asNonEmptyString2(p.projectId)
45529
46215
  });
45530
46216
  return toolJsonResult({ action, result }, { linkToMemory });
45531
46217
  }
@@ -45791,7 +46477,7 @@ Returns: Performance trace data with timing breakdown`,
45791
46477
  "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
45792
46478
  );
45793
46479
  }
45794
- const format = asNonEmptyString(params.format) ?? "ascii";
46480
+ const format = asNonEmptyString2(params.format) ?? "ascii";
45795
46481
  const limit = Math.min(typeof params.limit === "number" ? params.limit : 5, 20);
45796
46482
  const traces = profiler.getRecentTraces(limit);
45797
46483
  const stats = profiler.getStats();
@@ -45862,7 +46548,7 @@ Returns: Performance trace data with timing breakdown`,
45862
46548
  "Profiling is disabled. Set profilingEnabled: true in your plugin config to enable."
45863
46549
  );
45864
46550
  }
45865
- const format = asNonEmptyString(params.format) ?? "ascii";
46551
+ const format = asNonEmptyString2(params.format) ?? "ascii";
45866
46552
  const limit = Math.min(typeof params.limit === "number" ? params.limit : 5, 20);
45867
46553
  const traces = profiler.getRecentTraces(limit);
45868
46554
  const stats = profiler.getStats();
@@ -55320,10 +56006,15 @@ var EngramAccessService = class {
55320
56006
  sessionKey: lcmSessionKey,
55321
56007
  role: m.role,
55322
56008
  content: m.content,
56009
+ parts: m.parts,
56010
+ rawContent: m.rawContent,
56011
+ sourceFormat: m.sourceFormat,
55323
56012
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
55324
56013
  }));
55325
56014
  try {
55326
- const extractionPromise = this.orchestrator.ingestReplayBatch(turns);
56015
+ const extractionPromise = this.orchestrator.ingestReplayBatch(turns, {
56016
+ archiveLcm: false
56017
+ });
55327
56018
  extractionPromise.catch((err) => {
55328
56019
  log.error(`access-observe background extraction failed: ${err}`);
55329
56020
  });
@@ -56705,7 +57396,33 @@ var setCodingContextRequestSchema = external_exports.object({
56705
57396
  });
56706
57397
  var messageSchema = external_exports.object({
56707
57398
  role: external_exports.enum(["user", "assistant"]),
56708
- content: external_exports.string().min(1, "message content must be non-empty")
57399
+ content: external_exports.string().min(1, "message content must be non-empty"),
57400
+ sourceFormat: external_exports.enum(["openai", "anthropic", "openclaw", "lossless-claw", "remnic"]).nullable().optional(),
57401
+ rawContent: external_exports.unknown().nullable().optional(),
57402
+ parts: external_exports.array(
57403
+ external_exports.object({
57404
+ ordinal: external_exports.number().int().min(0).nullable().optional(),
57405
+ kind: external_exports.enum([
57406
+ "text",
57407
+ "tool_call",
57408
+ "tool_result",
57409
+ "patch",
57410
+ "file_read",
57411
+ "file_write",
57412
+ "step_start",
57413
+ "step_finish",
57414
+ "snapshot",
57415
+ "retry"
57416
+ ]),
57417
+ payload: external_exports.record(external_exports.string(), external_exports.unknown()),
57418
+ toolName: external_exports.string().nullable().optional(),
57419
+ tool_name: external_exports.string().nullable().optional(),
57420
+ filePath: external_exports.string().nullable().optional(),
57421
+ file_path: external_exports.string().nullable().optional(),
57422
+ createdAt: external_exports.string().nullable().optional(),
57423
+ created_at: external_exports.string().nullable().optional()
57424
+ })
57425
+ ).nullable().optional()
56709
57426
  });
56710
57427
  var observeRequestSchema = external_exports.object({
56711
57428
  sessionKey: external_exports.string().trim().min(1, "sessionKey is required").max(512),
@@ -57351,9 +58068,51 @@ var EngramMcpServer = class {
57351
58068
  type: "object",
57352
58069
  properties: {
57353
58070
  role: { type: "string", enum: ["user", "assistant"] },
57354
- content: { type: "string" }
58071
+ content: { type: "string" },
58072
+ sourceFormat: {
58073
+ type: "string",
58074
+ enum: ["openai", "anthropic", "openclaw", "lossless-claw", "remnic"]
58075
+ },
58076
+ rawContent: {
58077
+ description: "Optional native provider content blocks for structured message-part capture."
58078
+ },
58079
+ parts: {
58080
+ type: "array",
58081
+ description: "Optional normalized Remnic LCM message parts.",
58082
+ items: {
58083
+ type: "object",
58084
+ properties: {
58085
+ ordinal: { type: ["number", "null"], minimum: 0 },
58086
+ kind: {
58087
+ type: "string",
58088
+ enum: [
58089
+ "text",
58090
+ "tool_call",
58091
+ "tool_result",
58092
+ "patch",
58093
+ "file_read",
58094
+ "file_write",
58095
+ "step_start",
58096
+ "step_finish",
58097
+ "snapshot",
58098
+ "retry"
58099
+ ]
58100
+ },
58101
+ payload: { type: "object", additionalProperties: true },
58102
+ toolName: { type: ["string", "null"] },
58103
+ tool_name: { type: ["string", "null"] },
58104
+ filePath: { type: ["string", "null"] },
58105
+ file_path: { type: ["string", "null"] },
58106
+ createdAt: { type: ["string", "null"] },
58107
+ created_at: { type: ["string", "null"] }
58108
+ },
58109
+ required: ["kind", "payload"],
58110
+ additionalProperties: false
58111
+ }
58112
+ }
57355
58113
  },
57356
- required: ["role", "content"]
58114
+ required: ["role", "content"],
58115
+ additionalProperties: false
57357
58116
  },
57358
58117
  description: "Conversation messages to observe"
57359
58118
  },
@@ -58694,20 +59453,21 @@ ${body}`;
58694
59453
  effectivePrincipal
58695
59454
  );
58696
59455
  case "engram.observe": {
58697
- if ("cwd" in args && args.cwd !== void 0 && args.cwd !== null && typeof args.cwd !== "string") {
58698
- throw new EngramAccessInputError("cwd must be a string");
58699
- }
58700
- if ("projectTag" in args && args.projectTag !== void 0 && args.projectTag !== null && typeof args.projectTag !== "string") {
58701
- throw new EngramAccessInputError("projectTag must be a string");
58702
- }
59456
+ const body = parseMcpRequest("observe", args);
58703
59457
  return this.service.observe({
58704
- sessionKey: typeof args.sessionKey === "string" ? args.sessionKey : "",
58705
- messages: Array.isArray(args.messages) ? args.messages : [],
58706
- namespace: typeof args.namespace === "string" ? args.namespace : void 0,
59458
+ sessionKey: body.sessionKey,
59459
+ messages: body.messages.map((message) => ({
59460
+ role: message.role,
59461
+ content: message.content,
59462
+ parts: message.parts ?? void 0,
59463
+ rawContent: message.rawContent ?? void 0,
59464
+ sourceFormat: message.sourceFormat ?? void 0
59465
+ })),
59466
+ namespace: body.namespace,
58707
59467
  authenticatedPrincipal: effectivePrincipal,
58708
- skipExtraction: args.skipExtraction === true,
58709
- cwd: typeof args.cwd === "string" ? args.cwd : void 0,
58710
- projectTag: typeof args.projectTag === "string" ? args.projectTag : void 0
59468
+ skipExtraction: body.skipExtraction === true,
59469
+ cwd: body.cwd,
59470
+ projectTag: body.projectTag
58711
59471
  });
58712
59472
  }
58713
59473
  case "engram.lcm_search":
@@ -59886,7 +60646,13 @@ var EngramAccessHttpServer = class {
59886
60646
  this.ensureWriteRateLimitAvailable();
59887
60647
  const response = await this.service.observe({
59888
60648
  sessionKey: body.sessionKey,
59889
- messages: body.messages,
60649
+ messages: body.messages.map((message) => ({
60650
+ role: message.role,
60651
+ content: message.content,
60652
+ sourceFormat: message.sourceFormat ?? void 0,
60653
+ rawContent: message.rawContent ?? void 0,
60654
+ parts: message.parts ?? void 0
60655
+ })),
59890
60656
  namespace: this.resolveNamespace(req, body.namespace),
59891
60657
  authenticatedPrincipal: this.resolveRequestPrincipal(req),
59892
60658
  skipExtraction: body.skipExtraction === true,
@@ -67824,7 +68590,7 @@ import crypto2 from "crypto";
67824
68590
  function hashSha256(value) {
67825
68591
  return crypto2.createHash("sha256").update(value).digest("hex");
67826
68592
  }
67827
- function isRecord3(value) {
68593
+ function isRecord4(value) {
67828
68594
  return typeof value === "object" && value !== null && !Array.isArray(value);
67829
68595
  }
67830
68596
  function optionalString2(value) {
@@ -67840,11 +68606,11 @@ function normalizedToolName(toolName) {
67840
68606
  return toolNameTokens(toolName).join("_");
67841
68607
  }
67842
68608
  function parseToolArguments(value) {
67843
- if (isRecord3(value)) return value;
68609
+ if (isRecord4(value)) return value;
67844
68610
  if (typeof value !== "string") return void 0;
67845
68611
  try {
67846
68612
  const parsed = JSON.parse(value);
67847
- return isRecord3(parsed) ? parsed : void 0;
68613
+ return isRecord4(parsed) ? parsed : void 0;
67848
68614
  } catch {
67849
68615
  return void 0;
67850
68616
  }
@@ -67854,13 +68620,13 @@ function extractTextContent(value) {
67854
68620
  if (Array.isArray(value)) {
67855
68621
  return value.map((block) => {
67856
68622
  if (typeof block === "string") return block.trim();
67857
- if (isRecord3(block) && block.type === "text" && typeof block.text === "string") {
68623
+ if (isRecord4(block) && block.type === "text" && typeof block.text === "string") {
67858
68624
  return block.text.trim();
67859
68625
  }
67860
68626
  return "";
67861
68627
  }).filter((item) => item.length > 0).join("\n");
67862
68628
  }
67863
- if (isRecord3(value)) {
68629
+ if (isRecord4(value)) {
67864
68630
  return JSON.stringify(value);
67865
68631
  }
67866
68632
  return "";
@@ -67887,10 +68653,10 @@ function getToolCallContexts(messages) {
67887
68653
  const toolCalls = message.tool_calls ?? message.toolCalls;
67888
68654
  if (!Array.isArray(toolCalls)) continue;
67889
68655
  for (const call of toolCalls) {
67890
- if (!isRecord3(call)) continue;
68656
+ if (!isRecord4(call)) continue;
67891
68657
  const toolCallId = optionalString2(call.id) ?? optionalString2(call.toolCallId);
67892
68658
  if (!toolCallId) continue;
67893
- const fn = isRecord3(call.function) ? call.function : void 0;
68659
+ const fn = isRecord4(call.function) ? call.function : void 0;
67894
68660
  const toolName = optionalString2(fn?.name) ?? optionalString2(call.name);
67895
68661
  const args = parseToolArguments(fn?.arguments) ?? parseToolArguments(call.arguments) ?? parseToolArguments(call.args) ?? parseToolArguments(call.input);
67896
68662
  contexts.set(toolCallId, { toolCallId, toolName, args });
@@ -67933,7 +68699,7 @@ function fileContentHash(args) {
67933
68699
  }
67934
68700
  function inferOutcome(message, parsedPayload) {
67935
68701
  if (message.isError === true) return "failure";
67936
- if (isRecord3(parsedPayload)) {
68702
+ if (isRecord4(parsedPayload)) {
67937
68703
  if (parsedPayload.partial === true || parsedPayload.status === "partial") return "partial";
67938
68704
  if (parsedPayload.success === false || parsedPayload.ok === false) return "failure";
67939
68705
  if (parsedPayload.success === true || parsedPayload.ok === true) return "success";
@@ -72525,7 +73291,12 @@ Keep the reflection grounded in the evidence below.
72525
73291
  const lcmMessages = lastTurn.map((msg) => {
72526
73292
  const rawRole = typeof msg.role === "string" ? msg.role : "";
72527
73293
  const content = extractTextContent2(msg);
72528
- return { role: rawRole, content };
73294
+ return {
73295
+ role: rawRole,
73296
+ content,
73297
+ rawContent: msg,
73298
+ sourceFormat: "openclaw"
73299
+ };
72529
73300
  }).filter((m) => m.content.length > 0);
72530
73301
  if (lcmMessages.length > 0) {
72531
73302
  orchestrator.lcmEngine.enqueueObserveMessages(
@@ -4260,6 +4260,16 @@
4260
4260
  "default": 90,
4261
4261
  "description": "Days to retain archived LCM summaries"
4262
4262
  },
4263
+ "messagePartsEnabled": {
4264
+ "type": "boolean",
4265
+ "default": false,
4266
+ "description": "Opt in to structured LCM message-part capture for tool calls, file references, patches, and reasoning markers."
4267
+ },
4268
+ "messagePartsRecallMaxResults": {
4269
+ "type": "number",
4270
+ "default": 6,
4271
+ "description": "Maximum structured message-part matches to inject into recall when messagePartsEnabled is true."
4272
+ },
4263
4273
  "ircEnabled": {
4264
4274
  "type": "boolean",
4265
4275
  "default": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/plugin-openclaw",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "OpenClaw adapter for Remnic memory — thin wrapper delegating to @remnic/core",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "openai": "^6.0.0",
28
- "@remnic/core": "^1.1.4"
28
+ "@remnic/core": "^1.1.5"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "openclaw": ">=2026.4.8"