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

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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -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,
@@ -71,7 +71,12 @@ import {
71
71
  type SessionInfo as StoredSessionInfo,
72
72
  type UsageStatistics,
73
73
  } from "../../session/session-manager";
74
- import { ACP_BUILTIN_SLASH_COMMANDS, executeAcpBuiltinSlashCommand } from "../../slash-commands/acp-builtins";
74
+ import {
75
+ ACP_BUILTIN_RESERVED_NAMES,
76
+ ACP_BUILTIN_SLASH_COMMANDS,
77
+ executeAcpBuiltinSlashCommand,
78
+ isAcpBuiltinShadowedName,
79
+ } from "../../slash-commands/acp-builtins";
75
80
  import { AUTO_THINKING, parseConfiguredThinkingLevel } from "../../thinking";
76
81
  import { normalizeLocalScheme } from "../../tools/path-utils";
77
82
  import { runResolveInvocation } from "../../tools/resolve";
@@ -117,6 +122,7 @@ type PromptQueueState = {
117
122
  promise: Promise<void>;
118
123
  release: (() => void) | undefined;
119
124
  };
125
+ type PromptLifecycleError = Error & { readonly code: "ACP_SESSION_CLOSED" };
120
126
 
121
127
  type PromptTurnState = {
122
128
  userMessageId: string;
@@ -158,6 +164,9 @@ type ManagedSessionRecord = {
158
164
  // Installed inside `#scheduleBootstrapUpdates` (post-race-guard); released
159
165
  // in `#disposeSessionRecord`. Lives independent of any prompt turn.
160
166
  lifetimeUnsubscribe: (() => void) | undefined;
167
+ closedError: PromptLifecycleError | undefined;
168
+ promptEventHandlers: Set<Promise<void>>;
169
+ extensionUserMessageTasks: Set<Promise<void>>;
161
170
  };
162
171
 
163
172
  type ReplayableMessage = {
@@ -594,7 +603,23 @@ export class AcpAgent implements Agent {
594
603
  const record = this.#getSessionRecord(params.sessionId);
595
604
  const activeTurn = record.promptTurn;
596
605
  if (activeTurn && !activeTurn.settled && record.session.isStreaming) {
597
- throw new Error("ACP prompt already in progress for this session");
606
+ // New prompt arrived while the previous turn is still in-flight (e.g. the
607
+ // client sent a message immediately after pressing stop, before or without
608
+ // a preceding session/cancel notification). Implicitly cancel the running
609
+ // turn so the new prompt can queue behind the abort cleanup — identical to
610
+ // what cancel() does when called explicitly. #beginCancelCleanup is
611
+ // idempotent, so a concurrent session/cancel notification is harmless.
612
+ // Mirror cancel()'s timeout handling: if abort() hangs past the cleanup
613
+ // timeout, close the managed session instead of leaving it registered
614
+ // with a still-streaming AgentSession. The queued prompt below observes
615
+ // the same cleanup rejection and fails accordingly.
616
+ this.#beginCancelCleanup(record, activeTurn).catch(async (error: unknown) => {
617
+ logger.warn("ACP cancel cleanup timed out; closing session", {
618
+ sessionId: record.session.sessionId,
619
+ error,
620
+ });
621
+ await this.#closeManagedSession(params.sessionId, record);
622
+ });
598
623
  }
599
624
  return await this.#queuePrompt(record, async () => {
600
625
  const previousTurn = record.promptTurn;
@@ -607,6 +632,7 @@ export class AcpAgent implements Agent {
607
632
  await previousTurn.promise.catch(() => undefined);
608
633
  await previousTurn.cleanup;
609
634
  }
635
+ this.#throwIfRecordClosed(record);
610
636
 
611
637
  const converted = this.#convertPromptBlocks(params.prompt);
612
638
  const pendingPrompt = Promise.withResolvers<PromptResponse>();
@@ -623,7 +649,7 @@ export class AcpAgent implements Agent {
623
649
  };
624
650
 
625
651
  record.promptTurn.unsubscribe = record.session.subscribe(event => {
626
- void this.#handlePromptEvent(record, event);
652
+ this.#trackPromptEvent(record, event);
627
653
  });
628
654
 
629
655
  this.#runPromptOrCommand(record, converted.text, converted.images).catch((error: unknown) => {
@@ -643,6 +669,7 @@ export class AcpAgent implements Agent {
643
669
  release: releaseQueue,
644
670
  };
645
671
  await previousQueue.promise;
672
+ this.#throwIfRecordClosed(record);
646
673
  try {
647
674
  return await run();
648
675
  } finally {
@@ -653,6 +680,55 @@ export class AcpAgent implements Agent {
653
680
  }
654
681
  }
655
682
 
683
+ #throwIfRecordClosed(record: ManagedSessionRecord): void {
684
+ if (record.closedError) {
685
+ throw record.closedError;
686
+ }
687
+ }
688
+
689
+ #createPromptLifecycleError(message: string): PromptLifecycleError {
690
+ return Object.assign(new Error(message), { code: "ACP_SESSION_CLOSED" as const });
691
+ }
692
+
693
+ #trackPromptEvent(record: ManagedSessionRecord, event: AgentSessionEvent): void {
694
+ const handling = this.#handlePromptEvent(record, event).catch((error: unknown) => {
695
+ logger.warn("ACP prompt event handler failed", { error });
696
+ });
697
+ record.promptEventHandlers.add(handling);
698
+ void handling.finally(() => {
699
+ record.promptEventHandlers.delete(handling);
700
+ });
701
+ }
702
+
703
+ async #waitForPromptEventHandlers(record: ManagedSessionRecord): Promise<void> {
704
+ while (record.promptEventHandlers.size > 0) {
705
+ await Promise.allSettled(Array.from(record.promptEventHandlers));
706
+ }
707
+ }
708
+
709
+ #trackExtensionUserMessage(record: ManagedSessionRecord, task: Promise<void>): void {
710
+ const tracked = task.catch((error: unknown) => {
711
+ logger.warn("ACP extension sendUserMessage failed", { error });
712
+ });
713
+ record.extensionUserMessageTasks.add(tracked);
714
+ void tracked.finally(() => {
715
+ record.extensionUserMessageTasks.delete(tracked);
716
+ });
717
+ }
718
+
719
+ async #waitForExtensionUserMessages(
720
+ record: ManagedSessionRecord,
721
+ baseline: ReadonlySet<Promise<void>>,
722
+ ): Promise<void> {
723
+ while (true) {
724
+ const pending = Array.from(record.extensionUserMessageTasks).filter(task => !baseline.has(task));
725
+ if (pending.length === 0) {
726
+ return;
727
+ }
728
+ await Promise.allSettled(pending);
729
+ }
730
+ }
731
+
656
732
  async #runPromptOrCommand(record: ManagedSessionRecord, text: string, images: AgentImageContent[]): Promise<void> {
