@oh-my-pi/pi-coding-agent 14.6.3 → 14.6.4

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.
@@ -17,6 +17,15 @@ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
17
17
  import { getGatewayStatus } from "../../eval/py/gateway-coordinator";
18
18
  import { loadCustomShare } from "../../export/custom-share";
19
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
20
+ import {
21
+ diffMentalModelContent,
22
+ type HindsightApi,
23
+ type HindsightSessionState,
24
+ loadHindsightConfig,
25
+ reloadMentalModelsForSession,
26
+ resolveSeedsForScope,
27
+ summarizeMentalModel,
28
+ } from "../../hindsight";
20
29
  import { resolveMemoryBackend } from "../../memory-backend";
21
30
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
31
  import { BorderedLoader } from "../../modes/components/bordered-loader";
@@ -573,7 +582,7 @@ export class CommandController {
573
582
  const backend = resolveMemoryBackend(this.ctx.settings);
574
583
 
575
584
  if (action === "view") {
576
- const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings);
585
+ const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
577
586
  if (!payload) {
578
587
  this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
579
588
  return;
@@ -590,7 +599,7 @@ export class CommandController {
590
599
 
591
600
  if (action === "reset" || action === "clear") {
592
601
  try {
593
- await backend.clear(agentDir, this.ctx.sessionManager.getCwd());
602
+ await backend.clear(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
594
603
  await this.ctx.session.refreshBaseSystemPrompt();
595
604
  this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
596
605
  } catch (error) {
@@ -601,7 +610,7 @@ export class CommandController {
601
610
 
602
611
  if (action === "enqueue" || action === "rebuild") {
603
612
  try {
604
- await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd());
613
+ await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
605
614
  this.ctx.showStatus("Memory consolidation enqueued.");
606
615
  } catch (error) {
607
616
  this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -609,7 +618,257 @@ export class CommandController {
609
618
  return;
610
619
  }
611
620
 
612
- this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
621
+ if (action === "mm") {
622
+ await this.#handleMentalModelsSubcommand(argumentText);
623
+ return;
624
+ }
625
+
626
+ this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
627
+ }
628
+
629
+ async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
630
+ // Parse: "mm <verb> [arg]"
631
+ const parts = argumentText.split(/\s+/).slice(1);
632
+ const verb = parts[0]?.toLowerCase() ?? "list";
633
+ const arg = parts[1];
634
+
635
+ const state = this.ctx.session.getHindsightSessionState();
636
+ const primary = state && !state.aliasOf ? state : undefined;
637
+ if (!primary) {
638
+ this.ctx.showError("Hindsight backend is not active for this session.");
639
+ return;
640
+ }
641
+ if (!primary.config.mentalModelsEnabled) {
642
+ this.ctx.showError("Mental models are disabled (hindsight.mentalModelsEnabled = false).");
643
+ return;
644
+ }
645
+
646
+ switch (verb) {
647
+ case "list":
648
+ await this.#mmList(primary);
649
+ return;
650
+ case "show":
651
+ if (!arg) return this.ctx.showError("Usage: /memory mm show <id>");
652
+ await this.#mmShow(primary, arg);
653
+ return;
654
+ case "refresh":
655
+ await this.#mmRefresh(primary, arg);
656
+ return;
657
+ case "history":
658
+ if (!arg) return this.ctx.showError("Usage: /memory mm history <id>");
659
+ await this.#mmHistory(primary, arg);
660
+ return;
661
+ case "seed":
662
+ await this.#mmSeed(primary);
663
+ return;
664
+ case "reload":
665
+ await this.#mmReload(primary);
666
+ return;
667
+ case "delete":
668
+ case "remove":
669
+ if (!arg) return this.ctx.showError("Usage: /memory mm delete <id>");
670
+ await this.#mmDelete(primary, arg);
671
+ return;
672
+ default:
673
+ this.ctx.showError("Usage: /memory mm <list|show|refresh|history|seed|reload|delete>");
674
+ }
675
+ }
676
+
677
+ async #mmList(state: HindsightSessionState): Promise<void> {
678
+ const client: HindsightApi = state.client;
679
+ try {
680
+ const response = await client.listMentalModels(state.bankId, { detail: "metadata" });
681
+ const items = response.items ?? [];
682
+ if (items.length === 0) {
683
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
684
+ return;
685
+ }
686
+ const lines = items
687
+ .slice()
688
+ .sort((a, b) => a.id.localeCompare(b.id))
689
+ .map(summarizeMentalModel);
690
+ showMarkdownPanel(this.ctx, `Mental Models — ${state.bankId}`, lines.join("\n"));
691
+ } catch (error) {
692
+ this.ctx.showError(`mm list failed: ${error instanceof Error ? error.message : String(error)}`);
693
+ }
694
+ }
695
+
696
+ async #mmShow(state: HindsightSessionState, id: string): Promise<void> {
697
+ try {
698
+ const model = await state.client.getMentalModel(state.bankId, id, { detail: "content" });
699
+ if (!model) {
700
+ this.ctx.showError(`Mental model not found: ${id}`);
701
+ return;
702
+ }
703
+ const tags = model.tags && model.tags.length > 0 ? `\n_tags: ${model.tags.join(", ")}_` : "";
704
+ const refreshed = model.last_refreshed_at ? `\n_last refreshed: ${model.last_refreshed_at}_` : "";
705
+ const sourceQuery = model.source_query ? `\n\n**Source query:** ${model.source_query}` : "";
706
+ const content = (model.content ?? "_(empty — background reflect may still be running)_").trim();
707
+ showMarkdownPanel(
708
+ this.ctx,
709
+ model.name,
710
+ `**id:** \`${model.id}\`${tags}${refreshed}${sourceQuery}\n\n${content}`,
711
+ );
712
+ } catch (error) {
713
+ this.ctx.showError(`mm show failed: ${error instanceof Error ? error.message : String(error)}`);
714
+ }
715
+ }
716
+
717
+ async #mmRefresh(state: HindsightSessionState, id: string | undefined): Promise<void> {
718
+ try {
719
+ if (id) {
720
+ // Single-model refresh is explicit operator intent: bypass the
721
+ // auto-refresh filter so curated/manual models can still be
722
+ // refreshed on demand.
723
+ await state.client.refreshMentalModel(state.bankId, id);
724
+ this.ctx.showStatus(`Refresh queued for mental model ${id}.`);
725
+ } else {
726
+ // Bulk refresh: only touch models that opted into automatic
727
+ // refresh via `trigger.refresh_after_consolidation`. Curated
728
+ // models are reviewed before publishing and must not be
729
+ // silently regenerated by a bank-wide refresh sweep. Reading
730
+ // `detail: "content"` here is required because the trigger
731
+ // field is excluded from `detail: "metadata"`.
732
+ const list = await state.client.listMentalModels(state.bankId, { detail: "content" });
733
+ const items = list.items ?? [];
734
+ if (items.length === 0) {
735
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
736
+ return;
737
+ }
738
+ const targets = items.filter(m => m.trigger?.refresh_after_consolidation === true);
739
+ const skipped = items.length - targets.length;
740
+ if (targets.length === 0) {
741
+ this.ctx.showStatus(
742
+ `No mental models opted into auto-refresh; ${skipped} curated model(s) left untouched. Pass an explicit id to refresh one of them.`,
743
+ );
744
+ return;
745
+ }
746
+ let queued = 0;
747
+ for (const item of targets) {
748
+ try {
749
+ await state.client.refreshMentalModel(state.bankId, item.id);
750
+ queued++;
751
+ } catch (error) {
752
+ this.ctx.showWarning(
753
+ `Refresh failed for ${item.id}: ${error instanceof Error ? error.message : String(error)}`,
754
+ );
755
+ }
756
+ }
757
+ const skippedSuffix = skipped > 0 ? `; skipped ${skipped} curated model(s)` : "";
758
+ this.ctx.showStatus(
759
+ `Refresh queued for ${queued}/${targets.length} auto-refresh model(s)${skippedSuffix}.`,
760
+ );
761
+ }
762
+ // Reload the cache after a brief grace so the new content (if the refresh
763
+ // completes synchronously on the server) flows into the system prompt.
764
+ await Bun.sleep(500);
765
+ await reloadMentalModelsForSession(state.session);
766
+ } catch (error) {
767
+ this.ctx.showError(`mm refresh failed: ${error instanceof Error ? error.message : String(error)}`);
768
+ }
769
+ }
770
+
771
+ async #mmHistory(state: HindsightSessionState, id: string): Promise<void> {
772
+ try {
773
+ const [model, history] = await Promise.all([
774
+ state.client.getMentalModel(state.bankId, id, { detail: "content" }),
775
+ state.client.getMentalModelHistory(state.bankId, id),
776
+ ]);
777
+ if (!model) {
778
+ this.ctx.showError(`Mental model not found: ${id}`);
779
+ return;
780
+ }
781
+ if (history.length === 0) {
782
+ this.ctx.showStatus(`No history recorded for ${id}.`);
783
+ return;
784
+ }
785
+ // History is most-recent first. Each entry stores the content BEFORE that
786
+ // change. To diff "what changed at entry N", compare entry N's
787
+ // previous_content (= state before that change) with entry N-1's
788
+ // previous_content (= state after that change, which was state before
789
+ // the next change). For the most recent change, compare against the
790
+ // model's CURRENT content.
791
+ const sections: string[] = [];
792
+ for (let i = 0; i < history.length; i++) {
793
+ const before = history[i].previous_content ?? "";
794
+ const after = i === 0 ? (model.content ?? "") : (history[i - 1].previous_content ?? "");
795
+ const diff = diffMentalModelContent(before, after);
796
+ sections.push(`### ${history[i].changed_at}\n\n\`\`\`diff\n${diff}\n\`\`\``);
797
+ }
798
+ showMarkdownPanel(this.ctx, `History — ${model.name}`, sections.join("\n\n"));
799
+ } catch (error) {
800
+ this.ctx.showError(`mm history failed: ${error instanceof Error ? error.message : String(error)}`);
801
+ }
802
+ }
803
+
804
+ async #mmSeed(state: HindsightSessionState): Promise<void> {
805
+ try {
806
+ const config = loadHindsightConfig(this.ctx.settings);
807
+ const seeds = resolveSeedsForScope(
808
+ {
809
+ bankId: state.bankId,
810
+ retainTags: state.retainTags,
811
+ recallTags: state.recallTags,
812
+ recallTagsMatch: state.recallTagsMatch,
813
+ },
814
+ config.scoping,
815
+ );
816
+ if (seeds.length === 0) {
817
+ this.ctx.showStatus(`No built-in seeds apply to scoping=${config.scoping}.`);
818
+ return;
819
+ }
820
+ const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
821
+ const existing = new Set((list.items ?? []).map(m => m.id));
822
+ let created = 0;
823
+ let skipped = 0;
824
+ for (const seed of seeds) {
825
+ if (existing.has(seed.id)) {
826
+ skipped++;
827
+ continue;
828
+ }
829
+ try {
830
+ await state.client.createMentalModel(state.bankId, seed.name, seed.sourceQuery, {
831
+ id: seed.id,
832
+ tags: seed.tags.length > 0 ? seed.tags : undefined,
833
+ maxTokens: seed.maxTokens,
834
+ trigger: seed.trigger,
835
+ });
836
+ created++;
837
+ } catch (error) {
838
+ this.ctx.showWarning(
839
+ `Seed failed for ${seed.id}: ${error instanceof Error ? error.message : String(error)}`,
840
+ );
841
+ }
842
+ }
843
+ this.ctx.showStatus(`Seeded ${created} new mental model(s); ${skipped} already present.`);
844
+ } catch (error) {
845
+ this.ctx.showError(`mm seed failed: ${error instanceof Error ? error.message : String(error)}`);
846
+ }
847
+ }
848
+
849
+ async #mmReload(state: HindsightSessionState): Promise<void> {
850
+ const ok = await reloadMentalModelsForSession(state.session);
851
+ if (ok) {
852
+ this.ctx.showStatus("Mental-model cache reloaded.");
853
+ } else {
854
+ this.ctx.showError("Reload failed (Hindsight backend not active or mental models disabled).");
855
+ }
856
+ }
857
+
858
+ async #mmDelete(state: HindsightSessionState, id: string): Promise<void> {
859
+ try {
860
+ const removed = await state.client.deleteMentalModel(state.bankId, id);
861
+ if (!removed) {
862
+ this.ctx.showError(`Mental model not found: ${id}`);
863
+ return;
864
+ }
865
+ // Drop the cached snippet so the closing tag does not silently keep
866
+ // stale content in the system prompt until the next agent_end TTL.
867
+ await reloadMentalModelsForSession(state.session);
868
+ this.ctx.showStatus(`Deleted mental model ${id} from bank ${state.bankId}.`);
869
+ } catch (error) {
870
+ this.ctx.showError(`mm delete failed: ${error instanceof Error ? error.message : String(error)}`);
871
+ }
613
872
  }
