@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 (291) hide show
  1. package/CHANGELOG.md +304 -6
  2. package/dist/cli.js +1015 -881
  3. package/dist/types/async/job-manager.d.ts +15 -0
  4. package/dist/types/autolearn/controller.d.ts +25 -0
  5. package/dist/types/autolearn/managed-skills.d.ts +45 -0
  6. package/dist/types/autoresearch/state.d.ts +1 -1
  7. package/dist/types/autoresearch/types.d.ts +1 -1
  8. package/dist/types/cli/args.d.ts +19 -1
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -1
  11. package/dist/types/cli/setup-model-picker.d.ts +14 -0
  12. package/dist/types/collab/protocol.d.ts +1 -1
  13. package/dist/types/commands/say.d.ts +24 -0
  14. package/dist/types/config/keybindings.d.ts +3 -3
  15. package/dist/types/config/model-registry.d.ts +10 -0
  16. package/dist/types/config/models-config-schema.d.ts +12 -0
  17. package/dist/types/config/models-config.d.ts +8 -2
  18. package/dist/types/config/settings-schema.d.ts +261 -58
  19. package/dist/types/export/html/index.d.ts +2 -1
  20. package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +47 -1
  23. package/dist/types/extensibility/hooks/index.d.ts +2 -1
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
  25. package/dist/types/extensibility/plugins/loader.d.ts +11 -0
  26. package/dist/types/extensibility/shared-events.d.ts +1 -1
  27. package/dist/types/extensibility/skills.d.ts +10 -0
  28. package/dist/types/goals/guided-setup.d.ts +18 -0
  29. package/dist/types/goals/state.d.ts +1 -1
  30. package/dist/types/hindsight/transcript.d.ts +1 -1
  31. package/dist/types/index.d.ts +5 -0
  32. package/dist/types/internal-urls/local-protocol.d.ts +4 -2
  33. package/dist/types/main.d.ts +4 -3
  34. package/dist/types/mcp/startup-events.d.ts +11 -0
  35. package/dist/types/memories/index.d.ts +7 -0
  36. package/dist/types/memory-backend/local-backend.d.ts +4 -3
  37. package/dist/types/mnemopi/config.d.ts +4 -4
  38. package/dist/types/modes/components/agent-hub.d.ts +6 -0
  39. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  40. package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +39 -1
  42. package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
  43. package/dist/types/modes/components/session-selector.d.ts +1 -1
  44. package/dist/types/modes/components/tool-execution.d.ts +26 -16
  45. package/dist/types/modes/components/transcript-container.d.ts +23 -2
  46. package/dist/types/modes/components/tree-selector.d.ts +1 -1
  47. package/dist/types/modes/components/usage-row.d.ts +3 -0
  48. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  49. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  51. package/dist/types/modes/gradient-highlight.d.ts +9 -4
  52. package/dist/types/modes/image-references.d.ts +6 -0
  53. package/dist/types/modes/interactive-mode.d.ts +27 -3
  54. package/dist/types/modes/magic-keywords.d.ts +13 -1
  55. package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
  56. package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
  57. package/dist/types/modes/runtime-init.d.ts +4 -0
  58. package/dist/types/modes/theme/theme.d.ts +13 -2
  59. package/dist/types/modes/types.d.ts +8 -2
  60. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  61. package/dist/types/registry/agent-registry.d.ts +17 -0
  62. package/dist/types/secrets/obfuscator.d.ts +1 -1
  63. package/dist/types/session/agent-session.d.ts +14 -2
  64. package/dist/types/session/indexed-session-storage.d.ts +3 -4
  65. package/dist/types/session/session-context.d.ts +39 -0
  66. package/dist/types/session/session-entries.d.ts +159 -0
  67. package/dist/types/session/session-listing.d.ts +69 -0
  68. package/dist/types/session/session-loader.d.ts +16 -0
  69. package/dist/types/session/session-manager.d.ts +82 -474
  70. package/dist/types/session/session-migrations.d.ts +12 -0
  71. package/dist/types/session/session-paths.d.ts +25 -0
  72. package/dist/types/session/session-persistence.d.ts +8 -0
  73. package/dist/types/session/session-storage.d.ts +11 -12
  74. package/dist/types/session/snapcompact-inline.d.ts +12 -1
  75. package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
  76. package/dist/types/session/tool-choice-queue.d.ts +6 -6
  77. package/dist/types/stt/asr-client.d.ts +90 -0
  78. package/dist/types/stt/asr-protocol.d.ts +97 -0
  79. package/dist/types/stt/asr-worker.d.ts +2 -0
  80. package/dist/types/stt/downloader.d.ts +38 -0
  81. package/dist/types/stt/endpointer.d.ts +59 -0
  82. package/dist/types/stt/index.d.ts +5 -1
  83. package/dist/types/stt/models.d.ts +120 -0
  84. package/dist/types/stt/recorder.d.ts +17 -0
  85. package/dist/types/stt/stt-controller.d.ts +6 -0
  86. package/dist/types/stt/transcriber.d.ts +5 -7
  87. package/dist/types/stt/wav.d.ts +29 -0
  88. package/dist/types/system-prompt.d.ts +4 -0
  89. package/dist/types/task/executor.d.ts +2 -0
  90. package/dist/types/task/index.d.ts +9 -1
  91. package/dist/types/task/types.d.ts +36 -0
  92. package/dist/types/tools/bash.d.ts +2 -2
  93. package/dist/types/tools/eval-render.d.ts +1 -1
  94. package/dist/types/tools/index.d.ts +11 -1
  95. package/dist/types/tools/irc.d.ts +1 -0
  96. package/dist/types/tools/learn.d.ts +51 -0
  97. package/dist/types/tools/manage-skill.d.ts +40 -0
  98. package/dist/types/tools/plan-mode-guard.d.ts +10 -0
  99. package/dist/types/tools/renderers.d.ts +7 -11
  100. package/dist/types/tools/ssh.d.ts +1 -1
  101. package/dist/types/tools/todo.d.ts +1 -1
  102. package/dist/types/tools/tts.d.ts +25 -0
  103. package/dist/types/tools/write.d.ts +1 -1
  104. package/dist/types/tts/downloader.d.ts +20 -0
  105. package/dist/types/tts/index.d.ts +8 -0
  106. package/dist/types/tts/models.d.ts +82 -0
  107. package/dist/types/tts/player.d.ts +32 -0
  108. package/dist/types/tts/runtime.d.ts +6 -0
  109. package/dist/types/tts/streaming-player.d.ts +41 -0
  110. package/dist/types/tts/tts-client.d.ts +93 -0
  111. package/dist/types/tts/tts-protocol.d.ts +95 -0
  112. package/dist/types/tts/tts-worker.d.ts +2 -0
  113. package/dist/types/tts/vocalizer.d.ts +41 -0
  114. package/dist/types/tts/wav.d.ts +8 -0
  115. package/dist/types/utils/tool-choice.d.ts +8 -0
  116. package/dist/types/utils/tools-manager.d.ts +2 -1
  117. package/dist/types/utils/tools-manager.test.d.ts +1 -0
  118. package/dist/types/web/scrapers/github.d.ts +1 -1
  119. package/package.json +15 -14
  120. package/src/async/job-manager.ts +49 -0
  121. package/src/autolearn/controller.ts +139 -0
  122. package/src/autolearn/managed-skills.ts +257 -0
  123. package/src/autoresearch/state.ts +1 -1
  124. package/src/autoresearch/types.ts +1 -1
  125. package/src/cli/args.ts +56 -2
  126. package/src/cli/session-picker.ts +2 -1
  127. package/src/cli/setup-cli.ts +148 -47
  128. package/src/cli/setup-model-picker.ts +43 -0
  129. package/src/cli-commands.ts +1 -0
  130. package/src/cli.ts +45 -13
  131. package/src/collab/host.ts +1 -1
  132. package/src/collab/protocol.ts +1 -1
  133. package/src/commands/say.ts +102 -0
  134. package/src/commands/setup.ts +1 -1
  135. package/src/commit/agentic/tools/analyze-file.ts +3 -0
  136. package/src/config/keybindings.ts +2 -2
  137. package/src/config/model-discovery.ts +11 -5
  138. package/src/config/model-registry.ts +64 -9
  139. package/src/config/models-config-schema.ts +4 -1
  140. package/src/config/models-config.ts +2 -1
  141. package/src/config/settings-schema.ts +248 -32
  142. package/src/config/settings.ts +10 -0
  143. package/src/discovery/builtin.ts +23 -1
  144. package/src/discovery/claude-plugins.ts +44 -5
  145. package/src/discovery/helpers.ts +41 -1
  146. package/src/eval/__tests__/budget-bridge.test.ts +1 -1
  147. package/src/eval/js/shared/prelude.txt +69 -17
  148. package/src/export/html/index.ts +3 -6
  149. package/src/extensibility/extensions/model-api.ts +41 -0
  150. package/src/extensibility/extensions/runner.ts +4 -0
  151. package/src/extensibility/extensions/types.ts +52 -1
  152. package/src/extensibility/extensions/wrapper.ts +41 -5
  153. package/src/extensibility/hooks/index.ts +2 -1
  154. package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
  155. package/src/extensibility/plugins/loader.ts +30 -19
  156. package/src/extensibility/plugins/manager.ts +221 -90
  157. package/src/extensibility/shared-events.ts +1 -1
  158. package/src/extensibility/skills.ts +96 -15
  159. package/src/goals/guided-setup.ts +133 -0
  160. package/src/goals/state.ts +1 -1
  161. package/src/hindsight/transcript.ts +1 -1
  162. package/src/index.ts +5 -0
  163. package/src/internal-urls/docs-index.generated.ts +10 -10
  164. package/src/internal-urls/history-protocol.ts +1 -1
  165. package/src/internal-urls/local-protocol.ts +29 -7
  166. package/src/main.ts +27 -7
  167. package/src/mcp/startup-events.ts +21 -0
  168. package/src/mcp/transports/stdio.ts +2 -1
  169. package/src/memories/index.ts +146 -11
  170. package/src/memory-backend/local-backend.ts +11 -5
  171. package/src/mnemopi/backend.ts +1 -0
  172. package/src/mnemopi/config.ts +26 -10
  173. package/src/modes/acp/acp-agent.ts +3 -5
  174. package/src/modes/components/agent-hub.ts +49 -4
  175. package/src/modes/components/assistant-message.ts +4 -37
  176. package/src/modes/components/compaction-summary-message.ts +125 -26
  177. package/src/modes/components/custom-editor.test.ts +96 -0
  178. package/src/modes/components/custom-editor.ts +164 -8
  179. package/src/modes/components/session-selector.ts +1 -1
  180. package/src/modes/components/settings-defs.ts +7 -0
  181. package/src/modes/components/tool-execution.ts +82 -43
  182. package/src/modes/components/transcript-container.ts +70 -1
  183. package/src/modes/components/tree-selector.ts +1 -1
  184. package/src/modes/components/usage-row.ts +18 -0
  185. package/src/modes/components/user-message.ts +4 -2
  186. package/src/modes/controllers/command-controller.ts +14 -4
  187. package/src/modes/controllers/event-controller.ts +78 -11
  188. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  189. package/src/modes/controllers/input-controller.ts +258 -27
  190. package/src/modes/controllers/selector-controller.ts +12 -2
  191. package/src/modes/gradient-highlight.ts +21 -9
  192. package/src/modes/image-references.ts +20 -0
  193. package/src/modes/interactive-mode.ts +286 -40
  194. package/src/modes/magic-keywords.ts +27 -5
  195. package/src/modes/rpc/rpc-mode.ts +146 -14
  196. package/src/modes/rpc/rpc-subagents.ts +2 -2
  197. package/src/modes/rpc/rpc-types.ts +8 -2
  198. package/src/modes/runtime-init.ts +28 -3
  199. package/src/modes/theme/theme.ts +98 -50
  200. package/src/modes/types.ts +6 -2
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  202. package/src/modes/utils/ui-helpers.ts +34 -6
  203. package/src/priority.json +5 -1
  204. package/src/prompts/agents/task.md +1 -0
  205. package/src/prompts/goals/guided-goal-interview.md +8 -0
  206. package/src/prompts/goals/guided-goal-system.md +12 -0
  207. package/src/prompts/memories/read-path.md +6 -0
  208. package/src/prompts/system/autolearn-guidance-learn.md +1 -0
  209. package/src/prompts/system/autolearn-guidance.md +7 -0
  210. package/src/prompts/system/autolearn-nudge.md +3 -0
  211. package/src/prompts/system/eager-task.md +7 -0
  212. package/src/prompts/system/eager-todo.md +11 -6
  213. package/src/prompts/system/subagent-system-prompt.md +4 -0
  214. package/src/prompts/system/system-prompt.md +10 -5
  215. package/src/prompts/system/title-marker-instruction.md +1 -0
  216. package/src/prompts/system/title-system-marker.md +16 -0
  217. package/src/prompts/tools/job.md +1 -0
  218. package/src/prompts/tools/learn.md +7 -0
  219. package/src/prompts/tools/manage-skill.md +9 -0
  220. package/src/prompts/tools/task.md +3 -0
  221. package/src/registry/agent-registry.ts +30 -0
  222. package/src/sdk.ts +88 -24
  223. package/src/secrets/obfuscator.ts +1 -1
  224. package/src/session/agent-session.ts +209 -87
  225. package/src/session/history-storage.ts +2 -2
  226. package/src/session/indexed-session-storage.ts +7 -17
  227. package/src/session/session-context.ts +352 -0
  228. package/src/session/session-entries.ts +194 -0
  229. package/src/session/session-listing.ts +588 -0
  230. package/src/session/session-loader.ts +106 -0
  231. package/src/session/session-manager.ts +933 -3145
  232. package/src/session/session-migrations.ts +78 -0
  233. package/src/session/session-paths.ts +193 -0
  234. package/src/session/session-persistence.ts +131 -0
  235. package/src/session/session-storage.ts +91 -50
  236. package/src/session/snapcompact-inline.ts +21 -1
  237. package/src/session/snapcompact-savings-journal.ts +113 -0
  238. package/src/session/tool-choice-queue.ts +23 -11
  239. package/src/slash-commands/builtin-registry.ts +25 -3
  240. package/src/stt/asr-client.ts +520 -0
  241. package/src/stt/asr-protocol.ts +65 -0
  242. package/src/stt/asr-worker.ts +790 -0
  243. package/src/stt/downloader.ts +107 -47
  244. package/src/stt/endpointer.ts +259 -0
  245. package/src/stt/index.ts +5 -1
  246. package/src/stt/models.ts +150 -0
  247. package/src/stt/recorder.ts +247 -60
  248. package/src/stt/stt-controller.ts +201 -22
  249. package/src/stt/transcriber.ts +37 -68
  250. package/src/stt/wav.ts +173 -0
  251. package/src/system-prompt.ts +8 -0
  252. package/src/task/agents.ts +1 -2
  253. package/src/task/executor.ts +49 -15
  254. package/src/task/index.ts +60 -6
  255. package/src/task/render.ts +83 -8
  256. package/src/task/types.ts +53 -0
  257. package/src/tools/ask.ts +8 -0
  258. package/src/tools/bash.ts +4 -3
  259. package/src/tools/eval-render.ts +4 -3
  260. package/src/tools/index.ts +40 -4
  261. package/src/tools/irc.ts +10 -2
  262. package/src/tools/job.ts +14 -2
  263. package/src/tools/learn.ts +144 -0
  264. package/src/tools/manage-skill.ts +104 -0
  265. package/src/tools/plan-mode-guard.ts +53 -19
  266. package/src/tools/renderers.ts +7 -11
  267. package/src/tools/ssh.ts +4 -3
  268. package/src/tools/todo.ts +1 -1
  269. package/src/tools/tts.ts +203 -92
  270. package/src/tools/write.ts +18 -2
  271. package/src/tts/downloader.ts +64 -0
  272. package/src/tts/index.ts +8 -0
  273. package/src/tts/models.ts +137 -0
  274. package/src/tts/player.ts +137 -0
  275. package/src/tts/runtime.ts +21 -0
  276. package/src/tts/streaming-player.ts +266 -0
  277. package/src/tts/tts-client.ts +647 -0
  278. package/src/tts/tts-protocol.ts +60 -0
  279. package/src/tts/tts-worker.ts +497 -0
  280. package/src/tts/vocalizer.ts +162 -0
  281. package/src/tts/wav.ts +58 -0
  282. package/src/utils/title-generator.ts +48 -5
  283. package/src/utils/tool-choice.ts +16 -0
  284. package/src/utils/tools-manager.test.ts +25 -0
  285. package/src/utils/tools-manager.ts +19 -1
  286. package/src/web/scrapers/github.ts +96 -0
  287. package/src/web/search/index.ts +13 -0
  288. package/src/web/search/providers/searxng.ts +13 -1
  289. package/dist/types/stt/setup.d.ts +0 -18
  290. package/src/stt/setup.ts +0 -52
  291. package/src/stt/transcribe.py +0 -70