657
733
  const skillResult = await this.#tryRunSkillCommand(record, text);
658
734
  if (skillResult) {
@@ -699,7 +775,18 @@ export class AcpAgent implements Agent {
699
775
  return;
700
776
  }
701
777
 
702
- await record.session.prompt(text, { images });
778
+ const extensionPromptBaseline = new Set(record.extensionUserMessageTasks);
779
+ const agentInvoked = await record.session.prompt(text, { images });
780
+ // Extension and custom-TS commands are handled locally inside session.prompt().
781
+ // An ACP extension command can still call pi.sendUserMessage(), which starts
782
+ // an async nested prompt through the extension runtime. Keep the ACP turn
783
+ // subscribed until those scheduled prompts and their event handlers drain;
784
+ // only then is `false` proof that the slash command was purely local.
785
+ if (!agentInvoked) {
786
+ await this.#waitForExtensionUserMessages(record, extensionPromptBaseline);
787
+ await this.#waitForPromptEventHandlers(record);
788
+ this.#finishPrompt(record, { stopReason: "end_turn" });
789
+ }
703
790
  }
704
791
 
705
792
  async #tryRunSkillCommand(record: ManagedSessionRecord, text: string): Promise<boolean> {
@@ -991,6 +1078,9 @@ export class AcpAgent implements Agent {
991
1078
  liveMessageProgress: undefined,
992
1079
  toolArgsById: new Map(),
993
1080
  extensionsConfigured: false,
1081
+ closedError: undefined,
1082
+ promptEventHandlers: new Set(),
1083
+ extensionUserMessageTasks: new Set(),
994
1084
  lifetimeUnsubscribe: undefined,
995
1085
  };
