@retaw106/mmc-pi 2.6.0 → 2.7.1

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.
Files changed (2) hide show
  1. package/dist/index.js +218 -48
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13915,6 +13915,39 @@ function keywordRelevance(query, content) {
13915
13915
  const words = new Set(terms(cleanSearchText(content)));
13916
13916
  return 0.8 * needles.filter((term) => words.has(term)).length / needles.length;
13917
13917
  }
13918
+ var CJK_RUN = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u;
13919
+ var CJK_RUN_SPLIT = /([\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+)/u;
13920
+ function hintTerms(text) {
13921
+ return [
13922
+ ...new Set(terms(text).flatMap((term) => term.split(CJK_RUN_SPLIT)).filter((term) => term.length > 0 && !QUERY_STOP_WORDS.has(term)))
13923
+ ];
13924
+ }
13925
+ function characterBigrams(run) {
13926
+ const characters = [...run];
13927
+ return characters.length === 1 ? characters : characters.slice(1).map((character, index) => characters[index] + character);
13928
+ }
13929
+ function hintKeywordRelevance(query, content) {
13930
+ const needles = hintTerms(query);
13931
+ if (needles.length === 0)
13932
+ return 0;
13933
+ const words = new Set(hintTerms(cleanSearchText(content)));
13934
+ const runs = [...words].filter((word) => CJK_RUN.test(word));
13935
+ const bigrams = new Set(runs.flatMap(characterBigrams));
13936
+ let matched = 0;
13937
+ for (const needle of needles) {
13938
+ if (!CJK_RUN.test(needle)) {
13939
+ matched += Number(words.has(needle));
13940
+ continue;
13941
+ }
13942
+ const grams = characterBigrams(needle);
13943
+ if ([...needle].length === 1) {
13944
+ matched += Number(runs.some((run) => run.includes(needle)));
13945
+ continue;
13946
+ }
13947
+ matched += Math.min(1, grams.filter((gram) => bigrams.has(gram)).length / Math.ceil(grams.length / 2));
13948
+ }
13949
+ return 0.8 * matched / needles.length;
13950
+ }
13918
13951
  function matchedSearchSnippet(content, query, cap = 220) {
13919
13952
  const text = cleanSearchText(content);
13920
13953
  const needles = new Set(terms(query));
@@ -21080,7 +21113,7 @@ function registerStandalonePluginMarker(version) {
21080
21113
  // package.json
21081
21114
  var package_default = {
21082
21115
  name: "@retaw106/mmc-pi",
21083
- version: "2.6.0",
21116
+ version: "2.7.1",
21084
21117
  type: "module",
21085
21118
  description: "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
21086
21119
  main: "dist/index.js",
@@ -29681,6 +29714,38 @@ function buildToolTarget(part, message, tagId) {
29681
29714
  };
29682
29715
  }
29683
29716
 
29717
+ // ../plugin/src/features/magic-context/auto-search-diagnostics.ts
29718
+ var memoryHintContent = new WeakMap;
29719
+ function withMemoryHintContent(result, fullContent) {
29720
+ memoryHintContent.set(result, fullContent);
29721
+ return result;
29722
+ }
29723
+ function createAutoSearchDiagnostics(threshold) {
29724
+ return {
29725
+ threshold,
29726
+ lanes: {},
29727
+ pool: "unknown",
29728
+ poolRows: null,
29729
+ peek: "not-run",
29730
+ workerStarted: false
29731
+ };
29732
+ }
29733
+ function autoSearchRelevance(query, result) {
29734
+ const content = result.source === "memory" ? memoryHintContent.get(result) ?? result.content : result.content;
29735
+ if (cleanSearchText(content).length === 0)
29736
+ return 0;
29737
+ return result.source === "message" || "matchType" in result ? hintKeywordRelevance(query, content) : result.score;
29738
+ }
29739
+ function recordAutoSearchLane(diagnostics, lane, query, results) {
29740
+ if (!diagnostics)
29741
+ return;
29742
+ const scores = results.map((result) => result.relevanceScore ?? autoSearchRelevance(query, result));
29743
+ diagnostics.lanes[lane] = {
29744
+ top: scores.length ? Math.max(...scores) : null,
29745
+ nearMisses: scores.filter((score) => score >= 0.5 && score < diagnostics.threshold).length
29746
+ };
29747
+ }
29748
+
29684
29749
  // ../plugin/src/features/magic-context/literal-probes.ts
29685
29750
  var MAX_PROBES = 5;
29686
29751
  var MIN_PROBE_LENGTH = 3;
@@ -30307,7 +30372,7 @@ function mergeMemoryResults(args) {
30307
30372
  suppressedVisibleIds.push(id);
30308
30373
  continue;
30309
30374
  }
30310
- results.push({
30375
+ results.push(withMemoryHintContent({
30311
30376
  source: "memory",
30312
30377
  content: previewText(memory.content),
30313
30378
  score,
@@ -30315,7 +30380,7 @@ function mergeMemoryResults(args) {
30315
30380
  category: memory.category,
30316
30381
  matchType: "fts",
30317
30382
  sourceName: args.sourceNameByMemoryId?.get(memory.id)
30318
- });
30383
+ }, memory.content));
30319
30384
  }
30320
30385
  return {
30321
30386
  results: results.sort((left, right) => {
@@ -30912,8 +30977,12 @@ async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
30912
30977
  options.diagnostics.suppressedLiveMessageMatches = outcome.suppressedLiveMessageMatches;
30913
30978
  }
30914
30979
  messageResults = outcome.results.filter((result) => result.messageId !== options.excludeMessageId);
30980
+ recordAutoSearchLane(options.autoSearchDiagnostics, "messages", trimmedQuery, messageResults);
30981
+ options.onAutoSearchDiagnostics?.();
30915
30982
  }
30916
30983
  const workspace = resolveSearchWorkspaceContext(db, projectPath);
30984
+ if (options.autoSearchDiagnostics)
30985
+ options.autoSearchDiagnostics.pool = "off";
30917
30986
  const [memoryOutcome, primerResults, noteResults] = await Promise.all([
30918
30987
  runMemory ? searchMemories({
30919
30988
  db,
@@ -30948,6 +31017,10 @@ async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
30948
31017
  if (options.diagnostics) {
30949
31018
  options.diagnostics.suppressedVisibleMemoryIds = memoryOutcome.suppressedVisibleIds;
30950
31019
  }
31020
+ recordAutoSearchLane(options.autoSearchDiagnostics, "memory", trimmedQuery, memoryOutcome.results);
31021
+ recordAutoSearchLane(options.autoSearchDiagnostics, "primers", trimmedQuery, primerResults);
31022
+ recordAutoSearchLane(options.autoSearchDiagnostics, "notes", trimmedQuery, noteResults);
31023
+ options.onAutoSearchDiagnostics?.();
30951
31024
  const results = [...memoryOutcome.results, ...primerResults, ...messageResults, ...noteResults].sort(compareUnifiedResults).slice(0, limit);
30952
31025
  const countRetrievals = options.countRetrievals ?? true;
30953
31026
  if (countRetrievals) {
@@ -30969,7 +31042,7 @@ async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
30969
31042
  if (options.autoHint) {
30970
31043
  return results.map((result) => ({
30971
31044
  ...result,
30972
- relevanceScore: cleanSearchText(result.content).length === 0 ? 0 : keywordRelevance(trimmedQuery, result.content)
31045
+ relevanceScore: autoSearchRelevance(trimmedQuery, result)
30973
31046
  }));
30974
31047
  }
30975
31048
  return results;
@@ -31215,6 +31288,32 @@ function selectAutoSearchResults(results, threshold) {
31215
31288
  return results.filter((result) => relevance(result) >= threshold).sort((left, right) => relevance(right) - relevance(left));
31216
31289
  }
31217
31290
 
31291
+ // ../plugin/src/hooks/magic-context/auto-search-observation.ts
31292
+ var disabledTurns = new WeakMap;
31293
+ function logDisabledAutoSearch(db, sessionId, messageId, threshold, startedAt) {
31294
+ const sessions = disabledTurns.get(db) ?? new Map;
31295
+ disabledTurns.set(db, sessions);
31296
+ if (sessions.get(sessionId) === messageId)
31297
+ return;
31298
+ sessions.set(sessionId, messageId);
31299
+ observeAutoSearch(sessionId, threshold, startedAt).finish("disabled");
31300
+ }
31301
+ function observeAutoSearch(sessionId, threshold, startedAt) {
31302
+ const diagnostics = createAutoSearchDiagnostics(threshold);
31303
+ let logged = false;
31304
+ return {
31305
+ diagnostics,
31306
+ finish(outcome, reason) {
31307
+ if (logged)
31308
+ return;
31309
+ logged = true;
31310
+ const lanes = Object.entries(diagnostics.lanes).map(([lane, value]) => `${lane}:${value.top?.toFixed(3) ?? "none"}`).join(",");
31311
+ const nearMisses = Object.values(diagnostics.lanes).reduce((sum, lane) => sum + lane.nearMisses, 0);
31312
+ sessionLog(sessionId, `auto-search: decision outcome=${outcome}${reason ? ` reason=${reason}` : ""} lanes=${lanes || "none"} nearMisses=${nearMisses} threshold=${threshold} pool=${diagnostics.pool} rows=${diagnostics.poolRows ?? "unknown"} workerStarted=${diagnostics.workerStarted ?? "unknown"} peek=${diagnostics.peek} elapsedMs=${Math.round(performance.now() - startedAt)}`);
31313
+ }
31314
+ };
31315
+ }
31316
+
31218
31317
  // src/auto-search-pi.ts
31219
31318
  var DEFAULT_SCORE_THRESHOLD = 0.55;
31220
31319
  var DEFAULT_MIN_PROMPT_CHARS = 20;
@@ -31351,8 +31450,6 @@ async function runAutoSearchHintForPi(args) {
31351
31450
  const timestamp = message.timestamp;
31352
31451
  return `test-entry-${index}:${typeof timestamp === "number" ? timestamp : "no-ts"}`;
31353
31452
  }) : args.entryIds;
31354
- if (!options.enabled)
31355
- return messages;
31356
31453
  const strictResolutionFailed = entryIds === null;
31357
31454
  const effectiveEntryIds = strictResolutionFailed ? messages.map((message) => {
31358
31455
  const id = message.id;
@@ -31362,6 +31459,11 @@ async function runAutoSearchHintForPi(args) {
31362
31459
  if (found === null)
31363
31460
  return messages;
31364
31461
  const { message: userMsg, messageId: userMsgId } = found;
31462
+ if (!options.enabled) {
31463
+ if (found.index === messages.length - 1)
31464
+ logDisabledAutoSearch(db, sessionId, userMsgId, options.scoreThreshold ?? DEFAULT_SCORE_THRESHOLD, startedAt);
31465
+ return messages;
31466
+ }
31365
31467
  const replayServedDecision = () => {
31366
31468
  const decision = getAutoSearchHintDecisions(db, sessionId).find((entry) => entry.messageId === userMsgId);
31367
31469
  if (decision?.decision === "hint") {
@@ -31390,21 +31492,24 @@ async function runAutoSearchHintForPi(args) {
31390
31492
  return messages;
31391
31493
  if (!isUsableProjectIdentity(options.projectPath))
31392
31494
  return messages;
31495
+ const observation = observeAutoSearch(sessionId, options.scoreThreshold ?? DEFAULT_SCORE_THRESHOLD, startedAt);
31393
31496
  const writeNoHintAndReconcile = (reason) => {
31394
31497
  const outcome = appendAutoSearchHintDecision(db, sessionId, {
31395
31498
  messageId: userMsgId,
31396
31499
  decision: "no-hint",
31397
31500
  reason
31398
31501
  });
31399
- if (!outcome.ok)
31502
+ if (!outcome.ok) {
31503
+ observation.finish("error", "cas-exhausted");
31400
31504
  return;
31505
+ }
31401
31506
  if (outcome.kind === "already-present" && outcome.decision.decision === "hint") {
31402
31507
  appendHintToUserMessage(userMsg, outcome.decision.text);
31403
31508
  }
31509
+ observation.finish(outcome.decision.decision === "hint" ? "attached" : reason === "below-threshold" ? "below-threshold" : reason === "error" ? "error" : "no-candidates", reason);
31404
31510
  };
31405
31511
  const rawPartsText = collectUserPromptParts(userMsg);
31406
31512
  if (hasStackedAugmentation(rawPartsText)) {
31407
- sessionLog(sessionId, "auto-search: skipping — user message already carries augmentation/hint");
31408
31513
  writeNoHintAndReconcile("stacked");
31409
31514
  return messages;
31410
31515
  }
@@ -31417,6 +31522,7 @@ async function runAutoSearchHintForPi(args) {
31417
31522
  let results;
31418
31523
  try {
31419
31524
  const searchOptions = {
31525
+ autoSearchDiagnostics: observation.diagnostics,
31420
31526
  limit: 10,
31421
31527
  autoHint: true,
31422
31528
  maxMessageOrdinal: Math.max(0, getLastCompartmentEndMessage(db, sessionId)),
@@ -31432,13 +31538,13 @@ async function runAutoSearchHintForPi(args) {
31432
31538
  countRetrievals: false
31433
31539
  });
31434
31540
  }, startedAt);
31435
- } catch (error) {
31436
- log(`[auto-search] unified search failed for session ${sessionId} (will retry next pass): ${error instanceof Error ? error.message : String(error)}`);
31541
+ } catch {
31542
+ observation.finish("error");
31437
31543
  return messages;
31438
31544
  }
31439
31545
  if (results === null) {
31440
31546
  persistAutoSearchSkip(db, sessionId, userMsgId, "timeout");
31441
- sessionLog(sessionId, `auto-search: timed out after ${AUTO_SEARCH_TIMEOUT_MS}ms, skipping hint for this turn (frozen)`);
31547
+ observation.finish("timeout");
31442
31548
  return messages;
31443
31549
  }
31444
31550
  if (results.length === 0) {
@@ -31448,7 +31554,6 @@ async function runAutoSearchHintForPi(args) {
31448
31554
  const scoreThreshold = options.scoreThreshold ?? DEFAULT_SCORE_THRESHOLD;
31449
31555
  results = selectAutoSearchResults(results, scoreThreshold);
31450
31556
  if (results.length === 0) {
31451
- sessionLog(sessionId, `auto-search: no fragment meets relevance threshold ${scoreThreshold}`);
31452
31557
  writeNoHintAndReconcile("below-threshold");
31453
31558
  return messages;
31454
31559
  }
@@ -31468,12 +31573,14 @@ ${hintText}`;
31468
31573
  decision: "hint",
31469
31574
  text: payload
31470
31575
  });
31471
- if (!outcome.ok)
31576
+ if (!outcome.ok) {
31577
+ observation.finish("error", "cas-exhausted");
31472
31578
  return messages;
31579
+ }
31473
31580
  if (outcome.decision.decision === "hint") {
31474
31581
  appendHintToUserMessage(userMsg, outcome.decision.text);
31475
31582
  }
31476
- sessionLog(sessionId, `auto-search: attached hint to ${userMsgId} (${results.length} fragments, top score ${results[0].score.toFixed(3)})`);
31583
+ observation.finish(outcome.decision.decision === "hint" ? "attached" : "no-candidates");
31477
31584
  return messages;
31478
31585
  }, replayServedDecision));
31479
31586
  }
@@ -42489,16 +42596,6 @@ function registerPiGuardedContext(pi, handler, options = {}) {
42489
42596
  refused: false,
42490
42597
  ended: false
42491
42598
  });
42492
- const lifecycle = new Map;
42493
- receiptMaps.add(lifecycle);
42494
- const lifecycleCounts = (key) => {
42495
- let counts = lifecycle.get(key);
42496
- if (!counts) {
42497
- counts = { starts: 0, ends: 0 };
42498
- lifecycle.set(key, counts);
42499
- }
42500
- return counts;
42501
- };
42502
42599
  const fenceEnabled = resolvePiHarnessKind() === "omp";
42503
42600
  const unsendableNonces = new Map;
42504
42601
  receiptMaps.add(unsendableNonces);
@@ -59480,9 +59577,9 @@ var FULL_PARAMETER_DESCRIPTIONS = {
59480
59577
  },
59481
59578
  mmc_memory: {
59482
59579
  action: "write | update | archive | merge | get",
59483
- content: "The memory text — one standalone fact (write, update, merge).",
59484
- category: "Kind of fact (required for write; on update/merge, pass the memory's current category to keep it).",
59485
- ids: "Memory ids from <project-memory>: one for update, one or more for archive, two or more for merge, 1–20 for get.",
59580
+ content: "The memory text — one standalone fact (write, update, merge). Archive and get take an empty string.",
59581
+ category: "Kind of fact: the new fact's kind for write; on update/merge, the memory's current category to keep it. Archive and get ignore it.",
59582
+ ids: "Memory ids from <project-memory>: one for update, one or more for archive, two or more for merge, 1–20 for get. Write takes an empty list.",
59486
59583
  reason: "Why it is being archived (optional)."
59487
59584
  },
59488
59585
  mmc_search: {
@@ -59514,9 +59611,9 @@ var LIGHT_PARAMETER_DESCRIPTIONS = {
59514
59611
  },
59515
59612
  mmc_memory: {
59516
59613
  action: "write | update | archive | merge | get",
59517
- content: "One standalone fact (write, update, merge).",
59518
- category: "Kind of fact (required for write; on update/merge pass the current category to keep it).",
59519
- ids: "Ids from <project-memory>: one for update, 1+ for archive, 2+ for merge, 1–20 for get.",
59614
+ content: "One standalone fact (write, update, merge). Archive and get: empty string.",
59615
+ category: "Kind of fact (write; on update/merge pass the current category to keep it). Archive and get ignore it.",
59616
+ ids: "Ids from <project-memory>: one for update, 1+ for archive, 2+ for merge, 1–20 for get. Write: empty list.",
59520
59617
  reason: "Why it is archived (optional)."
59521
59618
  },
59522
59619
  mmc_search: {
@@ -59631,6 +59728,17 @@ function resolveCtxExpandMode(args, domain) {
59631
59728
 
59632
59729
  // ../plugin/src/tools/ctx-expand/render.ts
59633
59730
  import { createHash as createHash25 } from "node:crypto";
59731
+ function recoveryParts(sessionId, message, attachments) {
59732
+ return attachments ? [...message.parts, ...attachments(sessionId, message.id)] : message.parts;
59733
+ }
59734
+ function attachmentNotice(part) {
59735
+ const field = (value) => typeof value === "string" ? value.trim().replace(/[\r\n]+/g, " ") : "";
59736
+ const source = isRecord7(part.source) ? part.source : {};
59737
+ const location = [part.url, part.path, source.uri, source.url, source.path].map(field).find((value) => value && !/^(data|blob):/i.test(value));
59738
+ const name = field(part.filename) || field(part.name);
59739
+ const mime = field(part.mime) || field(part.mimeType) || field(part.mediaType);
59740
+ return `[file]${name ? ` ${name}` : ""}${mime ? ` (${mime})` : ""}` + `${location ? ` — stored location: ${location}` : ""}` + "; content not recovered. If needed, ask the user to attach it again.";
59741
+ }
59634
59742
  function isRecord7(value) {
59635
59743
  return value !== null && typeof value === "object" && !Array.isArray(value);
59636
59744
  }
@@ -59737,8 +59845,8 @@ function renderPartPreview(part, expandTools) {
59737
59845
  if (reasoning !== null)
59738
59846
  return ` • [reasoning] ${truncate2(reasoning, 120)}`;
59739
59847
  const type = typeof part.type === "string" ? part.type : "part";
59740
- if (type === "file")
59741
- return " • [file]";
59848
+ if (type === "file" || type === "image")
59849
+ return ` • ${attachmentNotice(part)}`;
59742
59850
  if (type === "step-start" || type === "step-finish")
59743
59851
  return null;
59744
59852
  return ` • [${type}]`;
@@ -59768,13 +59876,11 @@ ${tool.output}`);
59768
59876
  `);
59769
59877
  }
59770
59878
  const type = typeof part.type === "string" ? part.type : "part";
59771
- if (type === "file") {
59772
- const name = typeof part.filename === "string" && part.filename || typeof part.url === "string" && part.url || "";
59773
- return ` [file]${name ? ` ${name}` : ""}`;
59774
- }
59879
+ if (type === "file" || type === "image")
59880
+ return ` ${attachmentNotice(part)}`;
59775
59881
  return null;
59776
59882
  }
59777
- function renderMessageByOrdinal(sessionId, ordinal) {
59883
+ function renderMessageByOrdinal(sessionId, ordinal, attachments) {
59778
59884
  const found = [];
59779
59885
  visitRawSessionMessages2(sessionId, ordinal, ordinal, (m) => {
59780
59886
  if (m.ordinal === ordinal)
@@ -59785,7 +59891,7 @@ function renderMessageByOrdinal(sessionId, ordinal) {
59785
59891
  if (!msg) {
59786
59892
  return `No message at ordinal ${ordinal} is currently in this session's stored history — ` + `it may have been removed by a session prune/revert, or the ordinal may be wrong. ` + `Re-run the tool if you still need the data.`;
59787
59893
  }
59788
- const rendered = msg.parts.map(renderPartFull).filter((l) => l !== null);
59894
+ const rendered = recoveryParts(sessionId, msg, attachments).map(renderPartFull).filter((l) => l !== null);
59789
59895
  const lines = [`[${msg.ordinal}] ${roleLabel(msg.role)} — full recovery:`, ""];
59790
59896
  if (rendered.length === 0) {
59791
59897
  lines.push(" (no recoverable content — message had only structural/reasoning parts)");
@@ -59795,14 +59901,14 @@ function renderMessageByOrdinal(sessionId, ordinal) {
59795
59901
  return lines.join(`
59796
59902
  `);
59797
59903
  }
59798
- function renderVerboseRange(sessionId, start, end, tokenBudget, expandTools) {
59904
+ function renderVerboseRange(sessionId, start, end, tokenBudget, expandTools, attachments) {
59799
59905
  const out = [];
59800
59906
  let usedTokens = 0;
59801
59907
  let lastOrdinal = start - 1;
59802
59908
  let truncated = false;
59803
59909
  visitRawSessionMessages2(sessionId, start, end, (msg) => {
59804
59910
  const header = `[${msg.ordinal}] ${verboseRoleLabel(msg)}`;
59805
- const partLines = msg.parts.map((part) => renderPartPreview(part, expandTools)).filter((l) => l !== null);
59911
+ const partLines = recoveryParts(sessionId, msg, attachments).map((part) => renderPartPreview(part, expandTools)).filter((l) => l !== null);
59806
59912
  const block = partLines.length > 0 ? `${header}
59807
59913
  ${partLines.join(`
59808
59914
  `)}` : header;
@@ -59820,7 +59926,7 @@ ${partLines.join(`
59820
59926
 
59821
59927
  `), lastOrdinal, truncated };
59822
59928
  }
59823
- function renderItemByTag(db, sessionId, number, textIndexDomain = "part") {
59929
+ function renderItemByTag(db, sessionId, number, textIndexDomain = "part", attachments) {
59824
59930
  const tag = getTagById(db, sessionId, number);
59825
59931
  if (!tag)
59826
59932
  return `no tag ${number} in this session; if ${number} came from a <session-history> heading or a ctx_search hit, it is an ordinal: use message=${number}`;
@@ -59845,17 +59951,19 @@ function renderItemByTag(db, sessionId, number, textIndexDomain = "part") {
59845
59951
  return rendered.length ? rendered.join(`
59846
59952
  `) : `Tag ${number}'s original tool call is no longer in stored history.`;
59847
59953
  }
59848
- const scoped = /^(.*):p(\d+)$/.exec(tag.messageId);
59954
+ const scoped = /^(.*):(?:p|file)(\d+)$/.exec(tag.messageId);
59849
59955
  const derived = /^(.*):mc-text-v1:([a-f0-9]+):([a-f0-9]+):o(\d+)$/.exec(tag.messageId);
59850
59956
  const owner = scoped?.[1] ?? derived?.[1] ?? tag.messageId;
59851
59957
  const message = readRawSessionMessageById2(sessionId, owner);
59852
59958
  if (!message)
59853
59959
  return `Tag ${number}'s original text is no longer in stored history.`;
59854
59960
  const index = scoped ? Number(scoped[2]) : 0;
59855
- const piText = message.parts.filter((part) => isRecord7(part) && part.type === "text");
59856
- const part = message.parts[index];
59961
+ const attached = attachments?.(sessionId, message.id) ?? [];
59962
+ const parts = [...message.parts, ...attached];
59963
+ const piText = parts.filter((part) => isRecord7(part) && part.type === "text");
59964
+ const part = tag.type === "file" ? attached.find((part) => isRecord7(part) && part.partIndex === index) ?? message.parts[index] : parts[index];
59857
59965
  const matching = derived ? piText.filter((part) => isRecord7(part) && typeof part.text === "string" && createHash25("sha256").update(part.text).digest("hex") === derived[3]) : [];
59858
- const selected = derived ? matching[Number(derived[4])] : textIndexDomain === "text" ? piText[index] : part;
59966
+ const selected = derived ? matching[Number(derived[4])] : textIndexDomain === "text" && tag.type !== "file" ? piText[index] : part;
59859
59967
  if (!selected)
59860
59968
  return `Tag ${number}'s original text part is no longer in stored history.`;
59861
59969
  const renderedPart = renderPartFull(selected);
@@ -60298,6 +60406,34 @@ var integerKeyPattern = new RegExp("^(?:0|[1-9][0-9]*)$");
60298
60406
 
60299
60407
  // ../../node_modules/.bun/typebox@1.3.27/node_modules/typebox/build/type/engine/indexed/from_object.mjs
60300
60408
  var NumericKeyPattern = new RegExp(IntegerKey);
60409
+ // src/tools/expand-attachments.ts
60410
+ function piRecoveryAttachments(entries) {
60411
+ return (_sessionId, messageId) => {
60412
+ const entry = entries.find((entry) => entry !== null && typeof entry === "object" && entry.id === messageId);
60413
+ if (entry?.message?.role !== "user" || !Array.isArray(entry.message.content))
60414
+ return [];
60415
+ return entry.message.content.flatMap((part, partIndex) => {
60416
+ if (!part || part.type !== "image" && part.type !== "file")
60417
+ return [];
60418
+ return [
60419
+ {
60420
+ type: part.type,
60421
+ filename: part.filename ?? part.name,
60422
+ mime: part.mimeType ?? part.mime,
60423
+ url: [
60424
+ part.url,
60425
+ part.source?.uri,
60426
+ part.source?.url,
60427
+ part.providerFile?.uri
60428
+ ].find((value) => typeof value === "string" && value.trim() && !/^(data|blob):/i.test(value.trim())),
60429
+ path: part.path ?? part.source?.path,
60430
+ partIndex
60431
+ }
60432
+ ];
60433
+ });
60434
+ };
60435
+ }
60436
+
60301
60437
  // src/tools/ctx-expand.ts
60302
60438
  var ParamsSchema = _Object_({
60303
60439
  tag: Optional(Union([Number2(), String2()], {
@@ -60347,12 +60483,13 @@ function createCtxExpandTool(deps) {
60347
60483
  readMessages: () => readPiSessionMessages(ctx, piRawOrdinalOffsetSource(deps.db, sessionId))
60348
60484
  });
60349
60485
  try {
60486
+ const attachments = piRecoveryAttachments(ctx.sessionManager.getBranch());
60350
60487
  const mode = resolveCtxExpandMode(params, "positive");
60351
60488
  if (mode.kind === "error") {
60352
60489
  return err(mode.message);
60353
60490
  }
60354
60491
  if (mode.kind === "tag") {
60355
- return ok(renderItemByTag(deps.db, sessionId, mode.tag, "text"));
60492
+ return ok(renderItemByTag(deps.db, sessionId, mode.tag, "text", attachments));
60356
60493
  }
60357
60494
  const alignment = resolvePiOrdinalAlignmentForContext(deps.db, sessionId, ctx);
60358
60495
  if (isPiOrdinalAlignmentUnanchored(alignment)) {
@@ -60370,7 +60507,7 @@ function createCtxExpandTool(deps) {
60370
60507
  if (mode.message <= lostThrough) {
60371
60508
  return ok(`No message at ordinal ${mode.message}: it is no longer in this session's history, which now starts at message ${firstAvailable}.`);
60372
60509
  }
60373
- return ok(renderMessageByOrdinal(sessionId, mode.message));
60510
+ return ok(renderMessageByOrdinal(sessionId, mode.message, attachments));
60374
60511
  }
60375
60512
  const { end, verbose } = mode;
60376
60513
  const requestedStart = mode.start;
@@ -60390,7 +60527,7 @@ function createCtxExpandTool(deps) {
60390
60527
 
60391
60528
  ` : "";
60392
60529
  if (verbose) {
60393
- const v = renderVerboseRange(sessionId, start, effectiveEnd, CTX_EXPAND_TOKEN_BUDGET, deps.expandTools);
60530
+ const v = renderVerboseRange(sessionId, start, effectiveEnd, CTX_EXPAND_TOKEN_BUDGET, deps.expandTools, attachments);
60394
60531
  if (!v.text) {
60395
60532
  return ok(`No messages found in range ${start}-${effectiveEnd}. The range may be outside this session's history.`);
60396
60533
  }
@@ -60480,6 +60617,20 @@ var ParamsShape = {
60480
60617
  var PrimaryParamsSchema = _Object_(ParamsShape, {
60481
60618
  additionalProperties: true
60482
60619
  });
60620
+ function rejectInvalidMemoryArguments(args) {
60621
+ if (args === null || typeof args !== "object")
60622
+ return;
60623
+ const record = args;
60624
+ if (record.content !== undefined && typeof record.content !== "string") {
60625
+ throw new Error("Error: 'content' must be a string.");
60626
+ }
60627
+ if (record.reason !== undefined && typeof record.reason !== "string") {
60628
+ throw new Error("Error: 'reason' must be a string.");
60629
+ }
60630
+ if (record.ids !== undefined && (!Array.isArray(record.ids) || record.ids.some((id) => typeof id !== "number" || !Number.isSafeInteger(id) || id < 1))) {
60631
+ throw new Error("Error: 'ids' must be an array of positive memory IDs.");
60632
+ }
60633
+ }
60483
60634
  function ok2(text) {
60484
60635
  return { content: [{ type: "text", text }], details: undefined };
60485
60636
  }
@@ -60578,6 +60729,10 @@ function createCtxMemoryTool(deps) {
60578
60729
  label: "Magic Context: Memory",
60579
60730
  description: CTX_MEMORY_DESCRIPTION,
60580
60731
  parameters: PrimaryParamsSchema,
60732
+ prepareArguments(args) {
60733
+ rejectInvalidMemoryArguments(args);
60734
+ return args;
60735
+ },
60581
60736
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
60582
60737
  params = unwrapImitatedReducedArgs(params, ["action"], {
60583
60738
  action: { type: "enum", values: PRIMARY_ACTIONS },
@@ -60965,6 +61120,17 @@ var ParamsSchema2 = _Object_({
60965
61120
  description: "Skip this many newest rows (default 0)."
60966
61121
  }))
60967
61122
  }, { additionalProperties: true });
61123
+ function rejectInvalidNoteArguments(args) {
61124
+ if (args === null || typeof args !== "object")
61125
+ return;
61126
+ const record = args;
61127
+ if (record.content !== undefined && typeof record.content !== "string") {
61128
+ throw new Error("Error: 'content' must be a string.");
61129
+ }
61130
+ if (record.note_ids !== undefined && (!Array.isArray(record.note_ids) || record.note_ids.some((id) => typeof id !== "number" || !Number.isSafeInteger(id) || id < 1))) {
61131
+ throw new Error("Error: 'note_ids' must be an array of positive note IDs.");
61132
+ }
61133
+ }
60968
61134
  function ok3(text) {
60969
61135
  return { content: [{ type: "text", text }], details: undefined };
60970
61136
  }
@@ -61021,6 +61187,10 @@ function createCtxNoteTool(deps) {
61021
61187
  label: "Magic Context: Notes",
61022
61188
  description: CTX_NOTE_DESCRIPTION,
61023
61189
  parameters: ParamsSchema2,
61190
+ prepareArguments(args) {
61191
+ rejectInvalidNoteArguments(args);
61192
+ return args;
61193
+ },
61024
61194
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
61025
61195
  params = unwrapImitatedReducedArgs(params, ["action", "content"], {
61026
61196
  action: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retaw106/mmc-pi",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "type": "module",
5
5
  "description": "Pi and OMP coding agent extension for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",