@@ -47,6 +47,12 @@ interface TaskRenderContext {
47
47
  }
48
48
  type TaskRenderOptions = RenderResultOptions & { renderContext?: TaskRenderContext };
49
49
 
50
+ const MAX_NESTED_TASK_RENDER_DEPTH = 8;
51
+
52
+ function renderNestedCycleLine(theme: Theme): string {
53
+ return theme.fg("dim", "… nested task progress already shown");
54
+ }
55
+
50
56
  /**
51
57
  * Get status icon for agent state.
52
58
  * For running status, uses animated spinner if spinnerFrame is provided.
@@ -687,6 +693,8 @@ function renderAgentProgress(
687
693
  theme: Theme,
688
694
  spinnerFrame?: number,
689
695
  frozen = false,
696
+ seenNestedTasks?: WeakSet<object>,
697
+ nestedDepth = 0,
690
698
  ): string[] {
691
699
  const lines: string[] = [];
692
700
 
@@ -859,7 +867,15 @@ function renderAgentProgress(
859
867
  const inflight = progress.inflightTaskDetails;
860
868
  if (completedTaskCalls.length > 0 || inflight) {
861
869
  const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
862
- const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame, frozen);
870
+ const nestedLines = renderNestedTaskTree(
871
+ snapshots,
872
+ expanded,
873
+ theme,
874
+ spinnerFrame,
875
+ frozen,
876
+ seenNestedTasks,
877
+ nestedDepth,
878
+ );
863
879
  for (const line of nestedLines) {
864
880
  lines.push(`${continuePrefix}${line}`);
865
881
  }
@@ -984,6 +1000,8 @@ function renderAgentResult(
984
1000
  continuePrefix: string,
985
1001
  expanded: boolean,
986
1002
  theme: Theme,
1003
+ seenNestedTasks?: WeakSet<object>,
1004
+ nestedDepth = 0,
987
1005
  ): string[] {
988
1006
  const lines: string[] = [];
989
1007
 
@@ -1088,11 +1106,24 @@ function renderAgentResult(
1088
1106
  // Skip review tools - handled above
1089
1107
  if (toolName === "yield" || toolName === "report_finding") continue;
1090
1108
 
1109
+ const isTaskTool = toolName === "task";
1110
+ if (isTaskTool && (dataArray as unknown[]).length > 0) {
1111
+ for (const line of renderNestedTaskResults(
1112
+ dataArray as TaskToolDetails[],
1113
+ expanded,
1114
+ theme,
1115
+ seenNestedTasks,
1116
+ nestedDepth,
1117
+ )) {
1118
+ deferredToolLines.push(`${continuePrefix}${line}`);
1119
+ }
1120
+ continue;
1121
+ }
1122
+
1091
1123
  const handler = subprocessToolRegistry.getHandler(toolName);
1092
1124
  if (handler?.renderFinal && (dataArray as unknown[]).length > 0) {
1093
- const isTaskTool = toolName === "task";
1094
1125
  const component = handler.renderFinal(dataArray as unknown[], theme, expanded);
1095
- const target = isTaskTool ? deferredToolLines : lines;
1126
+ const target = lines;
1096
1127
  if (!isTaskTool) {
1097
1128
  hasCustomRendering = true;
1098
1129
  target.push(`${continuePrefix}${theme.fg("dim", `Tool: ${toolName}`)}`);
@@ -1417,15 +1448,34 @@ function nestedMarkers(isLast: boolean, theme: Theme): { prefix: string; continu
1417
1448
  };
1418
1449
  }
1419
1450
 
1420
- function renderNestedTaskResults(detailsList: TaskToolDetails[], expanded: boolean, theme: Theme): string[] {
1451
+ function renderNestedTaskResults(
1452
+ detailsList: TaskToolDetails[],
1453
+ expanded: boolean,
1454
+ theme: Theme,
1455
+ seen: WeakSet<object> = new WeakSet<object>(),
1456
+ depth = 0,
1457
+ ): string[] {
1421
1458
  const lines: string[] = [];
1422
1459
  for (const details of detailsList) {
1423
- if (!details.results || details.results.length === 0) continue;
1460
+ if (seen.has(details)) {
1461
+ lines.push(renderNestedCycleLine(theme));
1462
+ continue;
1463
+ }
1464
+ if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
1465
+ lines.push(theme.fg("dim", "… nested task depth limit reached"));
1466
+ continue;
1467
+ }
1468
+ seen.add(details);
1469
+ if (!details.results || details.results.length === 0) {
1470
+ seen.delete(details);
1471
+ continue;
1472
+ }
1424
1473
  const ordered = orderResultsForDisplay(details.results);
1425
1474
  ordered.forEach((result, index) => {
1426
1475
  const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1427
- lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1476
+ lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
1428
1477
  });
1478
+ seen.delete(details);
1429
1479
  }
1430
1480
  return lines;
1431
1481
  }
@@ -1441,16 +1491,28 @@ function renderNestedTaskTree(
1441
1491
  theme: Theme,
1442
1492
  spinnerFrame?: number,
1443
1493
  frozen = false,
1494
+ seen: WeakSet<object> = new WeakSet<object>(),
1495
+ depth = 0,
1444
1496
  ): string[] {
1445
1497
  const lines: string[] = [];
1446
1498
  for (const details of detailsList) {
1499
+ if (seen.has(details)) {
1500
+ lines.push(renderNestedCycleLine(theme));
1501
+ continue;
1502
+ }
1503
+ if (depth >= MAX_NESTED_TASK_RENDER_DEPTH) {
1504
+ lines.push(theme.fg("dim", "… nested task depth limit reached"));
1505
+ continue;
1506
+ }
1507
+ seen.add(details);
1447
1508
  const hasResults = Boolean(details.results && details.results.length > 0);
1448
1509
  if (hasResults) {
1449
1510
  const ordered = orderResultsForDisplay(details.results);
1450
1511
  ordered.forEach((result, index) => {
1451
1512
  const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1452
- lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme));
1513
+ lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
1453
1514
  });
1515
+ seen.delete(details);
1454
1516
  continue;
1455
1517
  }
1456
1518
  const inflight = details.progress;
@@ -1458,9 +1520,22 @@ function renderNestedTaskTree(
1458
1520
  const ordered = orderProgressForDisplay(inflight);
1459
1521
  ordered.forEach((prog, index) => {
1460
1522
  const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1461
- lines.push(...renderAgentProgress(prog, prefix, continuePrefix, expanded, theme, spinnerFrame, frozen));
1523
+ lines.push(
1524
+ ...renderAgentProgress(
1525
+ prog,
1526
+ prefix,
1527
+ continuePrefix,
1528
+ expanded,
1529
+ theme,
1530
+ spinnerFrame,
1531
+ frozen,
1532
+ seen,
1533
+ depth + 1,
1534
+ ),
1535
+ );
1462
1536
  });
1463
1537
  }
1538
+ seen.delete(details);
1464
1539
  }
1465
1540
  return lines;
1466
1541
  }
package/src/task/types.ts CHANGED
@@ -74,6 +74,11 @@ export interface SubagentLifecyclePayload {
74
74
  detached?: boolean;
75
75
  }
76
76
 
77
+ /** Display cap for a normalized one-line label (roster line, registry `displayName`, prompt field). */
78
+ export const ROLE_LABEL_MAX = 80;
79
+ /** Schema bound on the raw `role` input, before it is label-normalized at every use site. */
80
+ export const ROLE_INPUT_MAX = 256;
81
+
77
82
  /**
78
83
  * One unit of work. The single-spawn schema is `{ agent, ...taskItemSchema }`;
79
84
  * the batch schema (`task.batch`) is `{ agent, context, tasks: taskItemSchema[] }`.
@@ -83,6 +88,13 @@ export interface SubagentLifecyclePayload {
83
88
  const taskItemShape = {
84
89
  id: z.string().max(48).optional().describe("stable agent id; default generated"),
85
90
  description: z.string().optional().describe("ui label, not seen by subagent"),
91
+ role: z
92
+ .string()
93
+ .max(ROLE_INPUT_MAX)
94
+ .optional()
95
+ .describe(
96
+ "specialist role/expertise this subagent embodies (e.g. 'Rust async-runtime specialist'); shapes its identity and display name",
97
+ ),
86
98
  assignment: z.string().describe("the work; self-contained instructions"),
87
99
  };
88
100
  const isolatedShape = {
@@ -104,6 +116,8 @@ export interface TaskItem {
104
116
  id?: string;
105
117
  /** UI label, not seen by the subagent. */
