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

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.
@@ -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). */
@@ -1,166 +0,0 @@
1
- /**
2
- * Global, debounced batch queue for tool-initiated `retain` calls.
3
- *
4
- * The `retain` tool used to block on a single-item HTTP round trip per
5
- * invocation. Now it pushes onto a per-session queue and returns immediately;
6
- * a flush fires when:
7
- * 1. the queue reaches `FLUSH_BATCH_SIZE`, or
8
- * 2. `FLUSH_INTERVAL_MS` elapses since the queue first became non-empty.
9
- *
10
- * On batch failure we surface a UI-only notice via `session.emitNotice` —
11
- * a single yellow "Hindsight: memory retention failed …" line in the TUI.
12
- * The LLM is NOT told; the agent already received "Memory queued" and has
13
- * moved on. This is purely so the user knows their facts didn't persist.
14
- *
15
- * Auto-retain (`retainSession` in backend.ts) is intentionally NOT routed
16
- * through this queue — it submits a full transcript as one large item and
17
- * already runs `async: true` server-side.
18
- */
19
-
20
- import { logger } from "@oh-my-pi/pi-utils";
21
- import { getHindsightSessionState, type HindsightSessionState } from "./backend";
22
- import { ensureBankMission } from "./bank";
23
- import type { MemoryItemInput } from "./client";
24
-
25
- const FLUSH_BATCH_SIZE = 16;
26
- const FLUSH_INTERVAL_MS = 5_000;
27
-
28
- interface PendingItem {
29
- content: string;
30
- context?: string;
31
- }
32
-
33
- interface SessionQueue {
34
- items: PendingItem[];
35
- timer?: NodeJS.Timeout;
36
- /** Currently in-flight flush; subsequent flushes await it before running. */
37
- flushing?: Promise<void>;
38
- }
39
-
40
- const QUEUES = new Map<string, SessionQueue>();
41
-
42
- /** Push a memory item onto the session's retain queue. Returns immediately. */
43
- export function enqueueRetain(sessionId: string, content: string, context?: string): void {
44
- const queue = QUEUES.get(sessionId) ?? createQueue(sessionId);
45
- queue.items.push({ content, context });
46
-
47
- if (queue.items.length >= FLUSH_BATCH_SIZE) {
48
- void flushSessionQueue(sessionId);
49
- return;
50
- }
51
- if (!queue.timer) {
52
- queue.timer = setTimeout(() => {
53
- void flushSessionQueue(sessionId);
54
- }, FLUSH_INTERVAL_MS);
55
- // Don't pin the event loop alive just for a pending retain flush.
56
- queue.timer.unref?.();
57
- }
58
- }
59
-
60
- /** Flush a single session's queue. Safe to call when empty or already in flight. */
61
- export async function flushSessionQueue(sessionId: string): Promise<void> {
62
- const queue = QUEUES.get(sessionId);
63
- if (!queue) return;
64
-
65
- if (queue.timer) {
66
- clearTimeout(queue.timer);
67
- queue.timer = undefined;
68
- }
69
-
70
- if (queue.flushing) {
71
- // Coalesce: wait for the in-flight flush, then drain anything that
72
- // landed after it started so we don't strand items.
73
- await queue.flushing;
74
- if (queue.items.length > 0) {
75
- await flushSessionQueue(sessionId);
76
- }
77
- return;
78
- }
79
-
80
- if (queue.items.length === 0) {
81
- QUEUES.delete(sessionId);
82
- return;
83
- }
84
-
85
- const items = queue.items.splice(0);
86
- const flushPromise = doFlush(sessionId, items);
87
- queue.flushing = flushPromise;
88
- try {
89
- await flushPromise;
90
- } finally {
91
- queue.flushing = undefined;
92
- if (queue.items.length === 0 && !queue.timer) {
93
- QUEUES.delete(sessionId);
94
- }
95
- }
96
- }
97
-
98
- /** Flush every pending session queue. Called from `clear`/`enqueue` backend hooks. */
99
- export async function flushAllRetainQueues(): Promise<void> {
100
- const ids = [...QUEUES.keys()];
101
- await Promise.all(ids.map(id => flushSessionQueue(id)));
102
- }
103
-
104
- /** Test helper: clear timers and pending items without triggering flushes. */
105
- export function clearRetainQueueForTest(): void {
106
- for (const queue of QUEUES.values()) {
107
- if (queue.timer) clearTimeout(queue.timer);
108
- }
109
- QUEUES.clear();
110
- }
111
-
112
- /** Test helper: peek at queued count for a session. */
113
- export function getRetainQueueDepthForTest(sessionId: string): number {
114
- return QUEUES.get(sessionId)?.items.length ?? 0;
115
- }
116
-
117
- async function doFlush(sessionId: string, items: PendingItem[]): Promise<void> {
118
- const state = getHindsightSessionState(sessionId);
119
- if (!state) {
120
- // Session went away before we could flush. We can't notify anyone, so
121
- // log and drop — these are best-effort facts, not transactional writes.
122
- logger.warn("Hindsight retain queue: session vanished, dropping batch", {
123
- sessionId,
124
- items: items.length,
125
- });
126
- return;
127
- }
128
-
129
- try {
130
- await ensureBankMission(state.client, state.bankId, state.config, state.missionsSet);
131
- const batch: MemoryItemInput[] = items.map(item => ({
132
- content: item.content,
133
- context: item.context ?? state.config.retainContext,
134
- metadata: { session_id: sessionId },
135
- tags: state.retainTags,
136
- }));
137
- await state.client.retainBatch(state.bankId, batch, { async: true });
138
- if (state.config.debug) {
139
- logger.debug("Hindsight retain queue: batch flushed", {
140
- sessionId,
141
- bankId: state.bankId,
142
- items: items.length,
143
- });
144
- }
145
- } catch (err) {
146
- const errorText = err instanceof Error ? err.message : String(err);
147
- logger.warn("Hindsight retain queue: batch flush failed", {
148
- sessionId,
149
- bankId: state.bankId,
150
- items: items.length,
151
- error: errorText,
152
- });
153
- notifyRetainFailure(state, items.length, errorText);
154
- }
155
- }
156
-
157
- function notifyRetainFailure(state: HindsightSessionState, count: number, errorText: string): void {
158
- const noun = count === 1 ? "memory" : "memories";
159
- state.session.emitNotice("warning", `Memory retention failed for ${count} ${noun}: ${errorText}`, "Hindsight");
160
- }
161
-
162
- function createQueue(sessionId: string): SessionQueue {
163
- const queue: SessionQueue = { items: [] };
164
- QUEUES.set(sessionId, queue);
165
- return queue;
166
- }