614
873
 
615
874
  async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
@@ -79,4 +79,5 @@ This format is purely textual. The tool has NO awareness of language, indentatio
79
79
  - Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
80
80
  - Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
81
81
  - To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
82
+ - `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
82
83
  </critical>
package/src/sdk.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  } from "./extensibility/extensions";
62
62
  import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills";
63
63
  import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
64
+ import type { HindsightSessionState } from "./hindsight/state";
64
65
  import {
65
66
  AgentProtocolHandler,
66
67
  ArtifactProtocolHandler,
@@ -216,6 +217,8 @@ export interface CreateAgentSessionOptions {
216
217
  requireYieldTool?: boolean;
217
218
  /** Task recursion depth (for subagent sessions). Default: 0 */
218
219
  taskDepth?: number;
220
+ /** Parent Hindsight state to alias for subagent memory tools. */
221
+ parentHindsightSessionState?: HindsightSessionState;
219
222
  /** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
220
223
  agentId?: string;
221
224
  /** Display name for the agent in IRC. Default: "main" or "sub". */
@@ -968,6 +971,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
968
971
  trackEvalExecution: (execution, abortController) =>
969
972
  session ? session.trackEvalExecution(execution, abortController) : execution,
970
973
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
974
+ getHindsightSessionState: () => session?.getHindsightSessionState(),
971
975
  getAgentId: () => resolvedAgentId,
972
976
  getToolByName: name => session?.getToolByName(name),
973
977
  agentRegistry,
@@ -1335,7 +1339,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1335
1339
  const promptTools = buildSystemPromptToolMetadata(tools, {
1336
1340
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1337
1341
  });
1338
- const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(agentDir, settings);
1342
+ const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
1343
+ agentDir,
1344
+ settings,
1345
+ session,
1346
+ );
1339
1347
 
1340
1348
  // Build combined append prompt: memory instructions + MCP server instructions
1341
1349
  const serverInstructions = mcpManager?.getServerInstructions();
@@ -1755,6 +1763,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1755
1763
  modelRegistry,
1756
1764
  agentDir,
1757
1765
  taskDepth,
1766
+ parentHindsightSessionState: options.parentHindsightSessionState,
1758
1767
  }),
1759
1768
  ),
1760
1769
  );
@@ -102,6 +102,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
102
102
  import type { HookCommandContext } from "../extensibility/hooks/types";
103
103
  import type { Skill, SkillWarning } from "../extensibility/skills";
104
104
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
105
+ import type { HindsightSessionState } from "../hindsight/state";
105
106
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
106
107
  import {
107
108
  buildDiscoverableMCPSearchIndex,
@@ -563,6 +564,7 @@ export class AgentSession {
563
564
  #lastSuccessfulYieldToolCallId: string | undefined = undefined;
564
565
  #promptGeneration = 0;
565
566
  #providerSessionState = new Map<string, ProviderSessionState>();
567
+ #hindsightSessionState: HindsightSessionState | undefined = undefined;
566
568
 
567
569
  #startPowerAssertion(): void {
568
570
  if (process.platform !== "darwin") {
@@ -702,6 +704,16 @@ export class AgentSession {
702
704
  return this.#providerSessionState;
703
705
  }
704
706
 
707
+ getHindsightSessionState(): HindsightSessionState | undefined {
708
+ return this.#hindsightSessionState;
709
+ }
710
+
711
+ setHindsightSessionState(state: HindsightSessionState | undefined): HindsightSessionState | undefined {
712
+ const previous = this.#hindsightSessionState;
713
+ this.#hindsightSessionState = state;
714
+ return previous;
715
+ }
716
+
705
717
  /** TTSR manager for time-traveling stream rules */
706
718
  get ttsrManager(): TtsrManager | undefined {
707
719
  return this.#ttsrManager;
@@ -1964,6 +1976,22 @@ export class AgentSession {
1964
1976
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
1965
1977
  }
1966
1978
 
1979
+ /** Keep Hindsight metadata aligned when the underlying agent session id changes. */
1980
+ #rekeyHindsightMemoryForCurrentSessionId(): void {
1981
+ if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
1982
+ const sid = this.agent.sessionId;
1983
+ if (!sid) return;
1984
+ this.getHindsightSessionState()?.setSessionId(sid);
1985
+ }
1986
+
1987
+ /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
1988
+ #resetHindsightConversationTrackingIfHindsight(): void {
1989
+ if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
1990
+ const state = this.getHindsightSessionState();
1991
+ if (!state || state.aliasOf) return;
1992
+ state.resetConversationTracking();
1993
+ }
1994
+
1967
1995
  /**
1968
1996
  * Remove all listeners, flush pending writes, and disconnect from agent.
1969
1997
  * Call this when completely done with the session.
@@ -1994,6 +2022,9 @@ export class AgentSession {
1994
2022
  this.#stopPowerAssertion();
1995
2023
  await this.sessionManager.close();
1996
2024
  this.#closeAllProviderSessions("dispose");
2025
+ const hindsightState = this.setHindsightSessionState(undefined);
2026
+ await hindsightState?.flushRetainQueue();
2027
+ hindsightState?.dispose();
1997
2028
  this.#disconnectFromAgent();
1998
2029
  this.#eventListeners = [];
1999
2030
  }
@@ -3626,6 +3657,8 @@ export class AgentSession {
3626
3657
  await this.sessionManager.newSession(options);
3627
3658
  this.setTodoPhases([]);
3628
3659
  this.agent.sessionId = this.sessionManager.getSessionId();
3660
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3661
+ this.#resetHindsightConversationTrackingIfHindsight();
3629
3662
  this.#steeringMessages = [];
3630
3663
  this.#followUpMessages = [];
3631
3664
  this.#pendingNextTurnMessages = [];
@@ -3719,6 +3752,7 @@ export class AgentSession {
3719
3752
 
3720
3753
  // Update agent session ID
3721
3754
  this.agent.sessionId = this.sessionManager.getSessionId();
3755
+ this.#rekeyHindsightMemoryForCurrentSessionId();
3722
3756
 
3723
3757
  // Emit session_switch event with reason "fork" to hooks
3724
3758
  if (this.#extensionRunner) {
@@ -4261,7 +4295,7 @@ export class AgentSession {
4261
4295
  if (!backend.preCompactionContext) return undefined;
4262
4296
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
4263
4297
  try {
4264
- return await backend.preCompactionContext(messages, this.settings);
4298
+ return await backend.preCompactionContext(messages, this.settings, this);
4265
4299
  } catch (err) {
4266
4300
  logger.debug("Memory backend preCompactionContext failed", {
4267
4301
  backend: backend.id,
@@ -4425,6 +4459,8 @@ export class AgentSession {
4425
4459
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
4426
4460
  this.agent.reset();
4427
4461
  this.agent.sessionId = this.sessionManager.getSessionId();
4462
+ this.#rekeyHindsightMemoryForCurrentSessionId();
4463
+ this.#resetHindsightConversationTrackingIfHindsight();
4428
4464
  this.#steeringMessages = [];
4429
4465
  this.#followUpMessages = [];
4430
4466
  this.#pendingNextTurnMessages = [];
@@ -6571,6 +6607,7 @@ export class AgentSession {
6571
6607
  try {
6572
6608
  await this.sessionManager.setSessionFile(sessionPath);
6573
6609
  this.agent.sessionId = this.sessionManager.getSessionId();
6610
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6574
6611
 
6575
6612
  const sessionContext = this.buildDisplaySessionContext();
6576
6613
  const didReloadConversationChange =
@@ -6640,11 +6677,15 @@ export class AgentSession {
6640
6677
  ? undefined
6641
6678
  : configuredServiceTier;
6642
6679
 
6680
+ if (switchingToDifferentSession) {
6681
+ this.#resetHindsightConversationTrackingIfHindsight();
6682
+ }
6643
6683
  this.#reconnectToAgent();
6644
6684
  return true;
6645
6685
  } catch (error) {
6646
6686
  this.sessionManager.restoreState(previousSessionState);
6647
6687
  this.agent.sessionId = previousSessionState.sessionId;
6688
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6648
6689
  let restoreMcpError: unknown;
6649
6690
  try {
6650
6691
  await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
@@ -6736,6 +6777,8 @@ export class AgentSession {
6736
6777
  }
6737
6778
  this.#syncTodoPhasesFromBranch();
6738
6779
  this.agent.sessionId = this.sessionManager.getSessionId();
6780
+ this.#rekeyHindsightMemoryForCurrentSessionId();
6781
+ this.#resetHindsightConversationTrackingIfHindsight();
6739
6782
 
6740
6783
  // Reload messages from entries (works for both file and in-memory mode)
6741
6784
  const sessionContext = this.buildDisplaySessionContext();
@@ -598,6 +598,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
598
598
  { name: "reset", description: "Alias for clear" },
599
599
  { name: "enqueue", description: "Enqueue memory consolidation maintenance" },
600
600
  { name: "rebuild", description: "Alias for enqueue" },
601
+ { name: "mm list", description: "List mental models on the active bank" },
602
+ { name: "mm show", description: "Show one mental model (id required)" },
603
+ {
604
+ name: "mm refresh",
605
+ description: "Refresh auto-refresh models bank-wide, or one model by id",
606
+ },
607
+ { name: "mm history", description: "Diff the change history of a mental model" },
608
+ { name: "mm seed", description: "Create any built-in mental models that are missing" },
609
+ { name: "mm delete", description: "Delete a mental model from the bank (id required)" },
610
+ { name: "mm reload", description: "Re-pull the cached <mental_models> block" },
601
611
  ],
602
612
  allowArgs: true,
603
613
  handle: async (command, runtime) => {
@@ -17,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
17
17
  import type { CustomTool } from "../extensibility/custom-tools/types";
18
18
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
19
19
  import type { Skill } from "../extensibility/skills";
20
+ import type { HindsightSessionState } from "../hindsight/state";
20
21
  import type { LocalProtocolOptions } from "../internal-urls";
21
22
  import { callTool } from "../mcp/client";
22
23
  import type { MCPManager } from "../mcp/manager";
@@ -163,6 +164,7 @@ export interface ExecutorOptions {
163
164
  settings?: Settings;
164
165
  /** Override local:// protocol options so subagent shares parent's local:// root */
165
166
  localProtocolOptions?: LocalProtocolOptions;
167
+ parentHindsightSessionState?: HindsightSessionState;
166
168
  }
167
169
 
168
170
  function parseStringifiedJson(value: unknown): unknown {
@@ -979,6 +981,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
979
981
  hasUI: false,
980
982
  spawns: spawnsEnv,
981
983
  taskDepth: childDepth,
984
+ parentHindsightSessionState: options.parentHindsightSessionState,
982
985
  parentTaskPrefix: id,
983
986
  agentId: id,
984
987
  agentDisplayName: agent.name,
package/src/task/index.ts CHANGED
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
864
864
  skills: availableSkills,
865
865
  promptTemplates,
866
866
  localProtocolOptions,
867
+ parentHindsightSessionState: this.session.getHindsightSessionState?.(),
867
868
  });
868
869
  }
869
870
 
@@ -918,6 +919,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
918
919
  skills: availableSkills,
919
920
  promptTemplates,
920
921
  localProtocolOptions,
922
+ parentHindsightSessionState: this.session.getHindsightSessionState?.(),
921
923
  });
922
924
  if (mergeMode === "branch" && result.exitCode === 0) {
923
925
  try {
@@ -1,7 +1,6 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
3
3
  import { type Static, Type } from "@sinclair/typebox";
4
- import { getHindsightSessionState } from "../hindsight/backend";
5
4
  import { formatCurrentTime, formatMemories } from "../hindsight/content";
6
5
  import recallDescription from "../prompts/tools/recall.md" with { type: "text" };
7
6
  import type { ToolSession } from ".";
@@ -30,8 +29,7 @@ export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSche
30
29
 
31
30
  async execute(_id: string, params: HindsightRecallParams, signal?: AbortSignal): Promise<AgentToolResult> {
32
31
  return untilAborted(signal, async () => {
33
- const sessionId = this.session.getSessionId?.();
34
- const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
32
+ const state = this.session.getHindsightSessionState?.();
35
33
  if (!state) {
36
34
  throw new Error("Hindsight backend is not initialised for this session.");
37
35
  }
@@ -1,7 +1,6 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
3
3
  import { type Static, Type } from "@sinclair/typebox";
4
- import { getHindsightSessionState } from "../hindsight/backend";
5
4
  import { ensureBankMission } from "../hindsight/bank";
6
5
  import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
7
6
  import type { ToolSession } from ".";
@@ -29,8 +28,7 @@ export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSc
29
28
 
30
29
  async execute(_id: string, params: HindsightReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
31
30
  return untilAborted(signal, async () => {
32
- const sessionId = this.session.getSessionId?.();
33
- const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
31
+ const state = this.session.getHindsightSessionState?.();
34
32
  if (!state) {
35
33
  throw new Error("Hindsight backend is not initialised for this session.");
36
34
  }
@@ -1,7 +1,5 @@
1
1
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import { type Static, Type } from "@sinclair/typebox";
3
- import { getHindsightSessionState } from "../hindsight/backend";
4
- import { enqueueRetain } from "../hindsight/retain-queue";
5
3
  import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
6
4
  import type { ToolSession } from ".";
7
5
 
@@ -39,18 +37,17 @@ export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSche
39
37
  }
40
38
 
41
39
  async execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult> {
42
- const sessionId = this.session.getSessionId?.();
43
- const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
44
- if (!state || !sessionId) {
40
+ const state = this.session.getHindsightSessionState?.();
41
+ if (!state) {
45
42
  throw new Error("Hindsight backend is not initialised for this session.");
46
43
  }
47
44
 
48
- // Push every item onto the global queue and return immediately. The
49
- // queue flushes either when it reaches its batch threshold or when its
50
- // debounce timer fires. If the eventual batch fails, the queue
45
+ // Push every item onto the session-owned queue and return immediately.
46
+ // The queue flushes either when it reaches its batch threshold or when
47
+ // its debounce timer fires. If the eventual batch fails, the queue
51
48
  // surfaces a UI-only warning notice — the LLM is not informed.
52
49
  for (const item of params.items) {
53
- enqueueRetain(sessionId, item.content, item.context);
50
+ state.enqueueRetain(item.content, item.context);
54
51
  }
55
52
 
56
53
  const count = params.items.length;
@@ -7,6 +7,7 @@ import type { Settings } from "../config/settings";
7
7
  import { EditTool } from "../edit";
8
8
  import { checkPythonKernelAvailability } from "../eval/py/kernel";
9
9
  import type { Skill } from "../extensibility/skills";
10
+ import type { HindsightSessionState } from "../hindsight/state";
10
11
  import type { InternalUrlRouter } from "../internal-urls";
11
12
  import { LspTool } from "../lsp";
12
13
  import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
@@ -141,6 +142,8 @@ export interface ToolSession {
141
142
  trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
142
143
  /** Get session ID */
143
144
  getSessionId?: () => string | null;
145
+ /** Get Hindsight runtime state for this agent session. */
146
+ getHindsightSessionState?: () => HindsightSessionState | undefined;
144
147
  /** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
145
148
  getAgentId?: () => string | null;
146
149
  /** Look up a registered tool by name (used by the eval js backend's tool bridge). */