@cortexkit/pi-magic-context 0.21.0 → 0.21.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.
package/dist/index.js CHANGED
@@ -143698,6 +143698,13 @@ function getDefaultHistorianFailureState() {
143698
143698
  lastFailureAt: null
143699
143699
  };
143700
143700
  }
143701
+ function setPersistedReasoningWatermark(db, sessionId, tagNumber) {
143702
+ ensureSessionMetaRow(db, sessionId);
143703
+ db.prepare("UPDATE session_meta SET cleared_reasoning_through_tag = ? WHERE session_id = ?").run(tagNumber, sessionId);
143704
+ }
143705
+ function clearPersistedReasoningWatermark(db, sessionId) {
143706
+ setPersistedReasoningWatermark(db, sessionId, 0);
143707
+ }
143701
143708
  function getPersistedStickyTurnReminder(db, sessionId) {
143702
143709
  const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
143703
143710
  if (!isPersistedStickyTurnReminderRow(result)) {
@@ -143939,6 +143946,12 @@ function clearEmergencyRecovery(db, sessionId) {
143939
143946
  db.prepare("UPDATE session_meta SET needs_emergency_recovery = 0 WHERE session_id = ?").run(sessionId);
143940
143947
  })();
143941
143948
  }
143949
+ function clearDetectedContextLimit(db, sessionId) {
143950
+ db.transaction(() => {
143951
+ ensureSessionMetaRow(db, sessionId);
143952
+ db.prepare("UPDATE session_meta SET detected_context_limit = 0 WHERE session_id = ?").run(sessionId);
143953
+ })();
143954
+ }
143942
143955
  function getPersistedCompactionMarkerState(db, sessionId) {
143943
143956
  const row = db.prepare("SELECT compaction_marker_state FROM session_meta WHERE session_id = ?").get(sessionId);
143944
143957
  const raw = row?.compaction_marker_state;
@@ -145455,10 +145468,10 @@ class PiSubagentRunner {
145455
145468
  });
145456
145469
  return;
145457
145470
  }