106
118
  description?: string;
119
+ /** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
120
+ role?: string;
107
121
  /** The work; required by the schema. */
108
122
  assignment?: string;
109
123
  /** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
@@ -149,6 +163,8 @@ export interface TaskParams {
149
163
  id?: string;
150
164
  /** UI label (flat form), not seen by the subagent. */
151
165
  description?: string;
166
+ /** Specialist role/expertise this subagent embodies; shapes its system-prompt identity and display name. */
167
+ role?: string;
152
168
  /** The work (flat form). */
153
169
  assignment?: string;
154
170
  /** Batch form (`task.batch`): one subagent per item. */
@@ -159,6 +175,43 @@ export interface TaskParams {
159
175
  isolated?: boolean;
160
176
  }
161
177
 
178
+ /**
179
+ * One-line, length-capped label safe for a single roster line, a registry
180
+ * `displayName`, or a system-prompt field. Collapses every run of whitespace
181
+ * AND control/format characters — including U+0085 NEL, ESC/ANSI, and the
182
+ * zero-width separators that `\s` misses — to a single space, then caps length.
183
+ * So untrusted text (a spawn `role`, a peer activity gist) can neither break the
184
+ * line, inject prompt structure, nor smuggle terminal escapes. Caps at `max`
185
+ * characters (clamped to >= 1; default `ROLE_LABEL_MAX`), appending an ellipsis when truncated.
186
+ */
187
+ export function oneLineLabel(text: string, max = ROLE_LABEL_MAX): string {
188
+ const oneLine = text.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim();
189
+ const cap = Math.max(1, max);
190
+ // Count/cut by code point, not UTF-16 code unit, so truncation can never
191
+ // split an astral character into a lone surrogate.
192
+ const chars = [...oneLine];
193
+ return chars.length > cap ? `${chars.slice(0, cap - 1).join("")}…` : oneLine;
194
+ }
195
+
196
+ /**
197
+ * Display name for a spawned subagent: its tailored `role` (label-normalized)
198
+ * when one is given, else the agent type's name. Empty/whitespace roles fall
199
+ * back to the agent name.
200
+ */
201
+ export function resolveSubagentDisplayName(role: string | undefined, agentName: string): string {
202
+ const trimmed = role?.trim();
203
+ return trimmed ? oneLineLabel(trimmed) : agentName;
204
+ }
205
+
206
+ /**
207
+ * Whether an agent at `taskDepth` may still spawn children — i.e. it currently
208
+ * holds the `task` tool. Mirrors the task-tool availability gate;
209
+ * `maxRecursionDepth < 0` disables the cap entirely.
210
+ */
211
+ export function canSpawnAtDepth(maxRecursionDepth: number, taskDepth: number): boolean {
212
+ return maxRecursionDepth < 0 || taskDepth < maxRecursionDepth;
213
+ }
214
+
162
215
  /** A code review finding reported by the reviewer agent */
