@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.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.
Files changed (188) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +69 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -272,8 +272,8 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
272
272
  if (this.#closed) throw new Error("Writer closed");
273
273
  if (this.#error) throw this.#error;
274
274
  try {
275
- const existing = this.#storage.existsSync(this.#path) ? this.#storage.readTextSync(this.#path) : "";
276
- this.#storage.writeTextSync(this.#path, `${existing}${line}`);
275
+ // O(1) chunked append see MemorySessionStorage.appendChunkSync.
276
+ this.#storage.appendChunkSync(this.#path, line);
277
277
  } catch (err) {
278
278
  throw this.#recordError(err);
279
279
  }
@@ -302,8 +302,26 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
302
302
  }
303
303
  }
304
304
 
305
+ /**
306
+ * Mirror entry stored per path. Chunks accumulate via O(1) `push` on
307
+ * `appendChunkSync`; readers materialise into a single string lazily.
308
+ * `byteLen` is kept in sync so `statSync` is O(1) (and returns true UTF-8
309
+ * bytes, not character count).
310
+ */
311
+ interface MirrorEntry {
312
+ chunks: string[];
313
+ byteLen: number;
314
+ mtimeMs: number;
315
+ }
316
+
317
+ function materialiseMirror(entry: MirrorEntry): string {
318
+ if (entry.chunks.length === 0) return "";
319
+ if (entry.chunks.length === 1) return entry.chunks[0];
320
+ return entry.chunks.join("");
321
+ }
322
+
305
323
  export class MemorySessionStorage implements SessionStorage {
306
- #files = new Map<string, { content: string; mtimeMs: number }>();
324
+ #files = new Map<string, MirrorEntry>();
307
325
 
308
326
  ensureDirSync(_dir: string): void {
309
327
  // No-op for in-memory storage.
@@ -314,20 +332,40 @@ export class MemorySessionStorage implements SessionStorage {
314
332
  }
315
333
 
316
334
  writeTextSync(path: string, content: string): void {
317
- this.#files.set(path, { content, mtimeMs: Date.now() });
335
+ this.#files.set(path, {
336
+ chunks: content.length === 0 ? [] : [content],
337
+ byteLen: Buffer.byteLength(content, "utf-8"),
338
+ mtimeMs: Date.now(),
339
+ });
340
+ }
341
+
342
+ /**
343
+ * Internal O(1) append used by {@link MemorySessionStorageWriter}. Lazily
344
+ * creates the entry. External callers should go through `openWriter()`
345
+ * rather than touching the mirror directly.
346
+ */
347
+ appendChunkSync(path: string, chunk: string): void {
348
+ let entry = this.#files.get(path);
349
+ if (!entry) {
350
+ entry = { chunks: [], byteLen: 0, mtimeMs: Date.now() };
351
+ this.#files.set(path, entry);
352
+ }
353
+ entry.chunks.push(chunk);
354
+ entry.byteLen += Buffer.byteLength(chunk, "utf-8");
355
+ entry.mtimeMs = Date.now();
318
356
  }
319
357
 
320
358
  readTextSync(path: string): string {
321
359
  const entry = this.#files.get(path);
322
360
  if (!entry) throw new Error(`File not found: ${path}`);
323
- return entry.content;
361
+ return materialiseMirror(entry);
324
362
  }
325
363
 