145458
- if (finalStopReason === "error" || finalStopReason === "aborted") {
145471
+ if (finalStopReason === "error" || finalStopReason === "aborted" || finalStopReason === "length") {
145459
145472
  settle({
145460
145473
  ok: false,
145461
- reason: "model_failed",
145474
+ reason: finalStopReason === "length" ? "truncated" : "model_failed",
145462
145475
  error: finalErrorMessage ?? `pi assistant stopped with reason "${finalStopReason}"`,
145463
145476
  durationMs: Date.now() - startTime,
145464
145477
  meta: { stderr: stderr.length > 0 ? stderr : undefined }
@@ -145580,10 +145593,18 @@ function parsePiEventLine(line) {
145580
145593
  }
145581
145594
  }
145582
145595
  function terminateChild(child) {
145596
+ let exited = false;
145597
+ child.once("close", () => {
145598
+ exited = true;
145599
+ });
145600
+ child.once("exit", () => {
145601
+ exited = true;
145602
+ });
145583
145603
  child.kill("SIGTERM");
145584
145604
  const killHandle = setTimeout(() => {
145585
- if (!child.killed)
145605
+ if (!exited && child.exitCode === null && child.signalCode === null) {
145586
145606
  child.kill("SIGKILL");
145607
+ }
145587
145608
  }, 2000);
145588
145609
  if (typeof killHandle.unref === "function") {
145589
145610
  killHandle.unref();
@@ -164952,7 +164973,6 @@ function loadPiConfigDetailed(opts = {}) {
164952
164973
  // src/embedding-bootstrap.ts
164953
164974
  async function ensureProjectRegisteredFromPiDirectory(directory, db) {
164954
164975
  const projectIdentity = resolveProjectIdentity(directory);
164955
- invalidateProject(projectIdentity);
164956
164976
  const detailed = loadPiConfigDetailed({ cwd: directory });
164957
164977
  if (isConfigLoadUntrusted(detailed)) {
164958
164978
  handleUntrustedLoad(db, projectIdentity, directory, detailed);
@@ -164971,6 +164991,7 @@ var sessionsById = new Map;
164971
164991
  var inFlightDreams = new Set;
164972
164992
  var sessionCounter = 0;
164973
164993
  var piSubagentRunnerFactory = () => new PiSubagentRunner;
164994
+ var startDreamScheduleTimerFn = startDreamScheduleTimer;
164974
164995
  function registerPiDreamerProject(opts) {
164975
164996
  if (!opts.config.enabled) {
164976
164997
  return;
@@ -164991,7 +165012,8 @@ function registerPiDreamerProject(opts) {
164991
165012
  min_reads: opts.config.pin_key_files.min_reads
164992
165013
  } : undefined;
164993
165014
  let cleanup;
164994
- startDreamScheduleTimer({
165015
+ let cancelled = false;
165016
+ startDreamScheduleTimerFn({
164995
165017
  directory: opts.projectDir,
164996
165018
  projectIdentity: opts.projectIdentity,
164997
165019
  client,
@@ -165001,6 +165023,10 @@ function registerPiDreamerProject(opts) {
165001
165023
  gitCommitIndexing: opts.gitCommitIndexing,
165002
165024
  ensureRegistered: ensureProjectRegisteredFromPiDirectory
165003
165025
  }).then((timerCleanup) => {
165026
+ if (cancelled) {
165027
+ timerCleanup?.();
165028
+ return;
165029
+ }
165004
165030
  cleanup = timerCleanup;
165005
165031
  });
165006
165032
  const runOnce = async () => processDreamQueue({
@@ -165014,7 +165040,10 @@ function registerPiDreamerProject(opts) {
165014
165040
  projectIdentity: opts.projectIdentity
165015
165041
  });
165016
165042
  registeredProjects2.set(opts.projectIdentity, {
165017
- cleanup: () => cleanup?.(),
165043
+ cleanup: () => {
165044
+ cancelled = true;
165045
+ cleanup?.();
165046
+ },
165018
165047
  runOnce
165019
165048
  });
165020
165049
  }
@@ -165086,6 +165115,7 @@ function createPiDreamerClient(opts) {
165086
165115
  { type: "text", text: result.assistantText }
165087
165116
  ])
165088
165117
  ];
165118
+ opts.onAdjunctsRefreshNeeded?.(opts.projectIdentity);
165089
165119
  } finally {
165090
165120
  inFlightDreams.delete(runPromise);
165091
165121
  }
@@ -165189,21 +165219,37 @@ function sendCtxStatusMessage(pi, content, details) {
165189
165219
  function registerCtxDreamCommand(pi, deps) {
165190
165220
  pi.registerCommand("ctx-dream", {
165191
165221
  description: "Run a Magic Context dreamer cycle for this project now",
165192
- handler: async () => {
165193
- const enqueued = enqueueDream(deps.db, deps.projectIdentity, "manual");
165222
+ handler: async (_args, ctx) => {
165223
+ const project = deps.resolveProject?.(ctx) ?? {
165224
+ projectDir: deps.projectDir,
165225
+ projectIdentity: deps.projectIdentity
165226
+ };
165227
+ deps.onProjectSeen?.(project.projectIdentity);
165228
+ if (deps.dreamerEnabled === false) {
165229
+ sendCtxStatusMessage(pi, {
165230
+ title: "/ctx-dream",
165231
+ text: "## /ctx-dream\n\nDreamer is not configured for this project (`dreamer.enabled=false`).",
165232
+ level: "info"
165233
+ }, {
165234
+ projectDir: project.projectDir,
165235
+ projectIdentity: project.projectIdentity
165236
+ });
165237
+ return;
165238
+ }
165239
+ const enqueued = enqueueDream(deps.db, project.projectIdentity, "manual", true);
165194
165240
  if (!enqueued) {
165195
165241
  sendCtxStatusMessage(pi, {
165196
165242
  title: "/ctx-dream",
165197
165243
  text: [
165198
165244
  "## /ctx-dream",
165199
165245
  "",
165200
- `Dream already queued or running for ${deps.projectIdentity}.`
165246
+ `Dream already queued or running for ${project.projectIdentity}.`
165201
165247
  ].join(`
165202
165248
  `),
165203
165249
  level: "info"
165204
165250
  }, {
165205
- projectDir: deps.projectDir,
165206
- projectIdentity: deps.projectIdentity,
165251
+ projectDir: project.projectDir,
165252
+ projectIdentity: project.projectIdentity,
165207
165253
  entry: null
165208
165254
  });
165209
165255
  return;
@@ -165213,18 +165259,18 @@ function registerCtxDreamCommand(pi, deps) {
165213
165259
  text: [
165214
165260
  "## /ctx-dream",
165215
165261
  "",
165216
- `Starting dream run #${enqueued.id} for ${deps.projectIdentity}…`,
165217
- `Project directory: ${deps.projectDir}`
165262
+ `Starting dream run #${enqueued.id} for ${project.projectIdentity}…`,
165263
+ `Project directory: ${project.projectDir}`
165218
165264
  ].join(`
165219
165265
  `),
165220
165266
  level: "info"
165221
165267
  }, {
165222
- projectDir: deps.projectDir,
165223
- projectIdentity: deps.projectIdentity,
165268
+ projectDir: project.projectDir,
165269
+ projectIdentity: project.projectIdentity,
165224
165270
  entry: enqueued
165225
165271
  });
165226
165272
  try {
165227
- const result = await runPiDreamForProject(deps.projectIdentity);
165273
+ const result = await runPiDreamForProject(project.projectIdentity);
165228
165274
  let summary;
165229
165275
  if (!result) {
165230
165276
  summary = "Dream queued, but another worker is already processing the queue.";
@@ -165250,13 +165296,13 @@ function registerCtxDreamCommand(pi, deps) {
165250
165296
  `),
165251
165297
  level: result ? "success" : "info"
165252
165298
  }, {
165253
- projectDir: deps.projectDir,
165254
- projectIdentity: deps.projectIdentity,
165299
+ projectDir: project.projectDir,
165300
+ projectIdentity: project.projectIdentity,
165255
165301
  entry: enqueued
165256
165302
  });
165257
165303
  } catch (error51) {
165258
165304
  const message = error51 instanceof Error ? error51.message : String(error51);
165259
- sessionLog(deps.projectIdentity, `/ctx-dream failed to drain queue: ${message}`);
165305
+ sessionLog(project.projectIdentity, `/ctx-dream failed to drain queue: ${message}`);
165260
165306
  sendCtxStatusMessage(pi, {
165261
165307
  title: "/ctx-dream",
165262
165308
  text: [
@@ -165268,8 +165314,8 @@ function registerCtxDreamCommand(pi, deps) {
165268
165314
  `),
165269
165315
  level: "error"
165270
165316
  }, {
165271
- projectDir: deps.projectDir,
165272
- projectIdentity: deps.projectIdentity,
165317
+ projectDir: project.projectDir,
165318
+ projectIdentity: project.projectIdentity,
165273
165319
  entry: enqueued
165274
165320
  });
165275
165321
  }
@@ -167526,8 +167572,31 @@ function collectUserPromptParts(message) {
167526
167572
  function hasStackedAugmentation(rawText) {
167527
167573
  return rawText.includes("<sidekick-augmentation>") || rawText.includes("<ctx-search-hint>") || rawText.includes("<ctx-search-auto>");
167528
167574
  }
167575
+ function stripNestedSystemReminders(text) {
167576
+ const OPEN = "<system-reminder>";
167577
+ const CLOSE = "</system-reminder>";
167578
+ let result = "";
167579
+ let depth = 0;
167580
+ let i = 0;
167581
+ while (i < text.length) {
167582
+ if (text.startsWith(OPEN, i)) {
167583
+ depth += 1;
167584
+ i += OPEN.length;
167585
+ } else if (text.startsWith(CLOSE, i)) {
167586
+ if (depth > 0)
167587
+ depth -= 1;
167588
+ i += CLOSE.length;
167589
+ } else if (depth === 0) {
167590
+ result += text[i];
167591
+ i += 1;
167592
+ } else {
167593
+ i += 1;
167594
+ }
167595
+ }
167596
+ return result;
167597
+ }
167529
167598
  function extractUserPromptText(message) {
167530
- return collectUserPromptParts(message).replace(/§\d+§\s*/g, "").replace(/<!--\s*\+[\d\s.hmdw]+\s*-->/g, "").replace(/<!--\s*OMO_INTERNAL_INITIATOR[\s\S]*?-->/g, "").replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").replace(/<ctx-search-hint>[\s\S]*?<\/ctx-search-hint>/g, "").replace(/<ctx-search-auto>[\s\S]*?<\/ctx-search-auto>/g, "").replace(/<instruction[^>]*>[\s\S]*?<\/instruction>/g, "").replace(/<sidekick-augmentation>[\s\S]*?<\/sidekick-augmentation>/g, "").replace(/[ \t]+\n/g, `
167599
+ return stripNestedSystemReminders(collectUserPromptParts(message)).replace(/§\d+§\s*/g, "").replace(/<!--\s*\+[\d\s.hmdw]+\s*-->/g, "").replace(/<!--\s*OMO_INTERNAL_INITIATOR[\s\S]*?-->/g, "").replace(/<!--\s*ALFONSO_INTERNAL_INITIATOR[\s\S]*?-->/g, "").replace(/<ctx-search-hint>[\s\S]*?<\/ctx-search-hint>/g, "").replace(/<ctx-search-auto>[\s\S]*?<\/ctx-search-auto>/g, "").replace(/<instruction[^>]*>[\s\S]*?<\/instruction>/g, "").replace(/<sidekick-augmentation>[\s\S]*?<\/sidekick-augmentation>/g, "").replace(/[ \t]+\n/g, `
167531
167600
  `).replace(/\n{3,}/g, `
167532
167601
 
167533
167602
  `).trim();
@@ -167769,7 +167838,8 @@ var DEDUP_SAFE_TOOLS = new Set([
167769
167838
  ]);
167770
167839
  function buildPiToolFingerprints(messages) {
167771
167840
  const fingerprints2 = new Map;
167772
- for (const message of messages) {
167841
+ for (let i = 0;i < messages.length; i++) {
167842
+ const message = messages[i];
167773
167843
  if (!message || typeof message !== "object")
167774
167844
  continue;
167775
167845
  const msg = message;
@@ -167777,6 +167847,7 @@ function buildPiToolFingerprints(messages) {
167777
167847
  continue;
167778
167848
  if (!Array.isArray(msg.content))
167779
167849
  continue;
167850
+ const ownerMsgId = typeof msg.timestamp === "number" ? `pi-msg-${i}-${msg.timestamp}-assistant` : `pi-msg-${i}-assistant`;
167780
167851
  for (const part of msg.content) {
167781
167852
  if (!part || typeof part !== "object")
167782
167853
  continue;
@@ -167799,7 +167870,9 @@ function buildPiToolFingerprints(messages) {
167799
167870
  } catch {
167800
167871
  continue;
167801
167872
  }
167802
- fingerprints2.set(p.id, `${p.name}:${serialized}`);
167873
+ const fingerprint = `${ownerMsgId}:${p.name}:${serialized}`;
167874
+ const compositeKey = `${ownerMsgId}\x00${p.id}`;
167875
+ fingerprints2.set(compositeKey, fingerprint);
167803
167876
  }
167804
167877
  }
167805
167878
  return fingerprints2;
@@ -167917,15 +167990,16 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
167917
167990
  })();
167918
167991
  const toolFingerprints = buildPiToolFingerprints(piMessages);
167919
167992
  if (toolFingerprints.size > 0) {
167920
- const tagsByMessageId = new Map;
167993
+ const tagsByCompositeKey = new Map;
167921
167994
  for (const tag of tags) {
167922
167995
  if (tag.type === "tool" && tag.status === "active" && tag.messageId) {
167923
- tagsByMessageId.set(tag.messageId, tag);
167996
+ const key = tag.toolOwnerMessageId ? `${tag.toolOwnerMessageId}\x00${tag.messageId}` : tag.messageId;
167997
+ tagsByCompositeKey.set(key, tag);
167924
167998
  }
167925
167999
  }
167926
168000
  const fingerprintGroups = new Map;
167927
- for (const [callId, fingerprint] of toolFingerprints) {
167928
- const tag = tagsByMessageId.get(callId);
168001
+ for (const [compositeKey, fingerprint] of toolFingerprints) {
168002
+ const tag = tagsByCompositeKey.get(compositeKey);
167929
168003
  if (!tag || tag.tagNumber > protectedCutoff)
167930
168004
  continue;
167931
168005
  const group = fingerprintGroups.get(fingerprint) ?? [];
@@ -169877,8 +169951,18 @@ function injectSyntheticTodowriteForPi(args) {
169877
169951
  }
169878
169952
 
169879
169953
  // src/reasoning-replay-pi.ts
169880
- var INLINE_THINKING_PATTERN = /<thinking>[\s\S]*?<\/thinking>\s*/gi;
169954
+ var INLINE_THINKING_PATTERNS = [
169955
+ /<thinking>[\s\S]*?<\/thinking>\s*/gi,
169956
+ /<think>[\s\S]*?<\/think>\s*/gi
169957
+ ];
169881
169958
  var CLEARED = "[cleared]";
169959
+ function stripInlineThinkingMarkup(text) {
169960
+ let cleaned = text;
169961
+ for (const pattern of INLINE_THINKING_PATTERNS) {
169962
+ cleaned = cleaned.replace(pattern, "");
169963
+ }
169964
+ return cleaned;
169965
+ }
169882
169966
  function buildMessageIdToMaxTag(targets) {
169883
169967
  const out = new Map;
169884
169968
  for (const [tagNumber, target] of targets) {
@@ -169931,6 +170015,51 @@ function clearOldReasoningPi(args) {
169931
170015
  }
169932
170016
  return { cleared, newWatermark };
169933
170017
  }
170018
+ function stripInlineThinkingPi(args) {
170019
+ const { messages, messageIdToMaxTag, clearReasoningAge, piMessageStableId } = args;
170020
+ let maxTag = 0;
170021
+ for (const t of messageIdToMaxTag.values())
170022
+ if (t > maxTag)
170023
+ maxTag = t;
170024
+ if (maxTag === 0)
170025
+ return { stripped: 0, newWatermark: 0 };
170026
+ const ageCutoff = maxTag - clearReasoningAge;
170027
+ if (ageCutoff <= 0)
170028
+ return { stripped: 0, newWatermark: 0 };
170029
+ let stripped = 0;
170030
+ let newWatermark = 0;
170031
+ for (let i = 0;i < messages.length; i++) {
170032
+ const raw = messages[i];
170033
+ if (!raw || typeof raw !== "object")
170034
+ continue;
170035
+ const msg = raw;
170036
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
170037
+ continue;
170038
+ const id = piMessageStableId(raw, i);
170039
+ if (!id)
170040
+ continue;
170041
+ const msgTag = messageIdToMaxTag.get(id) ?? 0;
170042
+ if (msgTag === 0 || msgTag > ageCutoff)
170043
+ continue;
170044
+ let strippedThisMessage = false;
170045
+ for (const part of msg.content) {
170046
+ if (part && typeof part === "object" && part.type === "text") {
170047
+ const tp = part;
170048
+ if (typeof tp.text !== "string")
170049
+ continue;
170050
+ const cleaned = stripInlineThinkingMarkup(tp.text);
170051
+ if (cleaned !== tp.text) {
170052
+ tp.text = cleaned;
170053
+ stripped++;
170054
+ strippedThisMessage = true;
170055
+ }
170056
+ }
170057
+ }
170058
+ if (strippedThisMessage && msgTag > newWatermark)
170059
+ newWatermark = msgTag;
170060
+ }
170061
+ return { stripped, newWatermark };
170062
+ }
169934
170063
  function replayClearedReasoningPi(args) {
169935
170064
  const { db, sessionId, messages, messageIdToMaxTag, piMessageStableId } = args;
169936
170065
  const meta3 = getOrCreateSessionMeta(db, sessionId);
@@ -169988,7 +170117,7 @@ function replayStrippedInlineThinkingPi(args) {
169988
170117
  const tp = part;
169989
170118
  if (typeof tp.text !== "string")
169990
170119
  continue;
169991
- const cleaned = tp.text.replace(INLINE_THINKING_PATTERN, "");
170120
+ const cleaned = stripInlineThinkingMarkup(tp.text);
169992
170121
  if (cleaned !== tp.text) {
169993
170122
  tp.text = cleaned;
169994
170123
  stripped++;
@@ -170079,6 +170208,408 @@ function stripPiDroppedPlaceholderMessages(args) {
170079
170208
  return { removed, discovered };
170080
170209
  }
170081
170210
 
170211
+ // src/system-prompt.ts
170212
+ import { createHash as createHash8 } from "node:crypto";
170213
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
170214
+ import { join as join15 } from "node:path";
170215
+
170216
+ // ../plugin/src/agents/magic-context-prompt.ts
170217
+ function getToolHistoryGuidance(dropToolStructure) {
170218
+ if (dropToolStructure) {
170219
+ return `Compressed history intentionally omits tool calls and their outputs — summaries like "I edited file X" are historian records, not patterns to replicate. In the live conversation, older tool calls and their results are cleaned up to save context — you may see your own past messages referencing actions without the corresponding tool call or result visible. This is normal context management. ALWAYS use real tool calls; never simulate, fabricate, or inline tool outputs in your text. If there is no tool result message, the action did not happen. NEVER simulate, hallucinate or claim tool calls, command output, search results, file edits, or diffs in plain text as if they actually occurred.`;
170220
+ }
170221
+ return `Older tool calls in your conversation show truncated inputs and [truncated] outputs — this is normal context management, not a pattern to follow. The original tool calls executed successfully with full inputs and produced real outputs that were later cleaned up to save context. ALWAYS use real tool calls with complete arguments; never copy truncated patterns like "filePa...[truncated]" into your tool inputs. If you need to re-read a file or re-run a command, make a fresh tool call.`;
170222
+ }
170223
+ var BASE_INTRO = (protectedTags, dropToolStructure) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
170224
+ Use \`ctx_reduce\` to manage context size. It supports one operation:
170225
+ - \`drop\`: Remove entirely (best for tool outputs you already acted on).
170226
+ Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
170227
+ Use \`ctx_note\` for deferred intentions — things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
170228
+ Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
170229
+ **Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
170230
+ - Found a project's source code path after searching → \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
170231
+ - Discovered a non-obvious build/test command → \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
170232
+ - Learned a constraint the hard way → \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
170233
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
170234
+ Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170235
+ **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
170236
+ - Can't remember where a related codebase or dependency lives → \`ctx_search(query="opencode source code path")\`
170237
+ - Forgot a prior architectural decision or constraint → \`ctx_search(query="why did we choose SQLite over postgres")\`
170238
+ - Need a config value, API key location, or environment detail → \`ctx_search(query="embedding provider configuration")\`
170239
+ - Looking for how something was implemented previously → \`ctx_search(query="how does the dreamer lease work")\`
170240
+ - Want to recall what was decided in an earlier conversation → \`ctx_search(query="dashboard release signing setup")\`
170241
+ \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
170242
+ ${getToolHistoryGuidance(dropToolStructure)}
170243
+ NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
170244
+ NEVER drop user messages — they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
170245
+ NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
170246
+ Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
170247
+ var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `Use \`ctx_note\` for deferred intentions — things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
170248
+ Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
170249
+ **Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
170250
+ - Found a project's source code path after searching → \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
170251
+ - Discovered a non-obvious build/test command → \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
170252
+ - Learned a constraint the hard way → \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
170253
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
170254
+ Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
170255
+ **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
170256
+ - Can't remember where a related codebase or dependency lives → \`ctx_search(query="opencode source code path")\`
170257
+ - Forgot a prior architectural decision or constraint → \`ctx_search(query="why did we choose SQLite over postgres")\`
170258
+ - Need a config value, API key location, or environment detail → \`ctx_search(query="embedding provider configuration")\`
170259
+ - Looking for how something was implemented previously → \`ctx_search(query="how does the dreamer lease work")\`
170260
+ - Want to recall what was decided in an earlier conversation → \`ctx_search(query="dashboard release signing setup")\`
170261
+ \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
170262
+ ${getToolHistoryGuidance(dropToolStructure)}`;
170263
+ var GENERIC_SECTION = `
170264
+ ### Reduction Triggers
170265
+ - After reading files or search results you already acted on — drop raw outputs.
170266
+ - After completing a logical step — drop intermediate outputs from that step.
170267
+ - Between major context switches — when moving to a new task area.
170268
+
170269
+ ### What to Drop
170270
+ - Large file reads, grep results, and tool outputs you already used.
170271
+ - Large build/test output after you analyzed and acted on it.
170272
+ - Old diagnostic or exploration results that are no longer relevant.
170273
+
170274
+ ### What to Keep
170275
+ - ALL user messages and assistant conversation text — these are cheap and compartmentalized automatically.
170276
+ - Your current task requirements and constraints.
170277
+ - Recent errors and unresolved decisions.
170278
+ - Active work context and files being edited.`;
170279
+ var TEMPORAL_AWARENESS_GUIDANCE = `
170280
+ **Temporal awareness**: User messages may be preceded by HTML comments like \`<!-- +12m -->\`, \`<!-- +2h 15m -->\`, or \`<!-- +3d 4h -->\` indicating time elapsed since the previous message's completion. Compartments in \`<session-history>\` carry \`start-date\` and \`end-date\` attributes (YYYY-MM-DD) showing real-time boundaries. Use these when reasoning about workflow pacing, log durations, build times, or how long ago something happened.`;
170281
+ var CAVEMAN_COMPRESSION_WARNING = `
170282
+ **BEWARE**: History compression is on; older user AND assistant text — including your own earlier responses — has been deterministically rewritten in a terse caveman style (dropped articles, missing auxiliaries, \`//\` instead of connectives like \`because\`). This is automatic context compression that runs after the fact, not your actual prior wording or the user's. **DO NOT mimic this style in new turns.** Write fresh responses in normal prose. If you notice your output drifting into caveman cadence, that drift is in-context-learning bleeding from the compressed history — consciously revert to full sentences.`;
170283
+ function buildMagicContextSection(_agent, protectedTags, ctxReduceEnabled = true, dreamerEnabled = false, dropToolStructure = true, temporalAwarenessEnabled = false, cavemanTextCompressionEnabled = false) {
170284
+ const smartNoteGuidance = dreamerEnabled ? `
170285
+ When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
170286
+ The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
170287
+ Example: \`ctx_note(action="write", content="Implement X because Y", surface_condition="When PR #42 is merged in this repo")\`` : "";
170288
+ const temporalGuidance = temporalAwarenessEnabled ? TEMPORAL_AWARENESS_GUIDANCE : "";
170289
+ const cavemanWarning = cavemanTextCompressionEnabled && !ctxReduceEnabled ? CAVEMAN_COMPRESSION_WARNING : "";
170290
+ if (!ctxReduceEnabled) {
170291
+ return `## Magic Context
170292
+
170293
+ ${BASE_INTRO_NO_REDUCE(dropToolStructure)}${smartNoteGuidance}${temporalGuidance}${cavemanWarning}`;
170294
+ }
170295
+ return `## Magic Context
170296
+
170297
+ ${BASE_INTRO(protectedTags, dropToolStructure)}${smartNoteGuidance}${temporalGuidance}
170298
+ ${GENERIC_SECTION}
170299
+
170300
+ Prefer many small targeted operations over one large blanket operation. Compress early and often — don't wait for warnings.`;
170301
+ }
170302
+
170303
+ // ../plugin/src/hooks/magic-context/key-files-block.ts
170304
+ import { readFileSync as readFileSync7, realpathSync as realpathSync3 } from "node:fs";
170305
+ import { join as join14, sep as sep2 } from "node:path";
170306
+ init_logger();
170307
+ var cachedKeyFilesBySession = new Map;
170308
+ var staleUpdates = new Map;
170309
+ function staleKey(update) {
170310
+ return `${update.projectPath}\x00${update.path}\x00${update.generatedAtWitness}\x00${update.staleReason}`;
170311
+ }
170312
+ function queueStaleUpdate(projectPath, path5, generatedAtWitness, staleReason) {
170313
+ const update = { projectPath, path: path5, generatedAtWitness, staleReason };
170314
+ staleUpdates.set(staleKey(update), update);
170315
+ }
170316
+ function flushStaleUpdates(db) {
170317
+ if (staleUpdates.size === 0)
170318
+ return 0;
170319
+ const updates = [...staleUpdates.values()];
170320
+ staleUpdates.clear();
170321
+ const stmt = db.prepare(`UPDATE project_key_files
170322
+ SET stale_reason = ?1
170323
+ WHERE project_path = ?2
170324
+ AND path = ?3
170325
+ AND generated_at = ?4
170326
+ AND (stale_reason IS NULL OR stale_reason != ?1)`);
170327
+ let changed = 0;
170328
+ for (const update of updates) {
170329
+ try {
170330
+ changed += stmt.run(update.staleReason, update.projectPath, update.path, update.generatedAtWitness).changes;
170331
+ } catch (error51) {
170332
+ log("[key-files] flushStaleUpdates failed:", error51);
170333
+ }
170334
+ }
170335
+ return changed;
170336
+ }
170337
+ function clearKeyFilesCacheForSession(sessionId) {
170338
+ cachedKeyFilesBySession.delete(sessionId);
170339
+ }
170340
+ function isUnderProject(projectPath, absPath) {
170341
+ const root = realpathSync3(projectPath);
170342
+ return absPath.startsWith(root + sep2) || absPath === root;
170343
+ }
170344
+ function buildKeyFilesBlock(db, projectPath, config2 = { enabled: true, tokenBudget: 1e4 }) {
170345
+ if (!config2.enabled)
170346
+ return null;
170347
+ if (!isAftAvailable())
170348
+ return null;
170349
+ const rows = readCurrentKeyFiles(db, projectPath);
170350
+ if (rows.length === 0)
170351
+ return null;
170352
+ for (const row of rows) {
170353
+ if (row.staleReason !== null)
170354
+ continue;
170355
+ let nextStale = null;
170356
+ let observed = false;
170357
+ try {
170358
+ const absPath = join14(projectPath, row.path);
170359
+ const real = realpathSync3(absPath);
170360
+ if (!isUnderProject(projectPath, real)) {
170361
+ nextStale = "missing";
170362
+ observed = true;
170363
+ } else {
170364
+ const diskHash = sha256(readFileSync7(real));
170365
+ if (diskHash !== row.contentHash)
170366
+ nextStale = "content_drift";
170367
+ observed = true;
170368
+ }
170369
+ } catch (error51) {
170370
+ const code = error51?.code;
170371
+ if (code === "ENOENT" || code === "ELOOP") {
170372
+ nextStale = "missing";
170373
+ observed = true;
170374
+ } else {
170375
+ log(`[key-files] freshness check transient failure: ${row.path}: ${error51 instanceof Error ? error51.message : String(error51)}`);
170376
+ }
170377
+ }
170378
+ if (observed && nextStale !== null) {
170379
+ queueStaleUpdate(row.projectPath, row.path, row.generatedAt, nextStale);
170380
+ }
170381
+ }
170382
+ const blocks = [];
170383
+ let used = 0;
170384
+ for (const row of rows) {
170385
+ if (used + row.localTokenEstimate > config2.tokenBudget)
170386
+ break;
170387
+ blocks.push(` <key-file path="${escapeXmlAttr(row.path)}">
170388
+ ${escapeXmlContent(row.content)}
170389
+ </key-file>`);
170390
+ used += row.localTokenEstimate;
170391
+ }
170392
+ if (blocks.length === 0)
170393
+ return null;
170394
+ const rendered = `<key-files>
170395
+ ${blocks.join(`
170396
+ `)}
170397
+ </key-files>`;
170398
+ flushStaleUpdates(db);
170399
+ return rendered;
170400
+ }
170401
+ function readVersionedKeyFiles(args) {
170402
+ const config2 = args.config ?? { enabled: true, tokenBudget: 1e4 };
170403
+ if (args.sessionMeta.isSubagent)
170404
+ return null;
170405
+ if (!config2.enabled)
170406
+ return null;
170407
+ if (!isAftAvailable())
170408
+ return null;
170409
+ const projectPath = resolveProjectPath(args.directory ?? args.sessionMeta.sessionId);
170410
+ if (!projectPath)
170411
+ return null;
170412
+ const currentVersion = getKeyFilesVersion(args.db, projectPath);
170413
+ if (args.sessionId) {
170414
+ const cached2 = cachedKeyFilesBySession.get(args.sessionId);
170415
+ if (cached2 && !args.isCacheBusting && cached2.version === currentVersion) {
170416
+ return cached2.value;
170417
+ }
170418
+ }
170419
+ const value = buildKeyFilesBlock(args.db, projectPath, config2);
170420
+ if (args.sessionId) {
170421
+ cachedKeyFilesBySession.set(args.sessionId, { value, version: currentVersion });
170422
+ if (value)
170423
+ sessionLog(args.sessionId, `loaded key-files block (v${currentVersion}, ${value.length} chars)`);
170424
+ }
170425
+ return value;
170426
+ }
170427
+
170428
+ // src/system-prompt.ts
170429
+ init_logger();
170430
+ var PROJECT_DOCS_MARKER = "<project-docs>";
170431
+ var USER_PROFILE_MARKER = "<user-profile>";
170432
+ var KEY_FILES_MARKER = "<key-files>";
170433
+ var MAGIC_CONTEXT_MARKER = "## Magic Context";
170434
+ var stickyDateBySession = new Map;
170435
+ var cachedDocsBySession = new Map;
170436
+ var cachedUserProfileBySession = new Map;
170437
+ var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
170438
+ function readProjectDocs(directory) {
170439
+ const sections = [];
170440
+ for (const filename of DOC_FILES) {
170441
+ const filePath = join15(directory, filename);
170442
+ try {
170443
+ if (existsSync11(filePath)) {
170444
+ const content = readFileSync8(filePath, "utf-8").trim();
170445
+ if (content.length > 0) {
170446
+ sections.push(`<${filename}>
170447
+ ${escapeXmlContent(content)}
170448
+ </${filename}>`);
170449
+ }
170450
+ }
170451
+ } catch (error51) {
170452
+ log(`[magic-context-pi] failed to read ${filename}:`, error51);
170453
+ }
170454
+ }
170455
+ if (sections.length === 0)
170456
+ return null;
170457
+ return `${PROJECT_DOCS_MARKER}
170458
+ ${sections.join(`
170459
+
170460
+ `)}
170461
+ </project-docs>`;
170462
+ }
170463
+ function buildUserProfileBlock(db) {
170464
+ const memories = getActiveUserMemories(db);
170465
+ if (memories.length === 0)
170466
+ return null;
170467
+ const items = memories.map((m) => `- ${escapeXmlContent(m.content)}`).join(`
170468
+ `);
170469
+ return `${USER_PROFILE_MARKER}
170470
+ ${items}
170471
+ </user-profile>`;
170472
+ }
170473
+ function buildMagicContextBlock(opts) {
170474
+ const sections = [];
170475
+ const sessionId = opts.sessionId;
170476
+ const isCacheBusting = opts.isCacheBusting ?? false;
170477
+ const existing = opts.existingSystemPrompt ?? "";
170478
+ if (opts.injectDocs && !existing.includes(PROJECT_DOCS_MARKER)) {
170479
+ const docsBlock = readCachedAdjunct({
170480
+ cache: cachedDocsBySession,
170481
+ sessionId,
170482
+ isCacheBusting,
170483
+ compute: () => readProjectDocs(opts.cwd),
170484
+ describe: "project docs"
170485
+ });
170486
+ if (docsBlock)
170487
+ sections.push(docsBlock);
170488
+ }
170489
+ if (opts.userMemoriesEnabled && !existing.includes(USER_PROFILE_MARKER)) {
170490
+ const profileBlock = readCachedAdjunct({
170491
+ cache: cachedUserProfileBySession,
170492
+ sessionId,
170493
+ isCacheBusting,
170494
+ compute: () => buildUserProfileBlock(opts.db),
170495
+ describe: "user profile"
170496
+ });
170497
+ if (profileBlock)
170498
+ sections.push(profileBlock);
170499
+ }
170500
+ if (opts.pinKeyFilesEnabled && sessionId && !existing.includes(KEY_FILES_MARKER)) {
170501
+ let sessionMeta = null;
170502
+ try {
170503
+ sessionMeta = getOrCreateSessionMeta(opts.db, sessionId);
170504
+ } catch (error51) {
170505
+ sessionLog(sessionId, "Pi key-files session meta load failed:", error51);
170506
+ }
170507
+ const keyFilesBlock = sessionMeta ? readVersionedKeyFiles({
170508
+ db: opts.db,
170509
+ sessionId,
170510
+ sessionMeta,
170511
+ directory: opts.cwd,
170512
+ isCacheBusting,
170513
+ config: {
170514
+ enabled: opts.pinKeyFilesEnabled,
170515
+ tokenBudget: opts.pinKeyFilesTokenBudget ?? 1e4
170516
+ }
170517
+ }) : null;
170518
+ if (keyFilesBlock)
170519
+ sections.push(keyFilesBlock);
170520
+ }
170521
+ const dataBlock = sections.length > 0 ? `<magic-context>
170522
+ ${sections.join(`
170523
+
170524
+ `)}
170525
+ </magic-context>` : null;
170526
+ const includeGuidance = (opts.includeGuidance ?? true) && !existing.includes(MAGIC_CONTEXT_MARKER);
170527
+ if (!includeGuidance) {
170528
+ return dataBlock;
170529
+ }
170530
+ const guidance = buildMagicContextSection(null, opts.protectedTags ?? 20, opts.ctxReduceEnabled ?? true, opts.dreamerEnabled ?? false, opts.dropToolStructure ?? true, opts.temporalAwarenessEnabled ?? false, opts.cavemanTextCompressionEnabled ?? false);
170531
+ if (dataBlock) {
170532
+ return `${guidance}
170533
+
170534
+ ${dataBlock}`;
170535
+ }
170536
+ return guidance;
170537
+ }
170538
+ function readCachedAdjunct(args) {
170539
+ const { cache, sessionId, isCacheBusting, compute, describe: describe3 } = args;
170540
+ if (!sessionId) {
170541
+ return compute();
170542
+ }
170543
+ const hasCached = cache.has(sessionId);
170544
+ if (!hasCached || isCacheBusting) {
170545
+ const value = compute();
170546
+ cache.set(sessionId, value);
170547
+ if (value && !hasCached) {
170548
+ sessionLog(sessionId, `loaded ${describe3} (${value.length} chars)`);
170549
+ } else if (value && isCacheBusting) {
170550
+ sessionLog(sessionId, `refreshed ${describe3} (cache-busting pass)`);
170551
+ }
170552
+ return value;
170553
+ }
170554
+ return cache.get(sessionId) ?? null;
170555
+ }
170556
+ var DATE_PATTERN = /Today's date: .+/;
170557
+ function processSystemPromptForCache(args) {
170558
+ const { db, sessionId, systemPrompt, isCacheBusting } = args;
170559
+ let sessionMeta;
170560
+ try {
170561
+ sessionMeta = getOrCreateSessionMeta(db, sessionId);
170562
+ } catch (error51) {
170563
+ sessionLog(sessionId, "system-prompt-hash session meta load failed:", error51);
170564
+ }
170565
+ const previousHash = sessionMeta?.systemPromptHash ?? "";
170566
+ const isFirstHash = previousHash === "" || previousHash === "0";
170567
+ let frozenPrompt = systemPrompt;
170568
+ const dateMatch = systemPrompt.match(DATE_PATTERN);
170569
+ const liveDate = dateMatch ? dateMatch[0] : null;
170570
+ const stickyDate = stickyDateBySession.get(sessionId);
170571
+ if (liveDate && !stickyDate) {
170572
+ stickyDateBySession.set(sessionId, liveDate);
170573
+ } else if (liveDate && stickyDate && liveDate !== stickyDate) {
170574
+ if (isCacheBusting) {
170575
+ stickyDateBySession.set(sessionId, liveDate);
170576
+ sessionLog(sessionId, `system prompt date updated: ${stickyDate} → ${liveDate} (cache-busting pass)`);
170577
+ } else {
170578
+ frozenPrompt = systemPrompt.replace(DATE_PATTERN, stickyDate);
170579
+ sessionLog(sessionId, `system prompt date frozen: real=${liveDate}, using=${stickyDate} (cache-stable pass)`);
170580
+ }
170581
+ }
170582
+ const currentHash = createHash8("md5").update(frozenPrompt).digest("hex");
170583
+ const hashChanged = !isFirstHash && currentHash !== previousHash;
170584
+ if (hashChanged) {
170585
+ sessionLog(sessionId, `system prompt hash changed: ${previousHash} → ${currentHash} (len=${frozenPrompt.length})`);
170586
+ } else if (isFirstHash) {
170587
+ sessionLog(sessionId, `system prompt hash initialized: ${currentHash} (len=${frozenPrompt.length})`);
170588
+ }
170589
+ const systemPromptTokens = estimateTokens(frozenPrompt);
170590
+ if (sessionMeta) {
170591
+ if (currentHash !== previousHash) {
170592
+ updateSessionMeta(db, sessionId, {
170593
+ systemPromptHash: currentHash,
170594
+ systemPromptTokens
170595
+ });
170596
+ } else if (Math.abs(sessionMeta.systemPromptTokens - systemPromptTokens) > 50) {
170597
+ updateSessionMeta(db, sessionId, { systemPromptTokens });
170598
+ }
170599
+ }
170600
+ return {
170601
+ systemPrompt: frozenPrompt,
170602
+ hashChanged,
170603
+ currentHash
170604
+ };
170605
+ }
170606
+ function clearPiSystemPromptSession(sessionId) {
170607
+ stickyDateBySession.delete(sessionId);
170608
+ cachedDocsBySession.delete(sessionId);
170609
+ cachedUserProfileBySession.delete(sessionId);
170610
+ clearKeyFilesCacheForSession(sessionId);
170611
+ }
170612
+
170082
170613
  // src/temporal-awareness-pi.ts
170083
170614
  function injectPiTemporalMarkers(messages) {
170084
170615
  let injected = 0;
@@ -170639,6 +171170,12 @@ var commitSeenLastPass = new Map;
170639
171170
  var historyRefreshSessions = new Set;
170640
171171
  var systemPromptRefreshSessions = new Set;
170641
171172
  var pendingMaterializationSessions = new Set;
171173
+ var deferredHistoryRefreshSessions = new Set;
171174
+ var deferredMaterializationSessions = new Set;
171175
+ var sessionsByProject = new Map;
171176
+ var lastSeenProjectIdentityBySession = new Map;
171177
+ var rawMessageProviderUnregistersBySession = new Map;
171178
+ var lastHeuristicsTurnIdBySession = new Map;
170642
171179
  var firstContextPassSeenBySession = new Set;
170643
171180
  var recentReduceBySession = new Map;
170644
171181
  var liveModelBySession = new Map;
@@ -170672,6 +171209,50 @@ function signalPiSystemPromptRefresh(sessionId) {
170672
171209
  function signalPiPendingMaterialization(sessionId) {
170673
171210
  pendingMaterializationSessions.add(sessionId);
170674
171211
  }
171212
+ function signalPiDeferredHistoryRefresh(sessionId) {
171213
+ deferredHistoryRefreshSessions.add(sessionId);
171214
+ }
171215
+ function signalPiDeferredMaterialization(sessionId) {
171216
+ deferredMaterializationSessions.add(sessionId);
171217
+ }
171218
+ function consumeDeferredHistoryRefresh(sessionId) {
171219
+ const wasSet = deferredHistoryRefreshSessions.has(sessionId);
171220
+ deferredHistoryRefreshSessions.delete(sessionId);
171221
+ return wasSet;
171222
+ }
171223
+ function consumeDeferredMaterialization(sessionId) {
171224
+ const wasSet = deferredMaterializationSessions.has(sessionId);
171225
+ deferredMaterializationSessions.delete(sessionId);
171226
+ return wasSet;
171227
+ }
171228
+ function trackSessionForProject(projectIdentity, sessionId) {
171229
+ let sessions = sessionsByProject.get(projectIdentity);
171230
+ if (!sessions) {
171231
+ sessions = new Set;
171232
+ sessionsByProject.set(projectIdentity, sessions);
171233
+ }
171234
+ sessions.add(sessionId);
171235
+ }
171236
+ function updateSessionProjectTracking(sessionId, projectIdentity) {
171237
+ const prev = lastSeenProjectIdentityBySession.get(sessionId);
171238
+ if (prev && prev !== projectIdentity) {
171239
+ const prevSessions = sessionsByProject.get(prev);
171240
+ prevSessions?.delete(sessionId);
171241
+ if (prevSessions?.size === 0)
171242
+ sessionsByProject.delete(prev);
171243
+ clearPiSystemPromptSession(sessionId);
171244
+ }
171245
+ trackSessionForProject(projectIdentity, sessionId);
171246
+ lastSeenProjectIdentityBySession.set(sessionId, projectIdentity);
171247
+ }
171248
+ function signalPiSystemPromptRefreshForProject(projectIdentity) {
171249
+ const sessions = sessionsByProject.get(projectIdentity);
171250
+ if (!sessions)
171251
+ return;
171252
+ for (const sessionId of sessions) {
171253
+ systemPromptRefreshSessions.add(sessionId);
171254
+ }
171255
+ }
170675
171256
  function recordPiLiveModel(sessionId, modelKey) {
170676
171257
  liveModelBySession.set(sessionId, modelKey);
170677
171258
  }
@@ -170849,8 +171430,6 @@ function collectMessageEntryIdsStrict(ctx, expectedLength, sessionId) {
170849
171430
  }
170850
171431
  function registerPiContextHandler(pi, options) {
170851
171432
  const tagger = createTagger();
170852
- const projectDirectory = process.cwd();
170853
- const projectIdentity = resolveProjectIdentity(projectDirectory);
170854
171433
  const schedulerConfig = options.scheduler ?? {
170855
171434
  executeThresholdPercentage: 65
170856
171435
  };
@@ -170876,12 +171455,17 @@ function registerPiContextHandler(pi, options) {
170876
171455
  return;
170877
171456
  }
170878
171457
  sessionIdForError = sessionId;
171458
+ const projectDirectory = ctx.cwd;
171459
+ const projectIdentity = resolveProjectIdentity(projectDirectory);
171460
+ updateSessionProjectTracking(sessionId, projectIdentity);
170879
171461
  logTransformTiming(sessionId, "findSessionId", tFindSession, `messages=${event.messages.length}`);
170880
171462
  const rawMessageProvider = {
170881
171463
  readMessages: () => readPiSessionMessages(ctx),
170882
171464
  readMessageById: (messageId) => readPiSessionMessageById(ctx, messageId)
170883
171465
  };
170884
- setRawMessageProvider(sessionId, rawMessageProvider);
171466
+ rawMessageProviderUnregistersBySession.get(sessionId)?.();
171467
+ const unregisterRaw = setRawMessageProvider(sessionId, rawMessageProvider);
171468
+ rawMessageProviderUnregistersBySession.set(sessionId, unregisterRaw);
170885
171469
  scheduleReconciliation(options.db, sessionId, readRawSessionMessages);
170886
171470
  const strictEntryIds = collectMessageEntryIdsStrict(ctx, event.messages.length, sessionId);
170887
171471
  const tLastUser = performance.now();
@@ -170916,12 +171500,18 @@ function registerPiContextHandler(pi, options) {
170916
171500
  lastContextPercentage: 0,
170917
171501
  lastInputTokens: 0,
170918
171502
  observedSafeInputTokens: 0,
170919
- cacheAlertSent: false
171503
+ cacheAlertSent: false,
171504
+ clearedReasoningThroughTag: 0
170920
171505
  });
171506
+ clearHistorianFailureState(options.db, sessionId);
171507
+ clearPersistedReasoningWatermark(options.db, sessionId);
171508
+ clearDetectedContextLimit(options.db, sessionId);
171509
+ clearEmergencyRecovery(options.db, sessionId);
170921
171510
  sessionMetaForUsage.lastContextPercentage = 0;
170922
171511
  sessionMetaForUsage.lastInputTokens = 0;
170923
171512
  sessionMetaForUsage.observedSafeInputTokens = 0;
170924
171513
  sessionMetaForUsage.cacheAlertSent = false;
171514
+ sessionMetaForUsage.clearedReasoningThroughTag = 0;
170925
171515
  }
170926
171516
  let usagePercentage = 0;
170927
171517
  let usageInputTokens = 0;
@@ -171050,11 +171640,25 @@ function registerPiContextHandler(pi, options) {
171050
171640
  isCacheBusting,
171051
171641
  usagePercentage,
171052
171642
  usageInputTokens,
171053
- usageContextLimit
171643
+ usageContextLimit,
171644
+ schedulerDecision
171054
171645
  });
171055
171646
  }
171056
171647
  const tPostTransform = performance.now();
171057
171648
  let outputMessages = result.messages;
171649
+ try {
171650
+ const cacheBustingPass = isCacheBusting || result.executedWorkThisPass;
171651
+ outputMessages = applyStickyTurnReminder({
171652
+ sessionId,
171653
+ db: options.db,
171654
+ messages: outputMessages,
171655
+ entryIds: entryIds ?? null,
171656
+ hasRecentReduceCall: recentReduceBySession.has(sessionId),
171657
+ isCacheBustingPass: cacheBustingPass
171658
+ });
171659
+ } catch (err) {
171660
+ sessionLog(sessionId, `sticky turn reminder failed: ${err instanceof Error ? err.message : String(err)}`);
171661
+ }
171058
171662
  if (nudgerFn && options.nudge) {
171059
171663
  try {
171060
171664
  const tNudge = performance.now();
@@ -171168,7 +171772,7 @@ function maybeFireCompressor(args) {
171168
171772
  const compressor = historian.compressor;
171169
171773
  if (!compressor?.enabled)
171170
171774
  return;
171171
- if (!args.isCacheBusting)
171775
+ if (!args.isCacheBusting && args.schedulerDecision !== "execute")
171172
171776
  return;
171173
171777
  if (inFlightHistorian.has(sessionId) || inFlightCompressor.has(sessionId)) {
171174
171778
  sessionLog(sessionId, "compressor trigger eval: in-flight historian/compressor, skipping");
@@ -171188,7 +171792,6 @@ function maybeFireCompressor(args) {
171188
171792
  });
171189
171793
  if (!historyBudgetTokens || historyBudgetTokens <= 0)
171190
171794
  return;
171191
- markPiCompressorRun(sessionId);
171192
171795
  const runPromise = runPiCompressionPassIfNeeded({
171193
171796
  db,
171194
171797
  sessionId,
@@ -171204,10 +171807,14 @@ function maybeFireCompressor(args) {
171204
171807
  maxCompartmentsPerPass: compressor.maxCompartmentsPerPass,
171205
171808
  graceCompartments: compressor.graceCompartments,
171206
171809
  onPublished: () => {
171207
- signalPiHistoryRefresh(sessionId);
171208
- signalPiPendingMaterialization(sessionId);
171810
+ signalPiDeferredHistoryRefresh(sessionId);
171811
+ signalPiDeferredMaterialization(sessionId);
171209
171812
  historian.onStatusChange?.(ctx, sessionId);
171210
171813
  }
171814
+ }).then((didPublish) => {
171815
+ if (didPublish === true) {
171816
+ markPiCompressorRun(sessionId);
171817
+ }
171211
171818
  }).catch((err) => {
171212
171819
  sessionLog(sessionId, `compressor failed in background: ${err instanceof Error ? err.message : String(err)}`);
171213
171820
  }).finally(() => {
@@ -171262,8 +171869,8 @@ function spawnPiHistorianRun(args) {
171262
171869
  } catch (err) {
171263
171870
  sessionLog(sessionId, `historian: clearEmergencyRecovery failed: ${err instanceof Error ? err.message : String(err)}`);
171264
171871
  }
171265
- signalPiHistoryRefresh(sessionId);
171266
- signalPiPendingMaterialization(sessionId);
171872
+ signalPiDeferredHistoryRefresh(sessionId);
171873
+ signalPiDeferredMaterialization(sessionId);
171267
171874
  historian.onStatusChange?.(ctx, sessionId);
171268
171875
  }
171269
171876
  }).finally(() => {
@@ -171393,6 +172000,12 @@ async function runPipeline(args) {
171393
172000
  logTransformTiming(args.sessionId, "injectTemporalMarkers", tTemporal);
171394
172001
  }
171395
172002
  const transcript = createPiTranscript(args.messages, args.sessionId);
172003
+ const currentTurnId = (() => {
172004
+ const ids = buildPiMessageIdByIndex(args.messages, args.entryIds ?? null);
172005
+ return findLatestUserMessageIdPi(args.messages, ids)?.messageId ?? null;
172006
+ })();
172007
+ const alreadyRanHeuristicsThisTurn = currentTurnId !== null && lastHeuristicsTurnIdBySession.get(args.sessionId) === currentTurnId;
172008
+ const shouldRunHeuristics = args.heuristics !== undefined && (args.forceMaterialization === true || args.schedulerDecision === "execute" && !alreadyRanHeuristicsThisTurn);
171396
172009
  const tTag = performance.now();
171397
172010
  const { targets } = tagTranscript(args.sessionId, transcript, args.tagger, args.db, {
171398
172011
  skipPrefixInjection: !args.ctxReduceEnabled
@@ -171413,10 +172026,16 @@ async function runPipeline(args) {
171413
172026
  sessionLog(args.sessionId, `commit-detect failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
171414
172027
  }
171415
172028
  const hasPendingMaterializeSignal = hasPendingMaterialization(args.sessionId);
172029
+ const deferredMaterializationWasPending = deferredMaterializationSessions.has(args.sessionId);
172030
+ const deferredHistoryRefreshWasPending = deferredHistoryRefreshSessions.has(args.sessionId);
171416
172031
  const pendingOps = getPendingOps(args.db, args.sessionId);
171417
- const shouldApplyPendingOps = args.schedulerDecision === "execute" || args.forceMaterialization || hasPendingMaterializeSignal;
172032
+ const baseShouldApplyPendingOps = args.schedulerDecision === "execute" || args.forceMaterialization || hasPendingMaterializeSignal;
172033
+ const canConsumeDeferredLate = baseShouldApplyPendingOps || shouldRunHeuristics;
172034
+ const deferredMaterialize = canConsumeDeferredLate && deferredMaterializationWasPending;
172035
+ const deferredHistoryRefresh = canConsumeDeferredLate && deferredHistoryRefreshWasPending;
172036
+ const shouldApplyPendingOps = baseShouldApplyPendingOps || deferredMaterialize;
171418
172037
  if (shouldApplyPendingOps) {
171419
- const applyReason = hasPendingMaterializeSignal ? "explicit_flush" : args.forceMaterialization ? "force_materialization" : `scheduler_execute (scheduler=${args.schedulerDecision})`;
172038
+ const applyReason = hasPendingMaterializeSignal ? "explicit_flush" : deferredMaterialize ? "deferred_publication" : args.forceMaterialization ? "force_materialization" : `scheduler_execute (scheduler=${args.schedulerDecision})`;
171420
172039
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
171421
172040
  try {
171422
172041
  const tApplyPending = performance.now();
@@ -171426,6 +172045,9 @@ async function runPipeline(args) {
171426
172045
  if (hasPendingMaterializeSignal) {
171427
172046
  consumePendingMaterialization(args.sessionId);
171428
172047
  }
172048
+ if (deferredMaterialize) {
172049
+ consumeDeferredMaterialization(args.sessionId);
172050
+ }
171429
172051
  } catch (err) {
171430
172052
  sessionLog(args.sessionId, `pending operations failed: ${err instanceof Error ? err.message : String(err)}`);
171431
172053
  throw err;
@@ -171482,7 +172104,6 @@ async function runPipeline(args) {
171482
172104
  const tActiveTags = performance.now();
171483
172105
  const activeTags = getActiveTagsBySession(args.db, args.sessionId);
171484
172106
  logTransformTiming(args.sessionId, "getActiveTagsBySession", tActiveTags, `count=${activeTags.length}`);
171485
- const shouldRunHeuristics = args.heuristics !== undefined && (args.schedulerDecision === "execute" || args.forceMaterialization === true);
171486
172107
  if (shouldRunHeuristics) {
171487
172108
  const reason = args.forceMaterialization ? "force_materialization" : `scheduler_execute (pendingOps=${pendingOps.length}, scheduler=${args.schedulerDecision})`;
171488
172109
  sessionLog(args.sessionId, `heuristics WILL RUN — reason=${reason}, context=${args.contextUsage.percentage.toFixed(1)}%, turn=n/a`);
@@ -171502,6 +172123,9 @@ async function runPipeline(args) {
171502
172123
  }, activeTags);
171503
172124
  heuristicsExecuted = true;
171504
172125
  executedWorkThisPass = true;
172126
+ if (currentTurnId !== null) {
172127
+ lastHeuristicsTurnIdBySession.set(args.sessionId, currentTurnId);
172128
+ }
171505
172129
  logTransformTiming(args.sessionId, "applyHeuristicCleanup", tHeuristic, `droppedTools=${heuristicsResult.droppedTools} deduplicatedTools=${heuristicsResult.deduplicatedTools} droppedInjections=${heuristicsResult.droppedInjections} compressedTextTags=${heuristicsResult.compressedTextTags}`);
171506
172130
  } catch (err) {
171507
172131
  sessionLog(args.sessionId, `heuristic cleanup failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
@@ -171510,21 +172134,26 @@ async function runPipeline(args) {
171510
172134
  if (args.reasoningClearing && (args.schedulerDecision === "execute" || args.forceMaterialization === true)) {
171511
172135
  try {
171512
172136
  const tClearReasoning = performance.now();
172137
+ const meta3 = getOrCreateSessionMeta(args.db, args.sessionId);
172138
+ const prevWatermark = meta3.clearedReasoningThroughTag ?? 0;
171513
172139
  const clearOutcome = clearOldReasoningPi({
171514
172140
  messages: args.messages,
171515
172141
  messageIdToMaxTag,
171516
172142
  clearReasoningAge: args.reasoningClearing.clearReasoningAge,
171517
172143
  piMessageStableId
171518
172144
  });
171519
- const meta3 = getOrCreateSessionMeta(args.db, args.sessionId);
171520
- const prevWatermark = meta3.clearedReasoningThroughTag ?? 0;
171521
- if (clearOutcome.cleared > 0 && clearOutcome.newWatermark > 0) {
171522
- if (clearOutcome.newWatermark > prevWatermark) {
171523
- updateSessionMeta(args.db, args.sessionId, {
171524
- clearedReasoningThroughTag: clearOutcome.newWatermark
171525
- });
171526
- sessionLog(args.sessionId, `reasoning cleanup: cleared=${clearOutcome.cleared} inlineStripped=0 watermark=${prevWatermark}→${clearOutcome.newWatermark}`);
171527
- }
172145
+ const stripOutcome = stripInlineThinkingPi({
172146
+ messages: args.messages,
172147
+ messageIdToMaxTag,
172148
+ clearReasoningAge: args.reasoningClearing.clearReasoningAge,
172149
+ piMessageStableId
172150
+ });
172151
+ const combinedWatermark = Math.max(clearOutcome.newWatermark, stripOutcome.newWatermark);
172152
+ if (combinedWatermark > prevWatermark) {
172153
+ updateSessionMeta(args.db, args.sessionId, {
172154
+ clearedReasoningThroughTag: combinedWatermark
172155
+ });
172156
+ sessionLog(args.sessionId, `reasoning cleanup: cleared=${clearOutcome.cleared} inlineStripped=${stripOutcome.stripped} watermark=${prevWatermark}→${combinedWatermark}`);
171528
172157
  }
171529
172158
  logTransformTiming(args.sessionId, "clearOldReasoning", tClearReasoning);
171530
172159
  logTransformTiming(args.sessionId, "watermarkCleanup", tClearReasoning);
@@ -171538,10 +172167,13 @@ async function runPipeline(args) {
171538
172167
  if (args.injection) {
171539
172168
  try {
171540
172169
  const tInjection = performance.now();
171541
- injectionResult = injectSessionHistoryIntoPi(args.db, args.sessionId, args.messages, args.isCacheBusting, args.projectIdentity, args.injection.injectionBudgetTokens, args.injection.temporalAwareness, args.entryIds);
172170
+ injectionResult = injectSessionHistoryIntoPi(args.db, args.sessionId, args.messages, args.isCacheBusting || deferredHistoryRefresh, args.projectIdentity, args.injection.injectionBudgetTokens, args.injection.temporalAwareness, args.entryIds);
171542
172171
  if (args.isCacheBusting) {
171543
172172
  historyRefreshSessions.delete(args.sessionId);
171544
172173
  }
172174
+ if (deferredHistoryRefresh) {
172175
+ consumeDeferredHistoryRefresh(args.sessionId);
172176
+ }
171545
172177
  logTransformTiming(args.sessionId, "prepareCompartmentInjection", tInjection);
171546
172178
  logTransformTiming(args.sessionId, "compartmentPhase", tInjection);
171547
172179
  } catch (err) {
@@ -171604,6 +172236,29 @@ function applyRollingNudge(args) {
171604
172236
  return messages;
171605
172237
  return injectPiNudge(messages, nudge);
171606
172238
  }
172239
+ function applyStickyTurnReminder(args) {
172240
+ const reminder = getPersistedStickyTurnReminder(args.db, args.sessionId);
172241
+ if (!reminder)
172242
+ return args.messages;
172243
+ if (args.hasRecentReduceCall && args.isCacheBustingPass) {
172244
+ clearPersistedStickyTurnReminder(args.db, args.sessionId);
172245
+ return args.messages;
172246
+ }
172247
+ const messageIdByIndex = buildPiMessageIdByIndex(args.messages, args.entryIds);
172248
+ if (reminder.messageId) {
172249
+ const reinjected = appendReminderToUserMessageByIdPi(args.messages, messageIdByIndex, reminder.messageId, reminder.text);
172250
+ if (!reinjected && args.isCacheBustingPass) {
172251
+ clearPersistedStickyTurnReminder(args.db, args.sessionId);
172252
+ }
172253
+ return args.messages;
172254
+ }
172255
+ const latest = findLatestUserMessageIdPi(args.messages, messageIdByIndex);
172256
+ if (latest) {
172257
+ appendReminderToPiUserMessage(args.messages[latest.index], reminder.text);
172258
+ setPersistedStickyTurnReminder(args.db, args.sessionId, reminder.text, latest.messageId);
172259
+ }
172260
+ return args.messages;
172261
+ }
171607
172262
  function applyNoteNudges(args) {
171608
172263
  const { sessionId, db, messages, projectIdentity, entryIds, isCacheBusting } = args;
171609
172264
  const messageIdByIndex = buildPiMessageIdByIndex(messages, entryIds);
@@ -171743,12 +172398,26 @@ function clearContextHandlerSession(sessionId) {
171743
172398
  historyRefreshSessions.delete(sessionId);
171744
172399
  pendingMaterializationSessions.delete(sessionId);
171745
172400
  systemPromptRefreshSessions.delete(sessionId);
172401
+ deferredHistoryRefreshSessions.delete(sessionId);
172402
+ deferredMaterializationSessions.delete(sessionId);
171746
172403
  firstContextPassSeenBySession.delete(sessionId);
171747
172404
  commitSeenLastPass.delete(sessionId);
171748
172405
  recentReduceBySession.delete(sessionId);
171749
172406
  liveModelBySession.delete(sessionId);
171750
172407
  toolUsageSinceUserTurn.delete(sessionId);
171751
172408
  latestUserMessageBySession.delete(sessionId);
172409
+ lastHeuristicsTurnIdBySession.delete(sessionId);
172410
+ lastSeenProjectIdentityBySession.delete(sessionId);
172411
+ for (const [projectIdentity, sessions] of sessionsByProject) {
172412
+ sessions.delete(sessionId);
172413
+ if (sessions.size === 0)
172414
+ sessionsByProject.delete(projectIdentity);
172415
+ }
172416
+ const unregister = rawMessageProviderUnregistersBySession.get(sessionId);
172417
+ if (unregister) {
172418
+ unregister();
172419
+ rawMessageProviderUnregistersBySession.delete(sessionId);
172420
+ }
171752
172421
  clearSessionTracking(sessionId);
171753
172422
  clearPiCompressorState(sessionId);
171754
172423
  }
@@ -171799,7 +172468,7 @@ init_logger();
171799
172468
  init_data_path();
171800
172469
  init_logger();
171801
172470
  await init_sqlite();
171802
- import { join as join14 } from "node:path";
172471
+ import { join as join16 } from "node:path";
171803
172472
  var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
171804
172473
  function randomBase62(length) {
171805
172474
  const chars = [];
@@ -171820,7 +172489,7 @@ function generatePartId(timestampMs, counter = 0n) {
171820
172489
  return generateId("prt", timestampMs, counter);
171821
172490
  }
171822
172491
  function getOpenCodeDbPath3() {
171823
- return join14(getDataDir(), "opencode", "opencode.db");
172492
+ return join16(getDataDir(), "opencode", "opencode.db");
171824
172493
  }
171825
172494
  var cachedWriteDb = null;
171826
172495
  var REQUIRED_MESSAGE_COLUMNS = ["id", "session_id", "time_created", "time_updated", "data"];
@@ -172378,7 +173047,7 @@ async function runCompressorPass2(args) {
172378
173047
 
172379
173048
  // ../plugin/src/hooks/magic-context/compartment-runner-historian.ts
172380
173049
  import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
172381
- import { join as join15 } from "node:path";
173050
+ import { join as join17 } from "node:path";
172382
173051
  init_data_path();
172383
173052
  function historianResponseDumpDir(directory) {
172384
173053
  return getProjectMagicContextHistorianDir(directory);
@@ -172632,7 +173301,7 @@ function dumpHistorianResponse(sessionId, directory, label, text) {
172632
173301
  mkdirSync6(dumpDir, { recursive: true });
172633
173302
  const safeSessionId = sanitizeDumpName(sessionId);
172634
173303
  const safeLabel = sanitizeDumpName(label);
172635
- const dumpPath = join15(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
173304
+ const dumpPath = join17(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
172636
173305
  writeFileSync3(dumpPath, text, "utf8");
172637
173306
  sessionLog(sessionId, "compartment agent: historian response dumped", {
172638
173307
  label,
@@ -173838,7 +174507,7 @@ import {
173838
174507
  // package.json
173839
174508
  var package_default = {
173840
174509
  name: "@cortexkit/pi-magic-context",
173841
- version: "0.21.0",
174510
+ version: "0.21.1",
173842
174511
  type: "module",
173843
174512
  description: "Pi coding agent extension for Magic Context — cross-session memory and context management",
173844
174513
  main: "dist/index.js",
@@ -174290,6 +174959,8 @@ function registerCtxStatusCommand(pi, deps) {
174290
174959
  pi.registerCommand("ctx-status", {
174291
174960
  description: "Show Magic Context status for the current Pi session",
174292
174961
  handler: async (_args, ctx) => {
174962
+ const projectIdentity = deps.resolveProject?.(ctx).projectIdentity ?? deps.projectIdentity;
174963
+ const currentDeps = { ...deps, projectIdentity };
174293
174964
  const sessionId = resolveSessionId(ctx);
174294
174965
  if (!sessionId) {
174295
174966
  sendCtxStatusMessage(pi, {
@@ -174303,13 +174974,13 @@ No active Pi session is available.`,
174303
174974
  }
174304
174975
  try {
174305
174976
  if (ctx.hasUI) {
174306
- await showStatusDialog(pi, ctx, deps);
174977
+ await showStatusDialog(pi, ctx, currentDeps);
174307
174978
  return;
174308
174979
  }
174309
174980
  const usage = ctx.getContextUsage?.();
174310
174981
  const modelKey = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
174311
- const statusText = executeStatus(deps.db, sessionId, deps.protectedTags ?? 20, deps.nudgeIntervalTokens, deps.executeThresholdPercentage, modelKey, deps.historyBudgetPercentage, deps.commitClusterTrigger, deps.executeThresholdTokens, usage?.contextWindow);
174312
- const details = buildStatusDetails(deps, sessionId);
174982
+ const statusText = executeStatus(currentDeps.db, sessionId, currentDeps.protectedTags ?? 20, currentDeps.nudgeIntervalTokens, currentDeps.executeThresholdPercentage, modelKey, currentDeps.historyBudgetPercentage, currentDeps.commitClusterTrigger, currentDeps.executeThresholdTokens, usage?.contextWindow);
174983
+ const details = buildStatusDetails(currentDeps, sessionId);
174313
174984
  sendCtxStatusMessage(pi, { title: "/ctx-status", text: statusText, level: "info" }, details);
174314
174985
  } catch (error51) {
174315
174986
  sendCtxStatusMessage(pi, {
@@ -174434,405 +175105,6 @@ function stripTagPrefixFromAssistantMessage(message) {
174434
175105
  return mutated;
174435
175106
  }
174436
175107
 
174437
- // src/system-prompt.ts
174438
- import { createHash as createHash8 } from "node:crypto";
174439
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
174440
- import { join as join17 } from "node:path";
174441
-
174442
- // ../plugin/src/agents/magic-context-prompt.ts
174443
- function getToolHistoryGuidance(dropToolStructure) {
174444
- if (dropToolStructure) {
174445
- return `Compressed history intentionally omits tool calls and their outputs — summaries like "I edited file X" are historian records, not patterns to replicate. In the live conversation, older tool calls and their results are cleaned up to save context — you may see your own past messages referencing actions without the corresponding tool call or result visible. This is normal context management. ALWAYS use real tool calls; never simulate, fabricate, or inline tool outputs in your text. If there is no tool result message, the action did not happen. NEVER simulate, hallucinate or claim tool calls, command output, search results, file edits, or diffs in plain text as if they actually occurred.`;
174446
- }
174447
- return `Older tool calls in your conversation show truncated inputs and [truncated] outputs — this is normal context management, not a pattern to follow. The original tool calls executed successfully with full inputs and produced real outputs that were later cleaned up to save context. ALWAYS use real tool calls with complete arguments; never copy truncated patterns like "filePa...[truncated]" into your tool inputs. If you need to re-read a file or re-run a command, make a fresh tool call.`;
174448
- }
174449
- var BASE_INTRO = (protectedTags, dropToolStructure) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
174450
- Use \`ctx_reduce\` to manage context size. It supports one operation:
174451
- - \`drop\`: Remove entirely (best for tool outputs you already acted on).
174452
- Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
174453
- Use \`ctx_note\` for deferred intentions — things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
174454
- Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
174455
- **Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
174456
- - Found a project's source code path after searching → \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
174457
- - Discovered a non-obvious build/test command → \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
174458
- - Learned a constraint the hard way → \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
174459
- Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
174460
- Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
174461
- **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
174462
- - Can't remember where a related codebase or dependency lives → \`ctx_search(query="opencode source code path")\`
174463
- - Forgot a prior architectural decision or constraint → \`ctx_search(query="why did we choose SQLite over postgres")\`
174464
- - Need a config value, API key location, or environment detail → \`ctx_search(query="embedding provider configuration")\`
174465
- - Looking for how something was implemented previously → \`ctx_search(query="how does the dreamer lease work")\`
174466
- - Want to recall what was decided in an earlier conversation → \`ctx_search(query="dashboard release signing setup")\`
174467
- \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
174468
- ${getToolHistoryGuidance(dropToolStructure)}
174469
- NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
174470
- NEVER drop user messages — they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
174471
- NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
174472
- Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
174473
- var BASE_INTRO_NO_REDUCE = (dropToolStructure) => `Use \`ctx_note\` for deferred intentions — things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
174474
- Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
174475
- **Save to memory proactively**: If you spent multiple turns finding something (a file path, a DB location, a config pattern, a workaround), save it with \`ctx_memory\` so future sessions don't repeat the search. Examples:
174476
- - Found a project's source code path after searching → \`ctx_memory(action="write", category="ENVIRONMENT", content="OpenCode source is at ~/Work/OSS/opencode")\`
174477
- - Discovered a non-obvious build/test command → \`ctx_memory(action="write", category="WORKFLOW_RULES", content="Always use scripts/release.sh for releases")\`
174478
- - Learned a constraint the hard way → \`ctx_memory(action="write", category="CONSTRAINTS", content="Dashboard Tauri build needs RGBA PNGs, not grayscale")\`
174479
- Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
174480
- Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start="N" end="M">\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
174481
- **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
174482
- - Can't remember where a related codebase or dependency lives → \`ctx_search(query="opencode source code path")\`
174483
- - Forgot a prior architectural decision or constraint → \`ctx_search(query="why did we choose SQLite over postgres")\`
174484
- - Need a config value, API key location, or environment detail → \`ctx_search(query="embedding provider configuration")\`
174485
- - Looking for how something was implemented previously → \`ctx_search(query="how does the dreamer lease work")\`
174486
- - Want to recall what was decided in an earlier conversation → \`ctx_search(query="dashboard release signing setup")\`
174487
- \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
174488
- ${getToolHistoryGuidance(dropToolStructure)}`;
174489
- var GENERIC_SECTION = `
174490
- ### Reduction Triggers
174491
- - After reading files or search results you already acted on — drop raw outputs.
174492
- - After completing a logical step — drop intermediate outputs from that step.
174493
- - Between major context switches — when moving to a new task area.
174494
-
174495
- ### What to Drop
174496
- - Large file reads, grep results, and tool outputs you already used.
174497
- - Large build/test output after you analyzed and acted on it.
174498
- - Old diagnostic or exploration results that are no longer relevant.
174499
-
174500
- ### What to Keep
174501
- - ALL user messages and assistant conversation text — these are cheap and compartmentalized automatically.
174502
- - Your current task requirements and constraints.
174503
- - Recent errors and unresolved decisions.
174504
- - Active work context and files being edited.`;
174505
- var TEMPORAL_AWARENESS_GUIDANCE = `
174506
- **Temporal awareness**: User messages may be preceded by HTML comments like \`<!-- +12m -->\`, \`<!-- +2h 15m -->\`, or \`<!-- +3d 4h -->\` indicating time elapsed since the previous message's completion. Compartments in \`<session-history>\` carry \`start-date\` and \`end-date\` attributes (YYYY-MM-DD) showing real-time boundaries. Use these when reasoning about workflow pacing, log durations, build times, or how long ago something happened.`;
174507
- var CAVEMAN_COMPRESSION_WARNING = `
174508
- **BEWARE**: History compression is on; older user AND assistant text — including your own earlier responses — has been deterministically rewritten in a terse caveman style (dropped articles, missing auxiliaries, \`//\` instead of connectives like \`because\`). This is automatic context compression that runs after the fact, not your actual prior wording or the user's. **DO NOT mimic this style in new turns.** Write fresh responses in normal prose. If you notice your output drifting into caveman cadence, that drift is in-context-learning bleeding from the compressed history — consciously revert to full sentences.`;
174509
- function buildMagicContextSection(_agent, protectedTags, ctxReduceEnabled = true, dreamerEnabled = false, dropToolStructure = true, temporalAwarenessEnabled = false, cavemanTextCompressionEnabled = false) {
174510
- const smartNoteGuidance = dreamerEnabled ? `
174511
- When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
174512
- The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
174513
- Example: \`ctx_note(action="write", content="Implement X because Y", surface_condition="When PR #42 is merged in this repo")\`` : "";
174514
- const temporalGuidance = temporalAwarenessEnabled ? TEMPORAL_AWARENESS_GUIDANCE : "";
174515
- const cavemanWarning = cavemanTextCompressionEnabled && !ctxReduceEnabled ? CAVEMAN_COMPRESSION_WARNING : "";
174516
- if (!ctxReduceEnabled) {
174517
- return `## Magic Context
174518
-
174519
- ${BASE_INTRO_NO_REDUCE(dropToolStructure)}${smartNoteGuidance}${temporalGuidance}${cavemanWarning}`;
174520
- }
174521
- return `## Magic Context
174522
-
174523
- ${BASE_INTRO(protectedTags, dropToolStructure)}${smartNoteGuidance}${temporalGuidance}
174524
- ${GENERIC_SECTION}
174525
-
174526
- Prefer many small targeted operations over one large blanket operation. Compress early and often — don't wait for warnings.`;
174527
- }
174528
-
174529
- // ../plugin/src/hooks/magic-context/key-files-block.ts
174530
- import { readFileSync as readFileSync7, realpathSync as realpathSync3 } from "node:fs";
174531
- import { join as join16, sep as sep2 } from "node:path";
174532
- init_logger();
174533
- var cachedKeyFilesBySession = new Map;
174534
- var staleUpdates = new Map;
174535
- function staleKey(update) {
174536
- return `${update.projectPath}\x00${update.path}\x00${update.generatedAtWitness}\x00${update.staleReason}`;
174537
- }
174538
- function queueStaleUpdate(projectPath, path5, generatedAtWitness, staleReason) {
174539
- const update = { projectPath, path: path5, generatedAtWitness, staleReason };
174540
- staleUpdates.set(staleKey(update), update);
174541
- }
174542
- function flushStaleUpdates(db) {
174543
- if (staleUpdates.size === 0)
174544
- return 0;
174545
- const updates = [...staleUpdates.values()];
174546
- staleUpdates.clear();
174547
- const stmt = db.prepare(`UPDATE project_key_files
174548
- SET stale_reason = ?1
174549
- WHERE project_path = ?2
174550
- AND path = ?3
174551
- AND generated_at = ?4
174552
- AND (stale_reason IS NULL OR stale_reason != ?1)`);
174553
- let changed = 0;
174554
- for (const update of updates) {
174555
- try {
174556
- changed += stmt.run(update.staleReason, update.projectPath, update.path, update.generatedAtWitness).changes;
174557
- } catch (error51) {
174558
- log("[key-files] flushStaleUpdates failed:", error51);
174559
- }
174560
- }
174561
- return changed;
174562
- }
174563
- function clearKeyFilesCacheForSession(sessionId) {
174564
- cachedKeyFilesBySession.delete(sessionId);
174565
- }
174566
- function isUnderProject(projectPath, absPath) {
174567
- const root = realpathSync3(projectPath);
174568
- return absPath.startsWith(root + sep2) || absPath === root;
174569
- }
174570
- function buildKeyFilesBlock(db, projectPath, config2 = { enabled: true, tokenBudget: 1e4 }) {
174571
- if (!config2.enabled)
174572
- return null;
174573
- if (!isAftAvailable())
174574
- return null;
174575
- const rows = readCurrentKeyFiles(db, projectPath);
174576
- if (rows.length === 0)
174577
- return null;
174578
- for (const row of rows) {
174579
- if (row.staleReason !== null)
174580
- continue;
174581
- let nextStale = null;
174582
- let observed = false;
174583
- try {
174584
- const absPath = join16(projectPath, row.path);
174585
- const real = realpathSync3(absPath);
174586
- if (!isUnderProject(projectPath, real)) {
174587
- nextStale = "missing";
174588
- observed = true;
174589
- } else {
174590
- const diskHash = sha256(readFileSync7(real));
174591
- if (diskHash !== row.contentHash)
174592
- nextStale = "content_drift";
174593
- observed = true;
174594
- }
174595
- } catch (error51) {
174596
- const code = error51?.code;
174597
- if (code === "ENOENT" || code === "ELOOP") {
174598
- nextStale = "missing";
174599
- observed = true;
174600
- } else {
174601
- log(`[key-files] freshness check transient failure: ${row.path}: ${error51 instanceof Error ? error51.message : String(error51)}`);
174602
- }
174603
- }
174604
- if (observed && nextStale !== null) {
174605
- queueStaleUpdate(row.projectPath, row.path, row.generatedAt, nextStale);
174606
- }
174607
- }
174608
- const blocks = [];
174609
- let used = 0;
174610
- for (const row of rows) {
174611
- if (used + row.localTokenEstimate > config2.tokenBudget)
174612
- break;
174613
- blocks.push(` <key-file path="${escapeXmlAttr(row.path)}">
174614
- ${escapeXmlContent(row.content)}
174615
- </key-file>`);
174616
- used += row.localTokenEstimate;
174617
- }
174618
- if (blocks.length === 0)
174619
- return null;
174620
- const rendered = `<key-files>
174621
- ${blocks.join(`
174622
- `)}
174623
- </key-files>`;
174624
- flushStaleUpdates(db);
174625
- return rendered;
174626
- }
174627
- function readVersionedKeyFiles(args) {
174628
- const config2 = args.config ?? { enabled: true, tokenBudget: 1e4 };
174629
- if (args.sessionMeta.isSubagent)
174630
- return null;
174631
- if (!config2.enabled)
174632
- return null;
174633
- if (!isAftAvailable())
174634
- return null;
174635
- const projectPath = resolveProjectPath(args.directory ?? args.sessionMeta.sessionId);
174636
- if (!projectPath)
174637
- return null;
174638
- const currentVersion = getKeyFilesVersion(args.db, projectPath);
174639
- if (args.sessionId) {
174640
- const cached2 = cachedKeyFilesBySession.get(args.sessionId);
174641
- if (cached2 && !args.isCacheBusting && cached2.version === currentVersion) {
174642
- return cached2.value;
174643
- }
174644
- }
174645
- const value = buildKeyFilesBlock(args.db, projectPath, config2);
174646
- if (args.sessionId) {
174647
- cachedKeyFilesBySession.set(args.sessionId, { value, version: currentVersion });
174648
- if (value)
174649
- sessionLog(args.sessionId, `loaded key-files block (v${currentVersion}, ${value.length} chars)`);
174650
- }
174651
- return value;
174652
- }
174653
-
174654
- // src/system-prompt.ts
174655
- init_logger();
174656
- var PROJECT_DOCS_MARKER = "<project-docs>";
174657
- var USER_PROFILE_MARKER = "<user-profile>";
174658
- var stickyDateBySession = new Map;
174659
- var cachedDocsBySession = new Map;
174660
- var cachedUserProfileBySession = new Map;
174661
- var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
174662
- function readProjectDocs(directory) {
174663
- const sections = [];
174664
- for (const filename of DOC_FILES) {
174665
- const filePath = join17(directory, filename);
174666
- try {
174667
- if (existsSync11(filePath)) {
174668
- const content = readFileSync8(filePath, "utf-8").trim();
174669
- if (content.length > 0) {
174670
- sections.push(`<${filename}>
174671
- ${content}
174672
- </${filename}>`);
174673
- }
174674
- }
174675
- } catch (error51) {
174676
- log(`[magic-context-pi] failed to read ${filename}:`, error51);
174677
- }
174678
- }
174679
- if (sections.length === 0)
174680
- return null;
174681
- return `${PROJECT_DOCS_MARKER}
174682
- ${sections.join(`
174683
-
174684
- `)}
174685
- </project-docs>`;
174686
- }
174687
- function buildUserProfileBlock(db) {
174688
- const memories = getActiveUserMemories(db);
174689
- if (memories.length === 0)
174690
- return null;
174691
- const items = memories.map((m) => `- ${m.content}`).join(`
174692
- `);
174693
- return `${USER_PROFILE_MARKER}
174694
- ${items}
174695
- </user-profile>`;
174696
- }
174697
- function buildMagicContextBlock(opts) {
174698
- const sections = [];
174699
- const sessionId = opts.sessionId;
174700
- const isCacheBusting = opts.isCacheBusting ?? false;
174701
- if (opts.injectDocs) {
174702
- const docsBlock = readCachedAdjunct({
174703
- cache: cachedDocsBySession,
174704
- sessionId,
174705
- isCacheBusting,
174706
- compute: () => readProjectDocs(opts.cwd),
174707
- describe: "project docs"
174708
- });
174709
- if (docsBlock)
174710
- sections.push(docsBlock);
174711
- }
174712
- if (opts.userMemoriesEnabled) {
174713
- const profileBlock = readCachedAdjunct({
174714
- cache: cachedUserProfileBySession,
174715
- sessionId,
174716
- isCacheBusting,
174717
- compute: () => buildUserProfileBlock(opts.db),
174718
- describe: "user profile"
174719
- });
174720
- if (profileBlock)
174721
- sections.push(profileBlock);
174722
- }
174723
- if (opts.pinKeyFilesEnabled && sessionId) {
174724
- let sessionMeta = null;
174725
- try {
174726
- sessionMeta = getOrCreateSessionMeta(opts.db, sessionId);
174727
- } catch (error51) {
174728
- sessionLog(sessionId, "Pi key-files session meta load failed:", error51);
174729
- }
174730
- const keyFilesBlock = sessionMeta ? readVersionedKeyFiles({
174731
- db: opts.db,
174732
- sessionId,
174733
- sessionMeta,
174734
- directory: opts.cwd,
174735
- isCacheBusting,
174736
- config: {
174737
- enabled: opts.pinKeyFilesEnabled,
174738
- tokenBudget: opts.pinKeyFilesTokenBudget ?? 1e4
174739
- }
174740
- }) : null;
174741
- if (keyFilesBlock)
174742
- sections.push(keyFilesBlock);
174743
- }
174744
- const dataBlock = sections.length > 0 ? `<magic-context>
174745
- ${sections.join(`
174746
-
174747
- `)}
174748
- </magic-context>` : null;
174749
- const includeGuidance = opts.includeGuidance ?? true;
174750
- if (!includeGuidance) {
174751
- return dataBlock;
174752
- }
174753
- const guidance = buildMagicContextSection(null, opts.protectedTags ?? 20, opts.ctxReduceEnabled ?? true, opts.dreamerEnabled ?? false, opts.dropToolStructure ?? true, opts.temporalAwarenessEnabled ?? false, opts.cavemanTextCompressionEnabled ?? false);
174754
- if (dataBlock) {
174755
- return `${guidance}
174756
-
174757
- ${dataBlock}`;
174758
- }
174759
- return guidance;
174760
- }
174761
- function readCachedAdjunct(args) {
174762
- const { cache, sessionId, isCacheBusting, compute, describe: describe3 } = args;
174763
- if (!sessionId) {
174764
- return compute();
174765
- }
174766
- const hasCached = cache.has(sessionId);
174767
- if (!hasCached || isCacheBusting) {
174768
- const value = compute();
174769
- cache.set(sessionId, value);
174770
- if (value && !hasCached) {
174771
- sessionLog(sessionId, `loaded ${describe3} (${value.length} chars)`);
174772
- } else if (value && isCacheBusting) {
174773
- sessionLog(sessionId, `refreshed ${describe3} (cache-busting pass)`);
174774
- }
174775
- return value;
174776
- }
174777
- return cache.get(sessionId) ?? null;
174778
- }
174779
- var DATE_PATTERN = /Today's date: .+/;
174780
- function processSystemPromptForCache(args) {
174781
- const { db, sessionId, systemPrompt, isCacheBusting } = args;
174782
- let sessionMeta;
174783
- try {
174784
- sessionMeta = getOrCreateSessionMeta(db, sessionId);
174785
- } catch (error51) {
174786
- sessionLog(sessionId, "system-prompt-hash session meta load failed:", error51);
174787
- }
174788
- const previousHash = sessionMeta?.systemPromptHash ?? "";
174789
- const isFirstHash = previousHash === "" || previousHash === "0";
174790
- let frozenPrompt = systemPrompt;
174791
- const dateMatch = systemPrompt.match(DATE_PATTERN);
174792
- const liveDate = dateMatch ? dateMatch[0] : null;
174793
- const stickyDate = stickyDateBySession.get(sessionId);
174794
- if (liveDate && !stickyDate) {
174795
- stickyDateBySession.set(sessionId, liveDate);
174796
- } else if (liveDate && stickyDate && liveDate !== stickyDate) {
174797
- if (isCacheBusting) {
174798
- stickyDateBySession.set(sessionId, liveDate);
174799
- sessionLog(sessionId, `system prompt date updated: ${stickyDate} → ${liveDate} (cache-busting pass)`);
174800
- } else {
174801
- frozenPrompt = systemPrompt.replace(DATE_PATTERN, stickyDate);
174802
- sessionLog(sessionId, `system prompt date frozen: real=${liveDate}, using=${stickyDate} (cache-stable pass)`);
174803
- }
174804
- }
174805
- const currentHash = createHash8("md5").update(frozenPrompt).digest("hex");
174806
- const hashChanged = !isFirstHash && currentHash !== previousHash;
174807
- if (hashChanged) {
174808
- sessionLog(sessionId, `system prompt hash changed: ${previousHash} → ${currentHash} (len=${frozenPrompt.length})`);
174809
- } else if (isFirstHash) {
174810
- sessionLog(sessionId, `system prompt hash initialized: ${currentHash} (len=${frozenPrompt.length})`);
174811
- }
174812
- const systemPromptTokens = estimateTokens(frozenPrompt);
174813
- if (sessionMeta) {
174814
- if (currentHash !== previousHash) {
174815
- updateSessionMeta(db, sessionId, {
174816
- systemPromptHash: currentHash,
174817
- systemPromptTokens
174818
- });
174819
- } else if (Math.abs(sessionMeta.systemPromptTokens - systemPromptTokens) > 50) {
174820
- updateSessionMeta(db, sessionId, { systemPromptTokens });
174821
- }
174822
- }
174823
- return {
174824
- systemPrompt: frozenPrompt,
174825
- hashChanged,
174826
- currentHash
174827
- };
174828
- }
174829
- function clearPiSystemPromptSession(sessionId) {
174830
- stickyDateBySession.delete(sessionId);
174831
- cachedDocsBySession.delete(sessionId);
174832
- cachedUserProfileBySession.delete(sessionId);
174833
- clearKeyFilesCacheForSession(sessionId);
174834
- }
174835
-
174836
175108
  // ../plugin/src/tools/ctx-expand/constants.ts
174837
175109
  var CTX_EXPAND_DESCRIPTION = "Decompress a compartment range to see the original conversation transcript. " + 'Use start/end from <compartment start="N" end="M"> attributes. ' + "Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.";
174838
175110
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
@@ -178974,6 +179246,7 @@ var ALL_ACTIONS = [
178974
179246
  "archive"
178975
179247
  ];
178976
179248
  var DREAMER_ONLY_ACTIONS = new Set([
179249
+ "list",
178977
179250
  "update",
178978
179251
  "merge",
178979
179252
  "archive"
@@ -179062,7 +179335,7 @@ function queueEmbedding(args) {
179062
179335
  }
179063
179336
  function createCtxMemoryTool(deps) {
179064
179337
  const dreamerAllowed = deps.allowDreamerActions === true;
179065
- const description = dreamerAllowed ? "Manage cross-session project memories. Memories persist across sessions and are " + "shared with OpenCode sessions on the same project. " + "Supported actions: write, delete, list, update, merge, archive." : "Manage cross-session project memories. Memories persist across sessions and are " + "shared with OpenCode sessions on the same project. " + "Supported actions: write, delete, list.";
179338
+ const description = dreamerAllowed ? "Manage cross-session project memories. Memories persist across sessions and are " + "shared with OpenCode sessions on the same project. " + "Supported actions: write, delete, list, update, merge, archive." : "Manage cross-session project memories. Memories persist across sessions and are " + "shared with OpenCode sessions on the same project. " + "Supported actions: write, delete.";
179066
179339
  return {
179067
179340
  name: "ctx_memory",
179068
179341
  label: "Magic Context: Memory",
@@ -179655,8 +179928,10 @@ function formatResult(result, index) {
179655
179928
  ].join(`
179656
179929
  `);
179657
179930
  }
179931
+ const expandStart = Math.max(1, result.messageOrdinal - 3);
179932
+ const expandEnd = result.messageOrdinal + 3;
179658
179933
  return [
179659
- `[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} role=${result.role}`,
179934
+ `[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} range=${expandStart}-${expandEnd} role=${result.role}`,
179660
179935
  result.content
179661
179936
  ].join(`
179662
179937
  `);
@@ -179665,7 +179940,11 @@ function formatSearchResults(query, results) {
179665
179940
  if (results.length === 0) {
179666
179941
  return `No results found for "${query}" across memories, git commits, or message history.`;
179667
179942
  }
179668
- const body = results.map((result, index) => formatResult(result, index + 1)).join(`
179943
+ const bodyParts = results.map((result, index) => formatResult(result, index + 1));
179944
+ if (results.some((result) => result.source === "message")) {
179945
+ bodyParts.push("Use ctx_expand(start, end) with the range from any message result above to read the full conversation context.");
179946
+ }
179947
+ const body = bodyParts.join(`
179669
179948
 
179670
179949
  `);
179671
179950
  return `Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":
@@ -179825,6 +180104,11 @@ function registerMagicContextTools(pi, opts) {
179825
180104
 
179826
180105
  // src/index.ts
179827
180106
  var PREFIX2 = "[magic-context][pi]";
180107
+ function resolveCurrentProject(ctx) {
180108
+ const projectDir = ctx.cwd;
180109
+ const projectIdentity = resolveProjectIdentity(projectDir);
180110
+ return { projectDir, projectIdentity };
180111
+ }
179828
180112
  function persistPiMessageEndModelMeta(args) {
179829
180113
  if (!args.message || typeof args.message !== "object")
179830
180114
  return;
@@ -180015,6 +180299,7 @@ async function src_default2(pi) {
180015
180299
  }
180016
180300
  const projectDir = process.cwd();
180017
180301
  const projectIdentity = resolveProjectIdentity(projectDir);
180302
+ const seenDreamerProjectIdentities = new Set([projectIdentity]);
180018
180303
  info(`loaded v${PLUGIN_VERSION} | harness=pi | db=${dbPath} | ` + `project=${projectIdentity} | dir=${projectDir}`);
180019
180304
  const { config: config2, warnings, loadedFromPaths } = loadPiConfig({
180020
180305
  cwd: projectDir
@@ -180045,7 +180330,10 @@ async function src_default2(pi) {
180045
180330
  const historianConfig = resolveHistorianFromConfig(config2);
180046
180331
  if (historianConfig) {
180047
180332
  historianConfig.onStatusChange = (ctx) => {
180048
- updateStatusLine(ctx, { db, projectIdentity });
180333
+ updateStatusLine(ctx, {
180334
+ db,
180335
+ projectIdentity: resolveCurrentProject(ctx).projectIdentity
180336
+ });
180049
180337
  };
180050
180338
  }
180051
180339
  const nudgeConfig = resolveNudgeFromConfig(config2);
@@ -180084,12 +180372,17 @@ async function src_default2(pi) {
180084
180372
  registerCtxStatusCommand(pi, {
180085
180373
  db,
180086
180374
  projectIdentity,
180375
+ resolveProject: resolveCurrentProject,
180087
180376
  protectedTags: config2.protected_tags,
180088
180377
  nudgeIntervalTokens: config2.nudge_interval_tokens,
180089
180378
  executeThresholdPercentage: config2.execute_threshold_percentage,
180090
180379
  historyBudgetPercentage: config2.history_budget_percentage,
180091
180380
  commitClusterTrigger: config2.commit_cluster_trigger,
180092
- executeThresholdTokens: config2.execute_threshold_tokens
180381
+ executeThresholdTokens: config2.execute_threshold_tokens,
180382
+ dreamer: {
180383
+ enabled: config2.dreamer?.enabled ?? false,
180384
+ schedule: config2.dreamer?.schedule
180385
+ }
180093
180386
  });
180094
180387
  info("registered /ctx-status");
180095
180388
  registerStatusLine(pi, { db, projectIdentity });
@@ -180111,7 +180404,10 @@ async function src_default2(pi) {
180111
180404
  registerCtxDreamCommand(pi, {
180112
180405
  db,
180113
180406
  projectDir,
180114
- projectIdentity
180407
+ projectIdentity,
180408
+ resolveProject: resolveCurrentProject,
180409
+ dreamerEnabled: config2.dreamer?.enabled ?? false,
180410
+ onProjectSeen: (identity) => seenDreamerProjectIdentities.add(identity)
180115
180411
  });
180116
180412
  info("registered /ctx-dream");
180117
180413
  const dreamerConfig = resolveDreamerFromConfig(config2);
@@ -180123,7 +180419,8 @@ async function src_default2(pi) {
180123
180419
  config: dreamerConfig,
180124
180420
  embeddingConfig: config2.embedding,
180125
180421
  memoryEnabled: config2.memory.enabled,
180126
- gitCommitIndexing: config2.experimental.git_commit_indexing
180422
+ gitCommitIndexing: config2.experimental.git_commit_indexing,
180423
+ onAdjunctsRefreshNeeded: signalPiSystemPromptRefreshForProject
180127
180424
  });
180128
180425
  info(dreamerConfig.enabled ? `registered dreamer (schedule=${dreamerConfig.schedule}, tasks=[${dreamerConfig.tasks.join(",")}])` : "registered dreamer: DISABLED (dreamer.enabled=false)");
180129
180426
  } else {
@@ -180131,6 +180428,8 @@ async function src_default2(pi) {
180131
180428
  }
180132
180429
  pi.on("before_agent_start", async (event, ctx) => {
180133
180430
  try {
180431
+ const currentProject = resolveCurrentProject(ctx);
180432
+ seenDreamerProjectIdentities.add(currentProject.projectIdentity);
180134
180433
  const sm = ctx.sessionManager;
180135
180434
  let sessionId;
180136
180435
  if (sm !== undefined) {
@@ -180143,10 +180442,20 @@ async function src_default2(pi) {
180143
180442
  } catch {}
180144
180443
  }
180145
180444
  }
180445
+ if (sessionId) {
180446
+ trackSessionForProject(currentProject.projectIdentity, sessionId);
180447
+ }
180448
+ if (config2.system_prompt_injection?.enabled === false) {
180449
+ return;
180450
+ }
180451
+ const skipSigs = config2.system_prompt_injection?.skip_signatures ?? [];
180452
+ if (skipSigs.some((sig) => sig.length > 0 && event.systemPrompt.includes(sig))) {
180453
+ return;
180454
+ }
180146
180455
  const isCacheBusting = sessionId ? hasSystemPromptRefresh(sessionId) : true;
180147
180456
  const block = buildMagicContextBlock({
180148
180457
  db,
180149
- cwd: ctx.cwd,
180458
+ cwd: currentProject.projectDir,
180150
180459
  sessionId,
180151
180460
  memoryEnabled: config2.memory.enabled,
180152
180461
  injectDocs: config2.dreamer?.inject_docs ?? true,
@@ -180160,7 +180469,8 @@ async function src_default2(pi) {
180160
180469
  userMemoriesEnabled: (config2.dreamer?.enabled ?? false) && (config2.dreamer?.user_memories?.enabled ?? false),
180161
180470
  pinKeyFilesEnabled: (config2.dreamer?.enabled ?? false) && (config2.dreamer?.pin_key_files?.enabled ?? false),
180162
180471
  pinKeyFilesTokenBudget: config2.dreamer?.pin_key_files?.token_budget ?? 1e4,
180163
- isCacheBusting
180472
+ isCacheBusting,
180473
+ existingSystemPrompt: event.systemPrompt
180164
180474
  });
180165
180475
  const composedPrompt = block ? `${event.systemPrompt}
180166
180476
 
@@ -180252,10 +180562,13 @@ ${block}` : event.systemPrompt;
180252
180562
  const sessionId = sm?.getSessionId?.();
180253
180563
  if (typeof sessionId !== "string" || sessionId.length === 0)
180254
180564
  return;
180255
- const rawMessages = readPiSessionMessages(ctx);
180256
- const latest = rawMessages[rawMessages.length - 1];
180257
- if (latest?.role === "assistant" && latest.id.length > 0) {
180258
- scheduleIncrementalIndex(db, sessionId, latest.id, () => latest);
180565
+ const endedMsg = event.message;
180566
+ if (endedMsg?.role === "assistant" && typeof endedMsg.id === "string" && endedMsg.id.length > 0) {
180567
+ const messageId = endedMsg.id;
180568
+ scheduleIncrementalIndex(db, sessionId, messageId, () => {
180569
+ const rawMessages = readPiSessionMessages(ctx);
180570
+ return rawMessages.find((message) => message.id === messageId) ?? null;
180571
+ });
180259
180572
  }
180260
180573
  persistPiMessageEndModelMeta({
180261
180574
  db,
@@ -180358,7 +180671,9 @@ ${block}` : event.systemPrompt;
180358
180671
  warn("shutdown: dreamer drain threw:", err5);
180359
180672
  }
180360
180673
  try {
180361
- unregisterPiDreamerProject({ projectIdentity });
180674
+ for (const identity of seenDreamerProjectIdentities) {
180675
+ unregisterPiDreamerProject({ projectIdentity: identity });
180676
+ }
180362
180677
  } catch (err5) {
180363
180678
  warn("shutdown: unregisterPiDreamerProject threw:", err5);
180364
180679
  }