163
216
  export interface ReviewFinding {
164
217
  title: string;
package/src/tools/ask.ts CHANGED
@@ -23,6 +23,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
23
23
  import type { ExtensionUISelectItem } from "../extensibility/extensions";
24
24
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
25
25
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
26
+ import { vocalizer } from "../tts/vocalizer";
26
27
  import { framedBlock, renderStatusLine } from "../tui";
27
28
  import type { ToolSession } from ".";
28
29
  import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
@@ -487,6 +488,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
487
488
  };
488
489
  }
489
490
 
491
+ // Speak the question(s) aloud before surfacing them. Ask vocalizes in every
492
+ // mode — it's the assistant addressing the user — gated only by speech.enabled
493
+ // (the vocalizer re-checks the setting and no-ops when disabled).
494
+ if (this.session.settings.get("speech.enabled")) {
495
+ vocalizer.speak(params.questions.map(q => q.question).join("\n"));
496
+ }
497
+
490
498
  const askQuestion = async (
491
499
  q: AskParams["questions"][number],
492
500
  options?: { previous?: QuestionResult; navigation?: NavigationControls },
package/src/tools/bash.ts CHANGED
@@ -1385,9 +1385,10 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1385
1385
  },
1386
1386
  mergeCallAndResult: true,
1387
1387
  inline: true,