996
1086
  }
@@ -1582,10 +1672,12 @@ export class AcpAgent implements Agent {
1582
1672
  commands.push(command);
1583
1673
  };
1584
1674
 
1585
- // Advertise in the order dispatch resolves them: ACP builtins first
1586
- // (so core commands like `/model`, `/mcp`, `/todo` cannot be shadowed),
1587
- // then skills, then custom/user commands, then file-based slash
1588
- // commands. `appendCommand` dedupes by name so earlier entries win.
1675
+ // Advertise in the order dispatch resolves them (mirrors AgentSession
1676
+ // dispatch: builtins skills extensions custom TS → file-based).
1677
+ // `appendCommand` dedupes by name so earlier entries win; extension
1678
+ // commands therefore correctly shadow custom TS commands of the same
1679
+ // name, matching the runtime behaviour of #tryExecuteExtensionCommand
1680
+ // running before #tryExecuteCustomCommand.
1589
1681
  for (const command of ACP_BUILTIN_SLASH_COMMANDS) {
1590
1682
  appendCommand(command);
1591
1683
  }
@@ -1600,6 +1692,20 @@ export class AcpAgent implements Agent {
1600
1692
  }
1601
1693
  }
1602
1694
 
1695
+ for (const command of session.extensionRunner?.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES) ?? []) {
1696
+ // Reserved-set filtering in getRegisteredCommands only covers exact
1697
+ // names; colon-namespaced names whose prefix is a builtin (e.g.
1698
+ // `model:foo`) would still dispatch to the builtin in ACP.
1699
+ if (isAcpBuiltinShadowedName(command.name)) {
1700
+ continue;
1701
+ }
1702
+ appendCommand({
1703
+ name: command.name,
1704
+ description: command.description ?? "(extension command)",
1705
+ input: { hint: "arguments" },
1706
+ });
1707
+ }
1708
+
1603
1709
  for (const command of session.customCommands) {
1604
1710
  appendCommand({
1605
1711
  name: command.command.name,
@@ -2069,9 +2175,7 @@ export class AcpAgent implements Agent {
2069
2175
  });
2070
2176
  },
2071
2177
  sendUserMessage: (content, options) => {
2072
- record.session.sendUserMessage(content, options).catch((error: unknown) => {
2073
- logger.warn("ACP extension sendUserMessage failed", { error });
2074
- });
2178
+ this.#trackExtensionUserMessage(record, record.session.sendUserMessage(content, options));
2075
2179
  },
2076
2180
  appendEntry: (customType, data) => {
2077
2181
  record.session.sessionManager.appendCustomEntry(customType, data);
@@ -2224,6 +2328,7 @@ export class AcpAgent implements Agent {
2224
2328
  }
2225
2329
 
2226
2330
  async #closeManagedSession(sessionId: string, record: ManagedSessionRecord): Promise<void> {
2331
+ record.closedError ??= this.#createPromptLifecycleError("ACP session closed before queued prompt could run");
2227
2332
  this.#sessions.delete(sessionId);
2228
2333
  await this.#cancelPromptForClose(record);
2229
2334
  await this.#disposeSessionRecord(record);
@@ -2279,6 +2384,9 @@ export class AcpAgent implements Agent {
2279
2384
  await Promise.all(
2280
2385
  records.map(async ([sessionId, record]) => {
2281
2386
  try {
2387
+ record.closedError ??= this.#createPromptLifecycleError(
2388
+ "ACP agent disposed before queued prompt could run",
2389
+ );
2282
2390
  await this.#cancelPromptForClose(record);
2283
2391
  await this.#disposeSessionRecord(record);
2284
2392
  } catch (error) {