@oh-my-pi/pi-coding-agent 15.10.11 → 15.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli.js +5349 -5328
  3. package/dist/types/cli/args.d.ts +1 -0
  4. package/dist/types/cli-commands.d.ts +12 -0
  5. package/dist/types/commands/launch.d.ts +4 -0
  6. package/dist/types/config/api-key-resolver.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +1 -0
  8. package/dist/types/config/model-resolver.d.ts +18 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  12. package/dist/types/eval/py/executor.d.ts +5 -0
  13. package/dist/types/eval/py/kernel.d.ts +6 -1
  14. package/dist/types/eval/py/runtime.d.ts +9 -0
  15. package/dist/types/exec/bash-executor.d.ts +2 -0
  16. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  17. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  18. package/dist/types/memory-backend/index.d.ts +1 -0
  19. package/dist/types/memory-backend/runtime.d.ts +4 -0
  20. package/dist/types/memory-backend/types.d.ts +66 -1
  21. package/dist/types/modes/index.d.ts +3 -3
  22. package/dist/types/modes/interactive-mode.d.ts +7 -2
  23. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  24. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  25. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  26. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  27. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  28. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  29. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  30. package/dist/types/modes/types.d.ts +2 -0
  31. package/dist/types/secrets/index.d.ts +1 -1
  32. package/dist/types/secrets/obfuscator.d.ts +8 -2
  33. package/dist/types/session/agent-session.d.ts +14 -2
  34. package/dist/types/session/streaming-output.d.ts +23 -0
  35. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  36. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  37. package/dist/types/slash-commands/types.d.ts +1 -1
  38. package/dist/types/system-prompt.d.ts +2 -0
  39. package/dist/types/task/executor.d.ts +1 -0
  40. package/dist/types/task/index.d.ts +2 -2
  41. package/dist/types/task/types.d.ts +8 -0
  42. package/dist/types/thinking.d.ts +4 -0
  43. package/dist/types/tiny/title-client.d.ts +11 -0
  44. package/dist/types/tiny/title-protocol.d.ts +1 -0
  45. package/dist/types/tools/index.d.ts +6 -0
  46. package/dist/types/utils/git.d.ts +15 -2
  47. package/dist/types/utils/title-generator.d.ts +3 -2
  48. package/package.json +10 -10
  49. package/src/auto-thinking/classifier.ts +1 -0
  50. package/src/cli/args.ts +3 -0
  51. package/src/cli-commands.ts +29 -0
  52. package/src/cli.ts +8 -9
  53. package/src/commands/launch.ts +4 -0
  54. package/src/commit/model-selection.ts +3 -2
  55. package/src/config/api-key-resolver.ts +8 -6
  56. package/src/config/model-registry.ts +97 -30
  57. package/src/config/model-resolver.ts +60 -0
  58. package/src/config/settings-schema.ts +43 -15
  59. package/src/config/settings.ts +61 -3
  60. package/src/edit/hashline/execute.ts +39 -2
  61. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  62. package/src/eval/completion-bridge.ts +1 -0
  63. package/src/eval/py/executor.ts +29 -7
  64. package/src/eval/py/index.ts +6 -1
  65. package/src/eval/py/kernel.ts +31 -11
  66. package/src/eval/py/runtime.ts +37 -0
  67. package/src/exec/bash-executor.ts +82 -3
  68. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  69. package/src/extensibility/extensions/runner.ts +6 -1
  70. package/src/extensibility/extensions/types.ts +3 -0
  71. package/src/hindsight/bank.ts +17 -2
  72. package/src/internal-urls/docs-index.generated.ts +3 -3
  73. package/src/main.ts +18 -6
  74. package/src/memories/index.ts +2 -0
  75. package/src/memory-backend/index.ts +1 -0
  76. package/src/memory-backend/local-backend.ts +9 -0
  77. package/src/memory-backend/off-backend.ts +9 -0
  78. package/src/memory-backend/runtime.ts +66 -0
  79. package/src/memory-backend/types.ts +81 -1
  80. package/src/mnemopi/backend.ts +151 -4
  81. package/src/modes/acp/acp-agent.ts +119 -11
  82. package/src/modes/components/assistant-message.ts +19 -21
  83. package/src/modes/components/footer.ts +3 -1
  84. package/src/modes/components/status-line/component.ts +118 -34
  85. package/src/modes/controllers/command-controller.ts +1 -1
  86. package/src/modes/controllers/input-controller.ts +1 -0
  87. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  88. package/src/modes/index.ts +3 -21
  89. package/src/modes/interactive-mode.ts +39 -9
  90. package/src/modes/oauth-manual-input.ts +30 -3
  91. package/src/modes/rpc/rpc-client.ts +154 -3
  92. package/src/modes/rpc/rpc-mode.ts +97 -12
  93. package/src/modes/rpc/rpc-subagents.ts +265 -0
  94. package/src/modes/rpc/rpc-types.ts +81 -1
  95. package/src/modes/setup-wizard/index.ts +12 -2
  96. package/src/modes/setup-wizard/lazy.ts +16 -0
  97. package/src/modes/types.ts +2 -0
  98. package/src/sdk.ts +8 -1
  99. package/src/secrets/index.ts +8 -1
  100. package/src/secrets/obfuscator.ts +39 -18
  101. package/src/session/agent-session.ts +179 -54
  102. package/src/session/streaming-output.ts +166 -10
  103. package/src/slash-commands/acp-builtins.ts +24 -0
  104. package/src/slash-commands/builtin-registry.ts +20 -0
  105. package/src/slash-commands/types.ts +1 -1
  106. package/src/system-prompt.ts +14 -0
  107. package/src/task/executor.ts +13 -12
  108. package/src/task/index.ts +9 -8
  109. package/src/task/render.ts +18 -3
  110. package/src/task/types.ts +9 -0
  111. package/src/thinking.ts +7 -0
  112. package/src/tiny/title-client.ts +34 -5
  113. package/src/tiny/title-protocol.ts +1 -1
  114. package/src/tiny/worker.ts +6 -4
  115. package/src/tools/bash.ts +46 -5
  116. package/src/tools/image-gen.ts +11 -4
  117. package/src/tools/index.ts +13 -1
  118. package/src/tools/inspect-image.ts +1 -0
  119. package/src/utils/commit-message-generator.ts +1 -0
  120. package/src/utils/git.ts +267 -13
  121. package/src/utils/title-generator.ts +24 -5