1388
- // Pending preview caps the command to a viewport-sized tail window that
1389
- // shifts while args stream; keep it out of native scrollback mid-run.
1390
- provisionalPendingPreview: true,
1388
+ // Collapsed pending preview caps the command to a viewport-sized tail
1389
+ // window that shifts while args stream. Expanded output is top-anchored
1390
+ // enough for the transcript to commit its settled prefix.
1391
+ provisionalPendingPreview: "collapsed",
1391
1392
  };
1392
1393
  }
1393
1394
 
@@ -754,8 +754,9 @@ export const evalToolRenderer = {
754
754
 
755
755
  mergeCallAndResult: true,
756
756
  inline: true,
757
- // Pending preview shows tail-window code cells; the result render
757
+ // Collapsed pending preview shows tail-window code cells; the result render
758
758
  // interleaves each cell's output under its code, re-laying-out every row
759
- // below the first cell. Keep the preview out of native scrollback mid-run.
760
- provisionalPendingPreview: true,
759
+ // below the first cell. Expanded output is top-anchored enough for the
760
+ // transcript to commit its settled prefix.
761
+ provisionalPendingPreview: "collapsed",
761
762
  };
@@ -22,9 +22,11 @@ import type { AgentRegistry } from "../registry/agent-registry";
22
22
  import type { ArtifactManager } from "../session/artifacts";