326
364
  statSync(path: string): SessionStorageStat {
327
365
  const entry = this.#files.get(path);
328
366
  if (!entry) throw new Error(`File not found: ${path}`);
329
367
  return {
330
- size: entry.content.length,
368
+ size: entry.byteLen,
331
369
  mtimeMs: entry.mtimeMs,
332
370
  mtime: new Date(entry.mtimeMs),
333
371
  };
@@ -353,13 +391,35 @@ export class MemorySessionStorage implements SessionStorage {
353
391
  readText(path: string): Promise<string> {
354
392
  const entry = this.#files.get(path);
355
393
  if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
356
- return Promise.resolve(entry.content);
394
+ return Promise.resolve(materialiseMirror(entry));
357
395
  }
358
396
 
359
397
  readTextPrefix(path: string, maxBytes: number): Promise<string> {
360
398
  const entry = this.#files.get(path);
361
399
  if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
362
- return Promise.resolve(entry.content.slice(0, maxBytes));
400
+ if (entry.chunks.length === 0 || maxBytes <= 0) return Promise.resolve("");
401
+
402
+ // Walk chunks until the byte budget is exhausted. Avoids materialising
403
+ // the full mirror just to slice a prefix — bounded work for big files.
404
+ let accumulatedBytes = 0;
405
+ const out: string[] = [];
406
+ for (const chunk of entry.chunks) {
407
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
408
+ if (accumulatedBytes + chunkBytes <= maxBytes) {
409
+ out.push(chunk);
410
+ accumulatedBytes += chunkBytes;
411
+ if (accumulatedBytes === maxBytes) break;
412
+ continue;
413
+ }
414
+ // Boundary chunk: slice in byte space and decode. Result MAY be
415
+ // shorter than the budget if a multi-byte codepoint straddles the
416
+ // boundary — matches `peekFile` semantics (partial decode at cap).
417
+ const remainingBytes = maxBytes - accumulatedBytes;
418
+ const utf8 = Buffer.from(chunk, "utf-8");
419
+ out.push(utf8Decoder.decode(utf8.subarray(0, remainingBytes)));
420
+ break;
421
+ }
422
+ return Promise.resolve(out.join(""));
363
423
  }
364
424
 
365
425
  writeText(path: string, content: string): Promise<void> {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Public shape of the `shake` operation, kept in a dependency-free leaf module
3
+ * so slash-command registries and controllers can import `formatShakeSummary`
4
+ * without pulling in the heavy `agent-session` module graph (which would form
5
+ * an import cycle through the slash-command registry).
6
+ */
7
+
8
+ /** Mode selector for `AgentSession.shake`. */
9
+ export type ShakeMode = "elide" | "images";
10
+
11
+ /** Outcome of an `AgentSession.shake` run. */
12
+ export interface ShakeResult {
13
+ mode: ShakeMode;
14
+ /** Whole tool-call results dropped. */
15
+ toolResultsDropped: number;
16
+ /** Large fenced/XML blocks dropped. */
17
+ blocksDropped: number;
18
+ /** Image blocks removed (images mode only). */
19
+ imagesDropped?: number;
20
+ /** Estimated context tokens reclaimed. */
21
+ tokensFreed: number;
22
+ /** Session artifact holding the dropped originals, when persisted. */
23
+ artifactId?: string;
24
+ }
25
+
26
+ /** One-line operator summary of a {@link ShakeResult} (shared by TUI + ACP). */
27
+ export function formatShakeSummary(result: ShakeResult): string {
28
+ if (result.mode === "images") {
29
+ const n = result.imagesDropped ?? 0;
30
+ return n === 0
31
+ ? "No images found in this session."
32
+ : `Dropped ${n} image${n === 1 ? "" : "s"} from this session.`;
33
+ }
34
+ const parts: string[] = [];
35
+ if (result.toolResultsDropped > 0) {
36
+ parts.push(`${result.toolResultsDropped} tool result${result.toolResultsDropped === 1 ? "" : "s"}`);
37
+ }
38
+ if (result.blocksDropped > 0) {
39
+ parts.push(`${result.blocksDropped} block${result.blocksDropped === 1 ? "" : "s"}`);
40
+ }
41
+ if (parts.length === 0) return "Nothing to shake.";
42
+ return `Shook ${parts.join(" + ")} (~${result.tokensFreed} tokens freed).`;
43
+ }
@@ -21,6 +21,7 @@ import {
21
21
  } from "../extensibility/plugins/marketplace";
22
22
  import { resolveMemoryBackend } from "../memory-backend";
23
23
  import type { InteractiveModeContext } from "../modes/types";
24
+ import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
24
25
  import { getChangelogPath, parseChangelog } from "../utils/changelog";
25
26
  import { buildContextReportText } from "./helpers/context-report";
26
27
  import { formatDuration } from "./helpers/format";
@@ -57,6 +58,14 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
57
58
  return commandConsumed();
58
59
  };
59
60
 
61
+ /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
62
+ function parseShakeMode(args: string): ShakeMode | { error: string } {
63
+ const verb = args.trim().toLowerCase();
64
+ if (verb === "" || verb === "elide") return "elide";
65
+ if (verb === "images") return "images";
66
+ return { error: `Unknown /shake mode "${verb}". Use elide or images.` };
67
+ }
68
+
60
69
  const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
61
70
  {
62
71
  name: "settings",
@@ -811,27 +820,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
811
820
  },
812
821
  },
813
822
  {
814
- name: "drop-images",
815
- description: "Strip every image from this session's history",
816
- acpDescription: "Drop all images from the conversation history",
817
- handle: async (_command, runtime) => {
818
- const { removed } = await runtime.session.dropImages();
819
- await runtime.output(
820
- removed === 0
821
- ? "No images found in this session."
822
- : `Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`,
823
- );
823
+ name: "shake",
824
+ description: "Drop heavy content from context (tool results, large blocks)",
825
+ acpDescription: "Shake heavy content out of the conversation context",
826
+ subcommands: [
827
+ { name: "elide", description: "Strip tool results + large blocks (default)" },
828
+ { name: "images", description: "Strip image blocks" },
829
+ ],
830
+ acpInputHint: "[elide|images]",
831
+ allowArgs: true,
832
+ handle: async (command, runtime) => {
833
+ const mode = parseShakeMode(command.args);
834
+ if (typeof mode !== "string") return usage(mode.error, runtime);
835
+ const result = await runtime.session.shake(mode);
836
+ await runtime.output(formatShakeSummary(result));
824
837
  return commandConsumed();
825
838
  },
826
- handleTui: async (_command, runtime) => {
839
+ handleTui: async (command, runtime) => {
827
840
  runtime.ctx.editor.setText("");
828
- const { removed } = await runtime.ctx.session.dropImages();
829
- if (removed === 0) {
830
- runtime.ctx.showStatus("No images found in this session.");
841
+ const mode = parseShakeMode(command.args);
842
+ if (typeof mode !== "string") {
843
+ runtime.ctx.showWarning(mode.error);
831
844
  return;
832
845
  }
833
- runtime.ctx.rebuildChatFromMessages();
834
- runtime.ctx.showStatus(`Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`);
846
+ await runtime.ctx.handleShakeCommand(mode);
835
847
  },
836
848
  },
837
849
  {
@@ -864,6 +876,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
864
876
  await runtime.ctx.handleBtwCommand(question);
865
877
  },
866
878
  },
879
+ {
880
+ name: "omfg",
881
+ description: "Forge a TTSR rule from a complaint to stop a recurring behavior",
882
+ inlineHint: "<complaint>",
883
+ allowArgs: true,
884
+ handleTui: async (command, runtime) => {
885
+ const complaint = command.text.slice(`/${command.name}`.length).trim();
886
+ runtime.ctx.editor.setText("");
887
+ await runtime.ctx.handleOmfgCommand(complaint);
888
+ },
889
+ },
867
890
  {
868
891
  name: "retry",
869
892
  description: "Retry the last failed agent turn",
@@ -21,7 +21,7 @@ import type { HindsightSessionState } from "../hindsight/state";
21
21
  import type { LocalProtocolOptions } from "../internal-urls";
22
22
  import { callTool } from "../mcp/client";
23
23
  import type { MCPManager } from "../mcp/manager";
24
- import type { MnemosyneSessionState } from "../mnemosyne/state";
24
+ import type { MnemopiSessionState } from "../mnemopi/state";
25
25
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
26
26
  import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
27
27
  import { AgentRegistry } from "../registry/agent-registry";
@@ -186,7 +186,7 @@ export interface ExecutorOptions {
186
186
  */
187
187
  parentArtifactManager?: ArtifactManager;
188
188
  parentHindsightSessionState?: HindsightSessionState;
189
- parentMnemosyneSessionState?: MnemosyneSessionState;
189
+ parentMnemopiSessionState?: MnemopiSessionState;
190
190
  /** Parent agent's eval executor session id. Subagents reuse it so eval state is shared. */
191
191
  parentEvalSessionId?: string;
192
192
  /**
@@ -633,7 +633,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
633
633
  if (atMaxDepth && toolNames?.includes("task")) {
634
634
  toolNames = toolNames.filter(name => name !== "task");
635
635
  }
636
- // IRC is always available; the [COOP] prompt advertises it, so a restricted
636
+ // IRC is always available; the COOP prompt section advertises it, so a restricted
637
637
  // whitelist must still carry `irc` for the subagent to actually use it.
638
638
  if (toolNames && !toolNames.includes("irc")) {
639
639
  toolNames = [...toolNames, "irc"];
@@ -1245,7 +1245,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1245
1245
  spawns: spawnsEnv,
1246
1246
  taskDepth: childDepth,
1247
1247
  parentHindsightSessionState: options.parentHindsightSessionState,
1248
- parentMnemosyneSessionState: options.parentMnemosyneSessionState,
1248
+ parentMnemopiSessionState: options.parentMnemopiSessionState,
1249
1249
  parentTaskPrefix: id,
1250
1250
  agentId: id,
1251
1251
  agentDisplayName: agent.name,
@@ -1446,9 +1446,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1446
1446
  );
1447
1447
  await awaitAbortable(session.waitForIdle());
1448
1448
  } catch (err) {
1449
- logger.error("Subagent prompt failed", {
1450
- error: err instanceof Error ? err.message : String(err),
1451
- });
1449
+ if (abortSignal.aborted || err instanceof ToolAbortError) {
1450
+ // Benign control-flow exit user cancel (^C) or compaction aborting
1451
+ // pending operations both surface here as ToolAbortError. The outer
1452
+ // catch and finally already mark the run aborted; logging at ERROR
1453
+ // would spam operator dashboards with non-failures.
1454
+ logger.debug("Subagent prompt aborted", {
1455
+ reason: abortReason ?? "signal",
1456
+ });
1457
+ } else {
1458
+ logger.error("Subagent prompt failed", {
1459
+ error: err instanceof Error ? err.message : String(err),
1460
+ });
1461
+ }
1452
1462
  }
1453
1463
  }
1454
1464
 
package/src/task/index.ts CHANGED
@@ -48,6 +48,7 @@ import { runSubprocess } from "./executor";
48
48
  import { AgentOutputManager } from "./output-manager";
49
49
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
50
50
  import { renderResult, renderCall as renderTaskCall } from "./render";
51
+ import { repairTaskParams } from "./repair-args";
51
52
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
52
53
  import {
53
54
  applyNestedPatches,
@@ -247,7 +248,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
247
248
  }
248
249
 
249
250
  renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
250
- return renderTaskCall(args as TaskParams, options, theme);
251
+ return renderTaskCall(repairTaskParams(args as TaskParams), options, theme);
251
252
  }
252
253
 
253
254
  /** Dynamic description that reflects current disabled-agent settings */
@@ -292,7 +293,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
292
293
  signal?: AbortSignal,
293
294
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
294
295
  ): Promise<AgentToolResult<TaskToolDetails>> {
295
- const params = rawParams as TaskParams;
296
+ const params = repairTaskParams(rawParams as TaskParams);
296
297
  const simpleMode = this.#getTaskSimpleMode();
297
298
  const validationError = validateTaskModeParams(simpleMode, params);
298
299
  if (validationError) {
@@ -762,8 +763,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
762
763
  const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
763
764
  const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
764
765
 
765
- // Share the parent session's local:// root with subagents so they read/write the same scratch space
766
- const localProtocolOptions: LocalProtocolOptions = {
766
+ const localProtocolOptions: LocalProtocolOptions = this.session.localProtocolOptions ?? {
767
767
  getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
768
768
  getSessionId: this.session.getSessionId ?? (() => null),
769
769
  };
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
864
864
  );
865
865
  const promptTemplates = this.session.promptTemplates;
866
866
  const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
867
+ const mcpManager = this.session.mcpManager ?? MCPManager.instance();
867
868
 
868
869
  // Initialize progress for all tasks
869
870
  for (let i = 0; i < tasksWithUniqueIds.length; i++) {
@@ -921,7 +922,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
921
922
  authStorage: this.session.authStorage,
922
923
  modelRegistry: this.session.modelRegistry,
923
924
  settings: this.session.settings,
924
- mcpManager: MCPManager.instance(),
925
+ mcpManager,
925
926
  contextFiles,
926
927
  skills: availableSkills,
927
928
  autoloadSkills: resolvedAutoloadSkills,
@@ -930,7 +931,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
930
931
  localProtocolOptions,
931
932
  parentArtifactManager,
932
933
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
933
- parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
934
+ parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
934
935
  parentTelemetry: this.session.getTelemetry?.(),
935
936
  parentEvalSessionId,
936
937
  });
@@ -978,7 +979,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
978
979
  authStorage: this.session.authStorage,
979
980
  modelRegistry: this.session.modelRegistry,
980
981
  settings: this.session.settings,
981
- mcpManager: MCPManager.instance(),
982
+ mcpManager,
982
983
  contextFiles,
983
984
  skills: availableSkills,
984
985
  autoloadSkills: resolvedAutoloadSkills,
@@ -987,7 +988,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
987
988
  localProtocolOptions,
988
989
  parentArtifactManager,
989
990
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
990
- parentMnemosyneSessionState: this.session.getMnemosyneSessionState?.(),
991
+ parentMnemopiSessionState: this.session.getMnemopiSessionState?.(),
991
992
  parentTelemetry: this.session.getTelemetry?.(),
992
993
  parentEvalSessionId,
993
994
  });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Repair double-encoded JSON string arguments for the task tool.
3
+ *
4
+ * Models occasionally JSON-escape a string value twice when emitting a
5
+ * `task` tool call, so a `context`/`assignment` that should read
6
+ *
7
+ * # Role
8
+ * You are a judge … "describe this" … return —
9
+ *
10
+ * arrives — after the one JSON decode the provider already applied — as the
11
+ * literal text
12
+ *
13
+ * # Role\nYou are a judge … \"describe this\" … return \u2014
14
+ *
15
+ * i.e. every newline, quote, and unicode character is still backslash-escaped.
16
+ * The subagent then receives that garbled prompt, and the call preview renders
17
+ * one long blob with visible `\n` / `\"` / `\uXXXX`.
18
+ *
19
+ * The *whole-arguments* form of this quirk (the entire `arguments` blob is a
20
+ * JSON string) is already auto-corrected by the validator's JSON-string
21
+ * coercion. This module handles the *per-field* form, where the object parses
22
+ * fine but an individual string value is double-encoded — the validator never
23
+ * fires there because a double-encoded string is still a structurally valid
24
+ * string.
25
+ *
26
+ * This is deliberately scoped to the task tool's natural-language fields
27
+ * (`context`, `assignment`, `description`). It is NOT applied to code-bearing
28
+ * tools (write/edit/bash/search), where a backslash or quote is load-bearing
29
+ * and a false-positive unescape would silently corrupt a file or command.
30
+ */
31
+ import type { TaskItem, TaskParams } from "./types";
32
+
33
+ /** A backslash that escapes a structural char — `\"`, `\\`, `\/`, or `\uXXXX`. */
34
+ const STRUCTURAL_ESCAPE = /\\(?:["\\/]|u[0-9a-fA-F]{4})/;
35
+
36
+ /**
37
+ * Whether `value` carries the signature of whole-string double-encoding rather
38
+ * than an incidental escape mention. A lone `\n`/`\t` in an instruction (e.g.
39
+ * "split lines on \n") is far more likely a literal mention than a
40
+ * double-encoded document, so it is left alone; a structural escape (`\"`,
41
+ * `\\`, `\uXXXX`) or two-plus escape sequences indicates a re-escaped payload.
42
+ */
43
+ function hasDoubleEncodeSignature(value: string): boolean {
44
+ if (STRUCTURAL_ESCAPE.test(value)) return true;
45
+ let count = 0;
46
+ for (let i = 0; i < value.length; i++) {
47
+ if (value.charCodeAt(i) === 0x5c /* \ */) {
48
+ count += 1;
49
+ if (count >= 2) return true;
50
+ i += 1; // skip the escaped char so `\\` counts once
51
+ }
52
+ }
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Return the once-unescaped string when `value` is uniformly double-encoded
58
+ * JSON (a well-formed JSON string body that decodes to a different string);
59
+ * otherwise return `value` unchanged.
60
+ *
61
+ * The `JSON.parse(\`"${value}"\`)` round-trip is the safety net: it only
62
+ * succeeds when *every* backslash begins a valid JSON escape and no bare
63
+ * double-quote exists — exactly the signature of double-encoding. Genuine
64
+ * prose with a Windows path (`C:\Users`), a regex (`\d+`), an embedded quote,
65
+ * or a real (already-decoded) newline makes the parse throw, so the value is
66
+ * returned untouched.
67
+ */
68
+ export function repairDoubleEncodedJsonString(value: string): string {
69
+ // Fast path: no backslash → nothing was escaped → the parse can never differ.
70
+ if (!value.includes("\\")) return value;
71
+ if (!hasDoubleEncodeSignature(value)) return value;
72
+ let decoded: unknown;
73
+ try {
74
+ decoded = JSON.parse(`"${value}"`);
75
+ } catch {
76
+ return value;
77
+ }
78
+ return typeof decoded === "string" && decoded !== value ? decoded : value;
79
+ }
80
+
81
+ /** Repair a single (possibly partial) task item's prose fields. */
82
+ function repairTaskItem(task: TaskItem): TaskItem {
83
+ if (task === null || typeof task !== "object") return task;
84
+ const assignment =
85
+ typeof task.assignment === "string" ? repairDoubleEncodedJsonString(task.assignment) : task.assignment;
86
+ const description =
87
+ typeof task.description === "string" ? repairDoubleEncodedJsonString(task.description) : task.description;
88
+ if (assignment === task.assignment && description === task.description) return task;
89
+ return { ...task, assignment, description };
90
+ }
91
+
92
+ /**
93
+ * Repair double-encoded prose in task-tool params (`context` and each task's
94
+ * `assignment`/`description`). Returns the same reference when nothing changed
95
+ * so callers can cheaply skip work. Defensive against partially-streamed args
96
+ * (missing/undefined fields, partial task arrays) so it is safe on the render
97
+ * path as well as on execution.
98
+ */
99
+ export function repairTaskParams(params: TaskParams): TaskParams {
100
+ if (params === null || typeof params !== "object") return params;
101
+
102
+ const context = typeof params.context === "string" ? repairDoubleEncodedJsonString(params.context) : params.context;
103
+
104
+ let tasks = params.tasks;
105
+ if (Array.isArray(params.tasks)) {
106
+ let changed = false;
107
+ const repaired = params.tasks.map(task => {
108
+ const next = repairTaskItem(task);
109
+ if (next !== task) changed = true;
110
+ return next;
111
+ });
112
+ if (changed) tasks = repaired;
113
+ }
114
+
115
+ if (context === params.context && tasks === params.tasks) return params;
116
+ return { ...params, context, tasks };
117
+ }
@@ -108,7 +108,7 @@ export const ONLINE_MEMORY_MODEL_KEY = "online";
108
108
  export const DEFAULT_MEMORY_LOCAL_MODEL_KEY = "qwen3-1.7b";
109
109
 
110
110
  /**
111
- * Local models for Mnemosyne memory tasks (fact extraction + consolidation).
111
+ * Local models for Mnemopi memory tasks (fact extraction + consolidation).
112
112
  * These are larger (1B-1.7B) than the title models: structured extraction and
113
113
  * faithful summarization need more capacity than 3-6 word titles. All q4.
114
114
  * Ranking/recipe rationale lives in docs/local-models.md.
@@ -177,7 +177,7 @@ export const TINY_MEMORY_MODEL_OPTIONS = [
177
177
  value: ONLINE_MEMORY_MODEL_KEY,
178
178
  label: "Online (smol/remote)",
179
179
  description:
180
- "Use the configured Mnemosyne LLM mode (smol or remote); no local model download or on-device inference.",
180
+ "Use the configured Mnemopi LLM mode (smol or remote); no local model download or on-device inference.",
181
181
  },
182
182
  ...TINY_MEMORY_LOCAL_MODELS.map(model => ({
183
183
  value: model.key,