package/src/main.ts CHANGED
@@ -67,7 +67,7 @@ import {
67
67
  import type { AgentSession } from "./session/agent-session";
68
68
  import type { AuthStorage } from "./session/auth-storage";
69
69
  import { resolveResumableSession, type SessionInfo, SessionManager } from "./session/session-manager";
70
- import { resolvePromptInput } from "./system-prompt";
70
+ import { discoverTitleSystemPromptFile, resolvePromptInput } from "./system-prompt";
71
71
  import { initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
72
72
  import { AUTO_THINKING } from "./thinking";
73
73
  import { discoverStartupLspServers, type LspStartupServerInfo } from "./tools";
@@ -85,6 +85,7 @@ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promis
85
85
  type RunRpcMode = (
86
86
  session: AgentSession,
87
87
  setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
88
+ eventBus?: EventBus,
88
89
  ) => Promise<never>;
89
90
 
90
91
  function maybeShowStartupSplash(options: {
@@ -370,6 +371,7 @@ async function runInteractiveMode(
370
371
  eventBus?: EventBus,
371
372
  initialMessage?: string,
372
373
  initialImages?: ImageContent[],
374
+ titleSystemPrompt?: string,
373
375
  ): Promise<void> {
374
376
  const mode = new InteractiveMode(
375
377
  session,
@@ -379,6 +381,7 @@ async function runInteractiveMode(
379
381
  lspServers,
380
382
  mcpManager,
381
383
  eventBus,
384
+ titleSystemPrompt,
382
385
  );
383
386
 
384
387
  // Cold-launch gate: the full setup wizard (every scene + the overlay and
@@ -724,7 +727,7 @@ async function buildSessionOptions(
724
727
  sessionManager: SessionManager | undefined,
725
728
  modelRegistry: ModelRegistry,
726
729
  activeSettings: Settings,
727
- ): Promise<{ options: CreateAgentSessionOptions }> {
730
+ ): Promise<{ options: CreateAgentSessionOptions; titleSystemPrompt?: string }> {
728
731
  const options: CreateAgentSessionOptions = {
729
732
  cwd: parsed.cwd ?? getProjectDir(),
730
733
  autoApprove: parsed.autoApprove ?? false,
@@ -735,6 +738,8 @@ async function buildSessionOptions(
735
738
  const resolvedSystemPrompt = await resolvePromptInput(systemPromptSource, "system prompt");
736
739
  const appendPromptSource = parsed.appendSystemPrompt ?? discoverAppendSystemPromptFile();
737
740
  const resolvedAppendPrompt = await resolvePromptInput(appendPromptSource, "append system prompt");
741
+ const titleSystemPromptSource = discoverTitleSystemPromptFile();
742
+ const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
738
743
 
739
744
  if (sessionManager) {
740
745
  options.sessionManager = sessionManager;
@@ -880,7 +885,7 @@ async function buildSessionOptions(
880
885
  options.additionalExtensionPaths = [];
881
886
  }
882
887
 
883
- return { options };
888
+ return { options, titleSystemPrompt };
884
889
  }
885
890
 
886
891
  interface RunRootCommandDependencies {
@@ -920,6 +925,7 @@ export async function runRootCommand(
920
925
  if (parsedArgs.listModels !== undefined) {
921
926
  const settingsInstance = await logger.time("settings:init:list-models", Settings.init, {
922
927
  cwd: getProjectDir(),
928
+ configFiles: parsedArgs.config,
923
929
  });
924
930
  await modelRegistry.refresh("online");
925
931
  const cliExtensionPaths = parsedArgs.noExtensions
@@ -983,11 +989,16 @@ export async function runRootCommand(
983
989
  }
984
990
 
985
991
  let cwd = getProjectDir();
986
- const settingsInstance = deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd }));
992
+ const settingsInstance =
993
+ deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd, configFiles: parsedArgs.config }));
987
994
  if (parsedArgs.approvalMode) {
988
995
  // Runtime override (not persisted): every settings.get("tools.approvalMode") downstream
989
996
  // sees this value. The wrapper still honours --auto-approve / --yolo on top of it.
990
997
  settingsInstance.override("tools.approvalMode", parsedArgs.approvalMode);
998
+ } else if (parsedArgs.autoApprove) {
999
+ // --auto-approve / --yolo without an explicit --approval-mode: reflect in settings so
1000
+ // setup-time checks (e.g. #wrapToolForAcpPermission) also see the yolo intent.
1001
+ settingsInstance.override("tools.approvalMode", "yolo");
991
1002
  }
992
1003
  if (parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui") {
993
1004
  applyRpcDefaultSettingOverrides(settingsInstance);
@@ -1133,7 +1144,7 @@ export async function runRootCommand(
1133
1144
  clearPluginRootsCache: clearPluginRootsAndCaches,
1134
1145
  });
1135
1146
 
1136
- const { options: sessionOptions } = await logger.time(
1147
+ const { options: sessionOptions, titleSystemPrompt } = await logger.time(
1137
1148
  "buildSessionOptions",
1138
1149
  buildSessionOptions,
1139
1150
  parsedArgs,
@@ -1298,7 +1309,7 @@ export async function runRootCommand(
1298
1309
  // Branch-only protocol runner: keep RPC host code out of normal interactive startup.
1299
1310
  const runRpcMode: RunRpcMode = (await import("./modes/rpc/rpc-mode")).runRpcMode;
1300
1311
  stopStartupWatchdog();
1301
- await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
1312
+ await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus);
1302
1313
  } else if (isInteractive) {
1303
1314
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1304
1315
  const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
@@ -1338,6 +1349,7 @@ export async function runRootCommand(
1338
1349
  eventBus,
1339
1350
  initialMessage,
1340
1351
  initialImages,
1352
+ titleSystemPrompt,
1341
1353
  );
1342
1354
  } else {
1343
1355
  // Branch-only single-shot runner: keep print-mode code out of normal interactive startup.
@@ -276,6 +276,7 @@ async function runPhase1(options: {
276
276
  apiKey: modelRegistry.resolver(phase1Model.provider, {
277
277
  sessionId: session.sessionId,
278
278
  baseUrl: phase1Model.baseUrl,
279
+ modelId: phase1Model.id,
279
280
  }),
280
281
  modelMaxTokens: computeModelTokenBudget(phase1Model, config),
281
282
  config,
@@ -436,6 +437,7 @@ async function runPhase2(options: {
436
437
  apiKey: modelRegistry.resolver(phase2Model.provider, {
437
438
  sessionId: session.sessionId,
438
439
  baseUrl: phase2Model.baseUrl,
440
+ modelId: phase2Model.id,
439
441
  }),
440
442
  metadata: session.agent?.metadataForProvider(phase2Model.provider),
441
443
  });
@@ -14,4 +14,5 @@ export type {
14
14
  export * from "./local-backend";
15
15
  export * from "./off-backend";
16
16
  export * from "./resolve";
17
+ export * from "./runtime";
17
18
  export * from "./types";
@@ -27,4 +27,13 @@ export const localBackend: MemoryBackend = {
27
27
  async enqueue(agentDir, cwd) {
28
28
  enqueueMemoryConsolidation(agentDir, cwd);
29
29
  },
30
+ async status() {
31
+ return {
32
+ backend: "local" as const,
33
+ active: true,
34
+ writable: false,
35
+ searchable: false,
36
+ message: "Local rollout-summary memory is active; structured search/save is not available.",
37
+ };
38
+ },
30
39
  };
@@ -13,4 +13,13 @@ export const offBackend: MemoryBackend = {
13
13
  },
14
14
  async clear() {},
15
15
  async enqueue() {},
16
+ async status() {
17
+ return {
18
+ backend: "off" as const,
19
+ active: false,
20
+ writable: false,
21
+ searchable: false,
22
+ message: "Memory backend is off.",
23
+ };
24
+ },
16
25
  };
@@ -0,0 +1,66 @@
1
+ import type { AgentSession } from "../session/agent-session";
2
+ import { resolveMemoryBackend } from "./resolve";
3
+ import type {
4
+ MemoryBackendId,
5
+ MemoryBackendOperationContext,
6
+ MemoryBackendSaveInput,
7
+ MemoryBackendSearchOptions,
8
+ MemoryRuntimeContext,
9
+ } from "./types";
10
+ export function createMemoryRuntimeContext(context: MemoryBackendOperationContext): MemoryRuntimeContext {
11
+ const settings = context.session?.settings;
12
+ return {
13
+ async status() {
14
+ if (!settings) {
15
+ return {
16
+ backend: "off" as const,
17
+ active: false,
18
+ writable: false,
19
+ searchable: false,
20
+ message: "No active agent session.",
21
+ };
22
+ }
23
+ const backend = await resolveMemoryBackend(settings);
24
+ return backend.status
25
+ ? await backend.status(context)
26
+ : {
27
+ backend: backend.id,
28
+ active: backend.id !== "off",
29
+ writable: false,
30
+ searchable: false,
31
+ message: "This memory backend does not expose structured status.",
32
+ };
33
+ },
34
+ async search(query: string, options?: MemoryBackendSearchOptions) {
35
+ if (!settings) return unavailableSearch("off", query, "No active agent session.");
36
+ const backend = await resolveMemoryBackend(settings);
37
+ return backend.search
38
+ ? await backend.search(context, query, options)
39
+ : unavailableSearch(backend.id, query, `Memory search is not available for the ${backend.id} backend.`);
40
+ },
41
+ async save(input: string | MemoryBackendSaveInput) {
42
+ if (!settings) return unavailableSave("off", "No active agent session.");
43
+ const backend = await resolveMemoryBackend(settings);
44
+ const normalized = typeof input === "string" ? { content: input } : input;
45
+ return backend.save
46
+ ? await backend.save(context, normalized)
47
+ : unavailableSave(backend.id, `Memory save is not available for the ${backend.id} backend.`);
48
+ },
49
+ };
50
+ }
51
+
52
+ export function createSessionMemoryRuntimeContext(
53
+ session: AgentSession,
54
+ agentDir: string,
55
+ cwd: string,
56
+ ): MemoryRuntimeContext {
57
+ return createMemoryRuntimeContext({ agentDir, cwd, session });
58
+ }
59
+
60
+ function unavailableSearch(backend: MemoryBackendId, query: string, message: string) {
61
+ return { backend, query, count: 0, items: [], message };
62
+ }
63
+
64
+ function unavailableSave(backend: MemoryBackendId, message: string) {
65
+ return { backend, stored: 0, message };
66
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Memory backend abstraction.
3
3
  *
4
- * Backends are mutually exclusive — `await resolveMemoryBackend(settings)` resolves
4
+ * Backends are mutually exclusive — `await resolveMemoryBackend(settings)` returns
5
5
  * exactly one. Implementations MUST be self-contained: they own the per-session
6
6
  * state they create in `start()` and tear it down on `clear()`.
7
7
  */
@@ -15,6 +15,73 @@ import type { AgentSession } from "../session/agent-session";
15
15
 
16
16
  export type MemoryBackendId = "off" | "local" | "hindsight" | "mnemopi";
17
17
 
18
+ export interface MemoryBackendStatus {
19
+ backend: MemoryBackendId;
20
+ active: boolean;
21
+ writable: boolean;
22
+ searchable: boolean;
23
+ scope?: string;
24
+ retainBank?: string;
25
+ recallBanks?: string[];
26
+ workingCount?: number;
27
+ episodicCount?: number;
28
+ tripleCount?: number;
29
+ lastMemory?: string;
30
+ lastRecall?: boolean;
31
+ database?: string;
32
+ message?: string;
33
+ error?: string;
34
+ }
35
+
36
+ export interface MemoryBackendSearchOptions {
37
+ limit?: number;
38
+ /** Best-effort abort signal. Backends may only observe it before/after an underlying recall call. */
39
+ signal?: AbortSignal;
40
+ }
41
+
42
+ export interface MemoryBackendSearchItem {
43
+ id?: string;
44
+ content: string;
45
+ source?: string;
46
+ timestamp?: string;
47
+ score?: number;
48
+ }
49
+
50
+ export interface MemoryBackendSearchResult {
51
+ backend: MemoryBackendId;
52
+ query: string;
53
+ count: number;
54
+ items: MemoryBackendSearchItem[];
55
+ message?: string;
56
+ }
57
+
58
+ export interface MemoryBackendSaveInput {
59
+ content: string;
60
+ context?: string;
61
+ source?: string;
62
+ importance?: number;
63
+ }
64
+
65
+ export interface MemoryBackendSaveResult {
66
+ backend: MemoryBackendId;
67
+ stored: number;
68
+ ids?: string[];
69
+ queued?: boolean;
70
+ message?: string;
71
+ }
72
+
73
+ export interface MemoryBackendOperationContext {
74
+ agentDir: string;
75
+ cwd: string;
76
+ session?: AgentSession;
77
+ }
78
+
79
+ export interface MemoryRuntimeContext {
80
+ status(): Promise<MemoryBackendStatus>;
81
+ search(query: string, options?: MemoryBackendSearchOptions): Promise<MemoryBackendSearchResult>;
82
+ save(input: string | MemoryBackendSaveInput): Promise<MemoryBackendSaveResult>;
83
+ }
84
+
18
85
  export interface MemoryBackendStartOptions {
19
86
  session: AgentSession;
20
87
  settings: Settings;
@@ -53,6 +120,19 @@ export interface MemoryBackend {
53
120
  /** Force consolidation/retain to happen now (slash `/memory enqueue`). */
54
121
  enqueue(agentDir: string, cwd: string, session?: AgentSession): Promise<void>;
55
122
 
123
+ /** Structured state for UI, slash commands, and extensions. */
124
+ status?(context: MemoryBackendOperationContext): Promise<MemoryBackendStatus>;
125
+
126
+ /** Explicit user-facing semantic/lexical search. */
127
+ search?(
128
+ context: MemoryBackendOperationContext,
129
+ query: string,
130
+ options?: MemoryBackendSearchOptions,
131
+ ): Promise<MemoryBackendSearchResult>;
132
+
133
+ /** Explicit user-facing save operation. */
134
+ save?(context: MemoryBackendOperationContext, input: MemoryBackendSaveInput): Promise<MemoryBackendSaveResult>;
135
+
56
136
  /** Render backend-specific memory statistics as markdown (`/memory stats`). */
57
137
  stats?(agentDir: string, cwd: string, session?: AgentSession): Promise<string | undefined>;
58
138
 
@@ -5,10 +5,15 @@ import type { Mnemopi } from "@oh-my-pi/pi-mnemopi";
5
5
  import type * as MnemopiDiagnoseNs from "@oh-my-pi/pi-mnemopi/diagnose";
6
6
  import type { DiagnosticSummary } from "@oh-my-pi/pi-mnemopi/diagnose";
7
7
  import { logger } from "@oh-my-pi/pi-utils";
8
-
9
8
  import type { ModelRegistry } from "../config/model-registry";
10
9
  import { resolveRoleSelection } from "../config/model-resolver";
11
- import type { MemoryBackend, MemoryBackendStartOptions } from "../memory-backend/types";
10
+ import type {
11
+ MemoryBackend,
12
+ MemoryBackendSaveInput,
13
+ MemoryBackendSearchItem,
14
+ MemoryBackendStartOptions,
15
+ MemoryBackendStatus,
16
+ } from "../memory-backend/types";
12
17
  import memoryConsolidationPrompt from "../prompts/system/memory-consolidation-system.md" with { type: "text" };
13
18
  import memoryExtractionPrompt from "../prompts/system/memory-extraction-system.md" with { type: "text" };
14
19
  import type { AgentSession } from "../session/agent-session";
@@ -166,6 +171,101 @@ export const mnemopiBackend: MemoryBackend = {
166
171
  return renderMnemopiDiagnostics(summaries);
167
172
  },
168
173
 
174
+ async status({ agentDir, session }): Promise<MemoryBackendStatus> {
175
+ const state = getMnemopiSessionState(session);
176
+ const primary = state?.aliasOf ?? state;
177
+ if (!primary) {
178
+ return {
179
+ backend: "mnemopi",
180
+ active: false,
181
+ writable: false,
182
+ searchable: false,
183
+ message: "Mnemopi backend is not initialised for this session.",
184
+ };
185
+ }
186
+
187
+ const { targets, owned } = createStatsTargets(agentDir, session);
188
+ try {
189
+ if (targets.length === 0) {
190
+ return {
191
+ backend: "mnemopi",
192
+ active: false,
193
+ writable: false,
194
+ searchable: false,
195
+ message: "Mnemopi backend is configured but not initialised for this session.",
196
+ };
197
+ }
198
+ return summarizeMnemopiStatus(targets, session);
199
+ } finally {
200
+ for (const memory of owned) memory.close();
201
+ }
202
+ },
203
+
204
+ async search({ session }, query, options) {
205
+ const state = getMnemopiSessionState(session);
206
+ const primary = state?.aliasOf ?? state;
207
+ if (!primary) {
208
+ return {
209
+ backend: "mnemopi",
210
+ query,
211
+ count: 0,
212
+ items: [],
213
+ message: "Mnemopi backend is not initialised for this session.",
214
+ };
215
+ }
216
+ if (options?.signal?.aborted) {
217
+ return { backend: "mnemopi", query, count: 0, items: [], message: "Search aborted." };
218
+ }
219
+ const limit = clampLimit(options?.limit);
220
+ const results = (await primary.recallResultsScoped(query)).slice(0, limit);
221
+ if (options?.signal?.aborted) {
222
+ return { backend: "mnemopi", query, count: 0, items: [], message: "Search aborted." };
223
+ }
224
+ const items: MemoryBackendSearchItem[] = results.map(result => ({
225
+ id: result.id,
226
+ content: result.content,
227
+ source: result.source ?? undefined,
228
+ timestamp: result.timestamp ?? undefined,
229
+ score: result.score,
230
+ }));
231
+ return { backend: "mnemopi", query, count: items.length, items };
232
+ },
233
+
234
+ async save({ cwd, session }, input: MemoryBackendSaveInput) {
235
+ const state = getMnemopiSessionState(session);
236
+ const primary = state?.aliasOf ?? state;
237
+ if (!primary) {
238
+ return {
239
+ backend: "mnemopi",
240
+ stored: 0,
241
+ message: "Mnemopi backend is not initialised for this session.",
242
+ };
243
+ }
244
+ const content = input.content.trim();
245
+ if (!content) return { backend: "mnemopi", stored: 0, message: "Memory content is empty." };
246
+ const id = primary.rememberScoped(content, {
247
+ source: input.source || "coding-agent-memory-command",
248
+ importance: normalizeImportance(input.importance),
249
+ metadata: {
250
+ session_id: primary.sessionId,
251
+ cwd,
252
+ context: input.context ?? null,
253
+ operation: "memory.save",
254
+ },
255
+ scope: "bank",
256
+ extract: true,
257
+ extractEntities: true,
258
+ veracity: "user",
259
+ memoryType: "fact",
260
+ });
261
+ return {
262
+ backend: "mnemopi",
263
+ stored: id ? 1 : 0,
264
+ ids: id ? [id] : [],
265
+ message: id ? undefined : "Mnemopi did not return a stored memory id.",
266
+ };
267
+ },
268
+
169
269
  async preCompactionContext(messages, _settings, session): Promise<string | undefined> {
170
270
  const state = getMnemopiSessionState(session);
171
271
  return await state?.recallForCompaction(messages);
@@ -247,6 +347,52 @@ function renderMnemopiStats(targets: readonly MnemopiStatsTarget[]): string {
247
347
  return lines.join("\n");
248
348
  }
249
349
 
350
+ function summarizeMnemopiStatus(
351
+ targets: readonly MnemopiStatsTarget[],
352
+ session: AgentSession | undefined,
353
+ ): MemoryBackendStatus {
354
+ let workingCount = 0;
355
+ let episodicCount = 0;
356
+ let tripleCount = 0;
357
+ let lastMemory: string | undefined;
358
+ let database: string | undefined;
359
+ for (const target of targets) {
360
+ const stats = target.memory.getStats();
361
+ workingCount += statCount(stats.beam.working_memory);
362
+ episodicCount += statCount(stats.beam.episodic_memory);
363
+ tripleCount += stats.beam.triples.total;
364
+ lastMemory ??= stats.last_memory ?? undefined;
365
+ database ??= stats.database ? shortenPath(stats.database) : undefined;
366
+ }
367
+ const state = getMnemopiSessionState(session);
368
+ const primary = state?.aliasOf ?? state;
369
+ return {
370
+ backend: "mnemopi",
371
+ active: true,
372
+ writable: true,
373
+ searchable: true,
374
+ scope: primary?.config.scoping,
375
+ retainBank: primary?.getScopedRetainTarget().bank ?? targets[0]?.bank,
376
+ recallBanks: primary?.getScopedRecallTargets().map(target => target.bank) ?? targets.map(target => target.bank),
377
+ workingCount,
378
+ episodicCount,
379
+ tripleCount,
380
+ lastMemory,
381
+ lastRecall: Boolean(primary?.lastRecallSnippet),
382
+ database,
383
+ };
384
+ }
385
+
386
+ function clampLimit(limit: number | undefined): number {
387
+ if (!Number.isFinite(limit)) return 10;
388
+ return Math.max(1, Math.min(50, Math.trunc(limit ?? 10)));
389
+ }
390
+
391
+ function normalizeImportance(value: number | undefined): number {
392
+ if (!Number.isFinite(value)) return 0.75;
393
+ return Math.max(0, Math.min(1, value ?? 0.75));
394
+ }
395
+
250
396
  function renderMnemopiDiagnostics(entries: readonly { bank: string; summary: DiagnosticSummary }[]): string {
251
397
  const lines = [
252
398
  "# Mnemopi Memory Diagnostics",
@@ -343,8 +489,8 @@ async function resolveMnemopiProviderOptions(
343
489
  return {
344
490
  ...base,
345
491
  llm: async (prompt, opts) => {
346
- const apiKey = await modelRegistry.getApiKey(model, sessionId);
347
- if (!apiKey) {
492
+ const hasApiKey = await modelRegistry.getApiKey(model, sessionId);
493
+ if (!hasApiKey) {
348
494
  logger.warn("Mnemopi: smol completion requested but no current API key is available.", {
349
495
  provider: model.provider,
350
496
  model: model.id,
@@ -360,6 +506,7 @@ async function resolveMnemopiProviderOptions(
360
506
  apiKey: modelRegistry.resolver(model.provider, {
361
507
  sessionId,
362
508
  baseUrl: model.baseUrl,
509
+ modelId: model.id,
363
510
  }),
364
511
  maxTokens: opts?.maxTokens,
365
512
  temperature: opts?.temperature,