23
23
  import type { ClientBridge } from "../session/client-bridge";
24
24
  import type { CustomMessage } from "../session/messages";
25
+ import type { UsageStatistics } from "../session/session-entries";
25
26
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
26
27
  import { TaskTool } from "../task";
27
28
  import type { AgentOutputManager } from "../task/output-manager";
29
+ import { canSpawnAtDepth } from "../task/types";
28
30
  import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
29
31
  import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
30
32
  import type { EventBus } from "../utils/event-bus";
@@ -44,6 +46,8 @@ import { GithubTool } from "./gh";
44
46
  import { InspectImageTool } from "./inspect-image";
45
47
  import { IrcTool, isIrcEnabled } from "./irc";
46
48
  import { JobTool } from "./job";
49
+ import { LearnTool } from "./learn";
50
+ import { ManageSkillTool } from "./manage-skill";
47
51
  import { MemoryEditTool } from "./memory-edit";
48
52
  import { MemoryRecallTool } from "./memory-recall";
49
53
  import { MemoryReflectTool } from "./memory-reflect";
@@ -82,6 +86,8 @@ export * from "./image-gen";
82
86
  export * from "./inspect-image";
83
87
  export * from "./irc";
84
88
  export * from "./job";
89
+ export * from "./learn";
90
+ export * from "./manage-skill";
85
91
  export * from "./memory-edit";
86
92
  export * from "./memory-recall";
87
93
  export * from "./memory-reflect";
@@ -144,6 +150,13 @@ export interface ToolSession {
144
150
  cwd: string;
145
151
  /** Whether UI is available */
146
152
  hasUI: boolean;
153
+ /**
154
+ * Suppress the spawn specialization/coordination advisory appended to `task`
155
+ * results. Set by internal/programmatic callers (e.g. the commit agent's
156
+ * file-analysis fan-out) whose results are consumed by code — not by a model
157
+ * orchestrating further spawns — so the nudge would only be noise.
158
+ */
159
+ suppressSpawnAdvisory?: boolean;
147
160
  /** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
148
161
  fetch?: FetchImpl;
149
162
  /** Skip Python kernel availability check and warmup */
@@ -255,7 +268,7 @@ export interface ToolSession {
255
268
  /** Goal runtime for the active agent session. */
256
269
  getGoalRuntime?: () => GoalRuntime | undefined;
257
270
  /** Get cumulative session usage statistics (input/output tokens, cost). */
258
- getUsageStatistics?: () => import("../session/session-manager").UsageStatistics;
271
+ getUsageStatistics?: () => UsageStatistics;
259
272
  /** Current per-turn token budget {total, spent, hard} for the eval `budget` helper. */
260
273
  getTurnBudget?: () => { total: number | null; spent: number; hard: boolean };
261
274
  /** Record output tokens consumed by an eval-spawned subagent toward the current turn budget. */
@@ -430,6 +443,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
430
443
  retain: MemoryRetainTool.createIf,
431
444
  recall: MemoryRecallTool.createIf,
432
445
  reflect: MemoryReflectTool.createIf,
446
+ learn: LearnTool.createIf,
447
+ manage_skill: ManageSkillTool.createIf,
433
448
  };
434
449
 
435
450
  export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
@@ -510,6 +525,21 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
510
525
  if (!requestedTools.includes(name)) requestedTools.push(name);
511
526
  }
512
527
  }
528
+ // Auto-learn tools are gated by `autolearn.enabled` but, like the memory
529
+ // tools above, must also be force-included into an explicit requestedTools
530
+ // list so a restricted top-level session whose controller/guidance is
531
+ // active still exposes the tools the nudge points at. Gated to top-level
532
+ // (taskDepth 0): the controller only runs there, so a subagent's explicit
533
+ // tool whitelist must never be silently widened with write-capable tools.
534
+ if (session.settings.get("autolearn.enabled") && (session.taskDepth ?? 0) === 0) {
535
+ if (!requestedTools.includes("manage_skill")) requestedTools.push("manage_skill");
536
+ if (
537
+ ["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "") &&
538
+ !requestedTools.includes("learn")
539
+ ) {
540
+ requestedTools.push("learn");
541
+ }
542
+ }
513
543
  }
514
544
  // Resolve effective tool discovery mode.
515
545
  // tools.discoveryMode controls the new modes; mcp.discoveryMode remains a back-compat alias for "mcp-only".
@@ -543,10 +573,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
543
573
  if (name === "retain" || name === "recall" || name === "reflect") {
544
574
  return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
545
575
  }
576
+ if (name === "manage_skill") return session.settings.get("autolearn.enabled") && (session.taskDepth ?? 0) === 0;
577
+ if (name === "learn") {
578
+ return (
579
+ session.settings.get("autolearn.enabled") &&
580
+ (session.taskDepth ?? 0) === 0 &&
581
+ ["hindsight", "mnemopi", "local"].includes(session.settings.get("memory.backend") ?? "")
582
+ );
583
+ }
546
584
  if (name === "task") {
547
- const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
548
- const currentDepth = session.taskDepth ?? 0;
549
- return maxDepth < 0 || currentDepth < maxDepth;
585
+ return canSpawnAtDepth(session.settings.get("task.maxRecursionDepth") ?? 2, session.taskDepth ?? 0);
550
586
  }
551
587
  return true;
552
588
  };
package/src/tools/irc.ts CHANGED
@@ -19,6 +19,7 @@ import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../irc/bus";
19
19
  import type { Theme } from "../modes/theme/theme";
20
20
  import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
21
21
  import type { AgentRegistry } from "../registry/agent-registry";
22
+ import { canSpawnAtDepth } from "../task/types";
22
23
  import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
23
24
  import type { ToolSession } from ".";
24
25
  import {
@@ -41,8 +42,10 @@ const DEFAULT_IRC_TIMEOUT_MS = 120_000;
41
42
  */
42
43
  export function isIrcEnabled(settings: Settings, taskDepth: number): boolean {
43
44
  if (taskDepth > 0) return true;
45
+ // Top-level session: peers exist only if it can still spawn subagents — the
46
+ // same capacity gate the task tool uses, reused here to avoid drift.
44
47
  const maxDepth = settings.get("task.maxRecursionDepth") ?? 2;
45
- return maxDepth < 0 || taskDepth < maxDepth;
48
+ return canSpawnAtDepth(maxDepth, taskDepth);
46
49
  }
47
50
 
48
51
  const ircSchema = z.object({
@@ -66,6 +69,7 @@ interface IrcPeerInfo {
66
69
  parentId?: string;
67
70
  unread: number;
68
71
  lastActivity: number;
72
+ activity?: string;
69
73
  }
70
74
 
71
75
  export interface IrcDetails {
@@ -146,6 +150,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
146
150
  parentId: ref.parentId,
147
151
  unread: bus.unreadCount(ref.id),
148
152
  lastActivity: ref.lastActivity,
153
+ activity: ref.activity,
149
154
  }));
150
155
  const lines: string[] = [];
151
156
  if (peers.length === 0) {
@@ -154,6 +159,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
154
159
  lines.push(`${peers.length} peer(s):`);
155
160
  for (const peer of peers) {
156
161
  const extras = [
162
+ peer.activity || undefined,
157
163
  peer.unread > 0 ? `unread ${peer.unread}` : undefined,
158
164
  peer.parentId ? `parent ${peer.parentId}` : undefined,
159
165
  `active ${formatDuration(Date.now() - peer.lastActivity)} ago`,
@@ -673,7 +679,9 @@ function renderListResult(details: Partial<IrcDetails>, expanded: boolean, theme
673
679
  const kindText = peer.parentId ? `${peer.kind}${theme.sep.dot}of ${peer.parentId}` : peer.kind;
674
680
  const unread = peer.unread > 0 ? ` ${formatBadge(`${peer.unread} unread`, "warning", theme)}` : "";
675
681
  const age = messageAge(peer.lastActivity);
676
- return `${peerStatusBadge(peer.status, theme)} ${theme.bold(replaceTabs(peer.id))} ${theme.fg("dim", kindText)}${unread}${age ? ` ${theme.fg("dim", age)}` : ""}`;
682
+ const activity = peer.activity ? ` ${theme.fg("dim", replaceTabs(peer.activity))}` : "";
683
+ const name = theme.fg("dim", replaceTabs(peer.displayName));
684
+ return `${peerStatusBadge(peer.status, theme)} ${theme.bold(replaceTabs(peer.id))} ${name} ${theme.fg("dim", kindText)}${activity}${unread}${age ? ` ${theme.fg("dim", age)}` : ""}`;
677
685
  },
678
686
  },
679
687
  theme,
package/src/tools/job.ts CHANGED
@@ -184,9 +184,16 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
184
184
  return this.#buildResult(manager, [...cancelledJobs, ...jobsToWatch], cancelOutcomes);
185
185
  }
186
186
 
187
- // Wait until at least one running job finishes, the wait duration elapses, or the call is aborted.
187
+ // Wait until at least one running job finishes, the wait window elapses,
188
+ // or the call is aborted. With `async.pollWaitDuration` set to `smart`,
189
+ // the window adapts: it starts at the ladder floor and climbs as the agent
190
+ // polls in a tight loop, then resets to the floor once the agent steps
191
+ // away from polling (see AsyncJobManager.nextPollWaitMs). Any fixed value
192
+ // waits that exact duration every time.
188
193
  const racePromises: Promise<unknown>[] = runningJobs.map(j => j.promise);
189
- const waitMs = parseWaitDurationMs(this.session.settings.get("async.pollWaitDuration"));
194
+ const pollSetting = this.session.settings.get("async.pollWaitDuration");
195
+ const smartPoll = pollSetting === "smart";
196
+ const waitMs = smartPoll ? manager.nextPollWaitMs(ownerId) : parseWaitDurationMs(pollSetting);
190
197
  const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<void>();
191
198
  const timeoutHandle = setTimeout(() => timeoutResolve(), waitMs);
192
199
  racePromises.push(timeoutPromise);
@@ -232,6 +239,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
232
239
  manager.unwatchJobs(watchedJobIds);
233
240
  clearTimeout(timeoutHandle);
234
241
  if (progressTimer) clearInterval(progressTimer);
242
+ if (smartPoll) {
243
+ // Reset the idle-gap clock: escalate if the agent polls again soon,
244
+ // drop back to the floor once it goes quiet for a while.
245
+ manager.recordPollWaitEnd(ownerId);
246
+ }
235
247
  }
236
248
 
237
249
  return this.#buildResult(manager, allTrackedJobs, cancelOutcomes);
@@ -0,0 +1,144 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import { z } from "zod/v4";
3
+ import { sanitizeSkillName, writeManagedSkill } from "../autolearn/managed-skills";
4
+ import { isNameClaimedByAuthoredSkill } from "../extensibility/skills";
5
+ import { localBackend } from "../memory-backend/local-backend";
6
+ import learnDescription from "../prompts/tools/learn.md" with { type: "text" };
7
+ import type { ToolSession } from ".";
8
+
9
+ const learnSchema = z.object({
10
+ memory: z.string().describe("the durable, self-contained lesson to remember (what, when, why)"),
11
+ context: z.string().describe("optional source context for the lesson").optional(),
12
+ skill: z
13
+ .object({
14
+ action: z.enum(["create", "update"]),
15
+ name: z.string().describe("kebab-case skill name"),
16
+ description: z.string().describe("one-line description of when to use the skill"),
17
+ body: z.string().describe("the SKILL.md body in markdown (no frontmatter)"),
18
+ })
19
+ .describe("also create or enhance a managed skill in the same call")
20
+ .optional(),
21
+ });
22
+
23
+ export type LearnParams = z.infer<typeof learnSchema>;
24
+
25
+ /**
26
+ * Orchestrating "learn" tool: persists a lesson to long-term memory and,
27
+ * given a `skill` payload, mints/enhances a managed skill via the shared
28
+ * `writeManagedSkill` primitive. Gated behind `autolearn.enabled` plus a live
29
+ * memory backend — `hindsight`/`mnemopi` (remote/SQLite) or `local` (the
30
+ * file-based rollout backend, where lessons append to `learned.md`).
31
+ */
32
+ export class LearnTool implements AgentTool<typeof learnSchema> {
33
+ readonly name = "learn";
34
+ readonly approval = (args: unknown) =>
35
+ (args as Partial<LearnParams>).skill || this.session.settings.get("memory.backend") === "local"
36
+ ? "write"
37
+ : "read";
38
+ readonly label = "Learn";
39
+ readonly description = learnDescription;
40
+ readonly parameters = learnSchema;
41
+ readonly strict = true;
42
+ readonly loadMode = "essential" as const;
43
+ readonly summary = "Capture a reusable lesson to memory (and optionally a managed skill)";
44
+
45
+ constructor(private readonly session: ToolSession) {}
46
+
47
+ static createIf(session: ToolSession): LearnTool | null {
48
+ if (!session.settings.get("autolearn.enabled")) return null;
49
+ const backend = session.settings.get("memory.backend");
50
+ if (backend !== "hindsight" && backend !== "mnemopi" && backend !== "local") return null;
51
+ return new LearnTool(session);
52
+ }
53
+
54
+ async execute(_id: string, params: LearnParams): Promise<AgentToolResult> {
55
+ // 1) Persist or queue the lesson to long-term memory (mirrors MemoryRetainTool).
56
+ const backend = this.session.settings.get("memory.backend");
57
+ let memoryMessage = "Lesson stored";
58
+ if (backend === "mnemopi") {
59
+ const state = this.session.getMnemopiSessionState?.();
60
+ if (!state) {
61
+ throw new Error("Mnemopi backend is not initialised for this session.");
62
+ }
63
+ const id = state.rememberScoped(params.memory, {
64
+ source: "coding-agent-learn",
65
+ importance: 0.8,
66
+ metadata: {
67
+ session_id: state.sessionId,
68
+ cwd: state.session.sessionManager.getCwd(),
69
+ context: params.context ?? null,
70
+ tool: "learn",
71
+ },
72
+ scope: "bank",
73
+ extract: true,
74
+ extractEntities: true,
75
+ veracity: "tool",
76
+ memoryType: "fact",
77
+ });
78
+ // rememberScoped returns undefined when the retain failed (closed DB /
79
+ // disk error); mirror mnemopiBackend.save and fail loudly rather than
80
+ // reporting (and minting a skill for) a lesson that was silently dropped.
81
+ if (!id) {
82
+ throw new Error("Mnemopi did not store the lesson (no memory id returned).");
83
+ }
84
+ } else if (backend === "local") {
85
+ const result = await localBackend.save?.(
86
+ { agentDir: this.session.settings.getAgentDir(), cwd: this.session.settings.getCwd() },
87
+ { content: params.memory, context: params.context, source: "coding-agent-learn", importance: 0.8 },
88
+ );
89
+ if (!result || result.stored === 0) {
90
+ throw new Error("Lesson was empty after sanitization; nothing stored.");
91
+ }
92
+ } else {
93
+ const state = this.session.getHindsightSessionState?.();
94
+ if (!state) {
95
+ throw new Error("Hindsight backend is not initialised for this session.");
96
+ }
97
+ state.enqueueRetain(params.memory, params.context);
98
+ memoryMessage = "Lesson queued for retention";
99
+ }
100
+
101
+ // 2) Optionally mint/enhance a managed skill. A failure here is surfaced
102
+ // as a partial outcome — the lesson is already stored or queued.
103
+ if (params.skill) {
104
+ // A managed skill resolves below any authored skill of the same name, so
105
+ // minting one under a claimed name writes a file that never surfaces. The
106
+ // lesson is already stored/queued; refuse the skill rather than report a
107
+ // false "Created" (mirrors ManageSkillTool).
108
+ let safeSkillName: string | undefined;
109
+ try {
110
+ safeSkillName = sanitizeSkillName(params.skill.name);
111
+ } catch {
112
+ safeSkillName = undefined;
113
+ }
114
+ if (params.skill.action === "create" && safeSkillName && isNameClaimedByAuthoredSkill(safeSkillName)) {
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: `${memoryMessage}. Did not create managed skill "${params.skill.name}": an authored skill of that name already exists, and managed skills cannot override authored ones. Choose a different name.`,
120
+ },
121
+ ],
122
+ isError: true,
123
+ details: { skill: null, shadowed: true },
124
+ };
125
+ }
126
+ try {
127
+ await writeManagedSkill(params.skill);
128
+ } catch (err) {
129
+ const reason = err instanceof Error ? err.message : String(err);
130
+ throw new Error(`${memoryMessage}, but the managed skill could not be written: ${reason}`);
131
+ }
132
+ const verb = params.skill.action === "create" ? "Created" : "Updated";
133
+ return {
134
+ content: [{ type: "text", text: `${memoryMessage}. ${verb} managed skill "${params.skill.name}".` }],
135
+ details: { skill: params.skill.name },
136
+ };
137
+ }
138
+
139
+ return {
140
+ content: [{ type: "text", text: `${memoryMessage}.` }],
141
+ details: { skill: null },
142
+ };
143
+ }
144
+ }