@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -84,6 +84,8 @@ export interface ModelChangeEntry extends SessionEntryBase {
84
84
  model: string;
85
85
  /** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
86
86
  role?: string;
87
+ /** True when this transition selected a retry-fallback model rather than the configured model. */
88
+ resolvedModelIsFallback?: boolean;
87
89
  }
88
90
 
89
91
  export interface ServiceTierChangeEntry extends SessionEntryBase {
@@ -207,6 +209,14 @@ export interface SessionInitEntry extends SessionEntryBase {
207
209
  task: string;
208
210
  /** Tools available to the agent */
209
211
  tools: string[];
212
+ /** Agent definition name (for example `scout` or `reviewer`). */
213
+ agent?: string;
214
+ /** Semantic model role declared by the agent, retained even after concrete model resolution. */
215
+ modelRole?: string;
216
+ /** Initially resolved provider/model selector for historical display. */
217
+ resolvedModel?: string;
218
+ /** Whether the agent definition is read-only, allowing an exact zero-LoC attribution. */
219
+ readOnly?: boolean;
210
220
  /** Output schema if structured output was requested. */
211
221
  outputSchema?: unknown;
212
222
  /** Enforcement policy recorded with the output schema for faithful revival. */
@@ -2,7 +2,7 @@ import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
  import type { Message } from "@oh-my-pi/pi-ai";
4
4
  import { getAgentDir as getDefaultAgentDir, logger, parseJsonlLenient, toError } from "@oh-my-pi/pi-utils";
5
- import { LRUCache } from "lru-cache/raw";
5
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
6
6
  import { computeDefaultSessionDir } from "./session-paths";
7
7
  import { FileSessionStorage, type SessionStorage, type SessionStorageStat } from "./session-storage";
8
8
 
@@ -21,6 +21,19 @@ import {
21
21
  } from "./session-title-slot";
22
22
 
23
23
  const STREAM_LOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
24
+ const STREAM_YIELD_BYTES = 1 * 1024 * 1024;
25
+ const STREAM_YIELD_ENTRIES = 8_192;
26
+
27
+ export interface VisitEntriesFromFileStreamOptions {
28
+ /** Stop after the visitor returns `false`. */
29
+ shouldContinue?: () => boolean;
30
+ /** Stop after this many valid or malformed JSONL records have been consumed. */
31
+ maxRecords?: number;
32
+ /** Yield to the macrotask queue after this many bytes have been consumed. */
33
+ yieldEveryBytes?: number;
34
+ /** Yield to the macrotask queue after this many entries have been visited. */
35
+ yieldEveryEntries?: number;
36
+ }
24
37
 
25
38
  function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
26
39
  const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
@@ -59,11 +72,19 @@ export function parseSessionContent(content: string): {
59
72
  /** Parse session JSONL and visit each entry without retaining prior entries. */
60
73
  export async function visitEntriesFromFileStream(
61
74
  filePath: string,
62
- visit: (entry: FileEntry) => void,
75
+ visit: (entry: FileEntry) => void | boolean,
76
+ options: VisitEntriesFromFileStreamOptions = {},
63
77
  ): Promise<SessionTitleUpdate | undefined> {
64
78
  let titleSlot: SessionTitleUpdate | undefined;
65
79
  let sawFirstLine = false;
80
+ let bytesSinceYield = 0;
81
+ let entriesSinceYield = 0;
82
+ let recordsSeen = 0;
83
+ const maxRecords = Math.max(0, options.maxRecords ?? Number.POSITIVE_INFINITY);
84
+ let stopped = false;
66
85
  let visitorThrew = false;
86
+ const yieldEveryBytes = Math.max(0, options.yieldEveryBytes ?? STREAM_YIELD_BYTES);
87
+ const yieldEveryEntries = Math.max(0, options.yieldEveryEntries ?? STREAM_YIELD_ENTRIES);
67
88
  // Byte buffer (NOT a decoded string): multibyte UTF-8 sequences that straddle
68
89
  // a stream-chunk boundary stay intact, and Bun.JSONL.parseChunk accepts typed
69
90
  // arrays directly. Only the unconsumed remainder is held (≤ one record + a
@@ -72,22 +93,62 @@ export async function visitEntriesFromFileStream(
72
93
  let buffer: Uint8Array = new Uint8Array();
73
94
  const decoder = new TextDecoder();
74
95
 
75
- const drain = () => {
76
- while (buffer.length > 0) {
96
+ const yieldToMacrotask = async (): Promise<void> => {
97
+ if (yieldEveryBytes === 0 && yieldEveryEntries === 0) return;
98
+ const bytesReady = yieldEveryBytes === 0 || bytesSinceYield < yieldEveryBytes;
99
+ const entriesReady = yieldEveryEntries === 0 || entriesSinceYield < yieldEveryEntries;
100
+ if (bytesReady && entriesReady) {
101
+ return;
102
+ }
103
+ bytesSinceYield = 0;
104
+ entriesSinceYield = 0;
105
+ await Bun.sleep(0);
106
+ };
107
+
108
+ const drain = async (): Promise<void> => {
109
+ while (buffer.length > 0 && !stopped) {
110
+ if (recordsSeen >= maxRecords) {
111
+ stopped = true;
112
+ break;
113
+ }
77
114
  const { values, error, read, done } = Bun.JSONL.parseChunk(buffer);
78
115
  for (const value of values) {
116
+ if (recordsSeen >= maxRecords) {
117
+ stopped = true;
118
+ break;
119
+ }
120
+ if (options.shouldContinue && !options.shouldContinue()) {
121
+ stopped = true;
122
+ break;
123
+ }
79
124
  try {
80
- visit(value as FileEntry);
125
+ if (visit(value as FileEntry) === false) {
126
+ stopped = true;
127
+ break;
128
+ }
129
+ recordsSeen++;
130
+ entriesSinceYield++;
131
+ if (recordsSeen >= maxRecords) {
132
+ stopped = true;
133
+ break;
134
+ }
81
135
  } catch (err) {
82
136
  visitorThrew = true;
83
137
  throw err;
84
138
  }
139
+ await yieldToMacrotask();
85
140
  }
141
+ if (stopped) break;
86
142
  if (error) {
87
143
  // Malformed record: skip past the next newline and continue.
88
144
  const nextNewline = buffer.indexOf(0x0a, read);
89
145
  if (nextNewline === -1) break; // rest of the bad line not yet received
146
+ recordsSeen++;
90
147
  buffer = buffer.subarray(nextNewline + 1);
148
+ if (recordsSeen >= maxRecords) {
149
+ stopped = true;
150
+ break;
151
+ }
91
152
  continue;
92
153
  }
93
154
  if (read === 0) break; // incomplete record awaiting more data
@@ -101,6 +162,8 @@ export async function visitEntriesFromFileStream(
101
162
 
102
163
  try {
103
164
  for await (const chunk of Bun.file(filePath).stream()) {
165
+ if (stopped) break;
166
+ bytesSinceYield += chunk.byteLength;
104
167
  buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
105
168
  // The optional fixed-width title slot is a physical first line that is
106
169
  // NOT JSON; peel it before the parser would (correctly) reject it. The
@@ -121,14 +184,15 @@ export async function visitEntriesFromFileStream(
121
184
  }
122
185
  }
123
186
  }
124
- drain();
187
+ await drain();
188
+ await yieldToMacrotask();
125
189
  }
126
190
  // A trailing record without a final newline: terminate it so the parser
127
191
  // can complete it (readline yielded it; parseChunk needs the delimiter).
128
- if (buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
192
+ if (!stopped && buffer.length > 0 && buffer[buffer.length - 1] !== 0x0a) {
129
193
  buffer = Buffer.concat([buffer, new Uint8Array([0x0a])]);
194
+ await drain();
130
195
  }
131
- drain();
132
196
  } catch (err) {
133
197
  if (visitorThrew) throw err;
134
198
  if (isEnoent(err)) return undefined;
@@ -144,7 +208,9 @@ export async function loadEntriesFromFileStream(filePath: string): Promise<{
144
208
  titleSlot: SessionTitleUpdate | undefined;
145
209
  }> {
146
210
  const entries: FileEntry[] = [];
147
- const titleSlot = await visitEntriesFromFileStream(filePath, entry => entries.push(entry));
211
+ const titleSlot = await visitEntriesFromFileStream(filePath, entry => {
212
+ entries.push(entry);
213
+ });
148
214
  return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
149
215
  }
150
216
 
@@ -2047,9 +2047,16 @@ export class SessionManager {
2047
2047
  * Append a model change as a child of the current leaf, then advance the leaf.
2048
2048
  * @param model Model in "provider/modelId" format
2049
2049
  * @param role Optional role (default: "default")
2050
+ * @param resolvedModelIsFallback Whether this transition selected a retry-fallback model
2050
2051
  */
2051
- appendModelChange(model: string, role?: string): string {
2052
- const entry: ModelChangeEntry = { type: "model_change", ...this.#freshEntryFields(), model, role };
2052
+ appendModelChange(model: string, role?: string, resolvedModelIsFallback = false): string {
2053
+ const entry: ModelChangeEntry = {
2054
+ type: "model_change",
2055
+ ...this.#freshEntryFields(),
2056
+ model,
2057
+ role,
2058
+ resolvedModelIsFallback,
2059
+ };
2053
2060
  this.#recordEntry(entry);
2054
2061
  return entry.id;
2055
2062
  }
@@ -2058,6 +2065,10 @@ export class SessionManager {
2058
2065
  systemPrompt: string;
2059
2066
  task: string;
2060
2067
  tools: string[];
2068
+ agent?: string;
2069
+ modelRole?: string;
2070
+ resolvedModel?: string;
2071
+ readOnly?: boolean;
2061
2072
  outputSchema?: unknown;
2062
2073
  outputSchemaMode?: StructuredSubagentSchemaMode;
2063
2074
  restrictToolNames?: boolean;
@@ -2547,6 +2558,9 @@ export class SessionManager {
2547
2558
  systemPrompt: string;
2548
2559
  task: string;
2549
2560
  tools: string[];
2561
+ agent?: string;
2562
+ modelRole?: string;
2563
+ resolvedModel?: string;
2550
2564
  outputSchema?: unknown;
2551
2565
  outputSchemaMode?: StructuredSubagentSchemaMode;
2552
2566
  restrictToolNames?: boolean;
@@ -2567,6 +2581,9 @@ export class SessionManager {
2567
2581
  systemPrompt: string;
2568
2582
  task: string;
2569
2583
  tools: string[];
2584
+ agent?: string;
2585
+ modelRole?: string;
2586
+ resolvedModel?: string;
2570
2587
  outputSchema?: unknown;
2571
2588
  outputSchemaMode?: StructuredSubagentSchemaMode;
2572
2589
  restrictToolNames?: boolean;
@@ -2580,6 +2597,9 @@ export class SessionManager {
2580
2597
  systemPrompt: entry.systemPrompt,
2581
2598
  task: entry.task,
2582
2599
  tools: entry.tools,
2600
+ agent: entry.agent,
2601
+ modelRole: entry.modelRole,
2602
+ resolvedModel: entry.resolvedModel,
2583
2603
  outputSchema: entry.outputSchema,
2584
2604
  outputSchemaMode: entry.outputSchemaMode,
2585
2605
  restrictToolNames: entry.restrictToolNames,
@@ -2,17 +2,10 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getTerminalId } from "@oh-my-pi/pi-tui";
5
- import {
6
- getSessionsDir,
7
- getTerminalSessionsDir,
8
- isEnoent,
9
- isRecord,
10
- logger,
11
- resolveEquivalentPath,
12
- } from "@oh-my-pi/pi-utils";
5
+ import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
13
6
  import type { SessionStorage } from "./session-storage";
14
7
 
15
- const SESSION_HEADER_PREFIX_BYTES = 4096;
8
+ const migratedSessionRoots = new Set<string>();
16
9
 
17
10
  /**
18
11
  * Merge or rename a legacy session directory into its canonical target.
@@ -24,11 +17,13 @@ function migrateSessionDirPath(oldPath: string, newPath: string): void {
24
17
  for (const file of fs.readdirSync(oldPath)) {
25
18
  const src = path.join(oldPath, file);
26
19
  const dst = path.join(newPath, file);
27
- if (!fs.existsSync(dst)) {
28
- fs.renameSync(src, dst);
20
+ if (fs.existsSync(dst)) {
21
+ logger.warn("Session directory migration collision; preserving legacy entry", { src, dst });
22
+ continue;
29
23
  }
24
+ fs.renameSync(src, dst);
30
25
  }
31
- fs.rmSync(oldPath, { recursive: true, force: true });
26
+ fs.rmdirSync(oldPath);
32
27
  return;
33
28
  }
34
29
  if (existing) {
@@ -42,14 +37,31 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
42
37
  return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
43
38
  }
44
39
 
45
- function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
40
+ function encodeRelativeSessionDirName(prefix: string, relative: string): string {
46
41
  const encoded = relative.replace(/[/\\:]/g, "-");
47
42
  return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
48
43
  }
49
44
 
45
+ /**
46
+ * Reconstruct the short-lived hashed session dir name used by 17.2.5-17.2.8
47
+ * (reverted PR #7397): `<scope>-<readable>-<sha256hex>` keyed by the canonical
48
+ * cwd. Kept only so {@link migrateHashedSessionDir} can recover sessions
49
+ * stranded when 17.2.9 restored the legacy names without a reverse migration.
50
+ */
51
+ function encodeHashedSessionDirName(canonicalCwd: string, scope: "home" | "tmp" | "abs"): string {
52
+ const normalized = canonicalCwd.replaceAll("\\", "/");
53
+ const readable = path
54
+ .basename(canonicalCwd)
55
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
56
+ .replace(/^-+|-+$/g, "")
57
+ .slice(-80);
58
+ const digest = Bun.SHA256.hash(normalized, "hex");
59
+ return `${scope}-${readable || "project"}-${digest}`;
60
+ }
61
+
50
62
  function getDefaultSessionDirName(cwd: string): {
51
63
  encodedDirName: string;
52
- legacyRelativeDirName: string | undefined;
64
+ hashedDirName: string;
53
65
  resolvedCwd: string;
54
66
  } {
55
67
  const resolvedCwd = path.resolve(cwd);
@@ -60,111 +72,106 @@ function getDefaultSessionDirName(cwd: string): {
60
72
  const canonicalTempRoot = resolveEquivalentPath(tempRoot);
61
73
  const homeRelative = path.relative(canonicalHome, canonicalCwd);
62
74
  const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
63
-
75
+ let encodedDirName: string;
64
76
  let scope: "home" | "tmp" | "abs";
65
- let legacyRelativeDirName: string | undefined;
66
77
  if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
78
+ encodedDirName = encodeRelativeSessionDirName("-", homeRelative);
67
79
  scope = "home";
68
- legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
69
80
  } else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
81
+ encodedDirName = encodeRelativeSessionDirName("-tmp", tempRelative);
70
82
  scope = "tmp";
71
- legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
72
83
  } else {
84
+ encodedDirName = encodeLegacyAbsoluteSessionDirName(canonicalCwd);
73
85
  scope = "abs";
74
86
  }
75
-
76
- const normalized = canonicalCwd.replaceAll("\\", "/");
77
- const readable = path
78
- .basename(canonicalCwd)
79
- .replace(/[^a-zA-Z0-9._-]+/g, "-")
80
- .replace(/^-+|-+$/g, "")
81
- .slice(-80);
82
- const digest = Bun.SHA256.hash(normalized, "hex");
83
- const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
84
- return { encodedDirName, legacyRelativeDirName, resolvedCwd };
87
+ return { encodedDirName, hashedDirName: encodeHashedSessionDirName(canonicalCwd, scope), resolvedCwd };
85
88
  }
86
89
 
87
- function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
88
- let descriptor: number | undefined;
90
+ /**
91
+ * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
92
+ * Runs once per sessions root on first access, best-effort.
93
+ */
94
+ function migrateHomeSessionDirs(sessionsRoot: string): void {
95
+ if (migratedSessionRoots.has(sessionsRoot)) return;
96
+ migratedSessionRoots.add(sessionsRoot);
97
+
98
+ const home = os.homedir();
99
+ const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
100
+ const oldPrefix = `--${homeEncoded}-`;
101
+ const oldExact = `--${homeEncoded}--`;
102
+
103
+ let entries: string[];
89
104
  try {
90
- descriptor = fs.openSync(sessionFile, "r");
91
- const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
92
- const prefix = buffer.toString("utf8", 0, bytesRead);
93
- for (const line of prefix.split(/\r?\n/, 3)) {
94
- try {
95
- const record: unknown = JSON.parse(line);
96
- if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
97
- return record.cwd;
98
- }
99
- } catch {
100
- // Ignore title slots or truncated/corrupt headers.
101
- }
102
- }
105
+ entries = fs.readdirSync(sessionsRoot);
103
106
  } catch {
104
- // Best-effort migration leaves unreadable sessions in the current bucket.
105
- } finally {
106
- if (descriptor !== undefined) fs.closeSync(descriptor);
107
+ return;
107
108
  }
108
- return undefined;
109
- }
110
109
 
111
- function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
112
- fs.mkdirSync(targetDir, { recursive: true });
113
- const artifactsName = path.basename(sessionName, ".jsonl");
114
110
  for (const entry of entries) {
115
- if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
116
- const source = path.join(sourceDir, entry);
117
- if (!fs.existsSync(source)) continue;
118
- const target = path.join(targetDir, entry);
119
- const existing = fs.statSync(target, { throwIfNoEntry: false });
120
- if (!existing) {
121
- fs.renameSync(source, target);
122
- } else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
123
- migrateSessionDirPath(source, target);
111
+ let remainder: string;
112
+ if (entry === oldExact) {
113
+ remainder = "";
114
+ } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
115
+ remainder = entry.slice(oldPrefix.length, -2);
124
116
  } else {
125
- fs.rmSync(source, { recursive: true, force: true });
117
+ continue;
118
+ }
119
+
120
+ const newName = remainder ? `-${remainder}` : "-";
121
+ const oldPath = path.join(sessionsRoot, entry);
122
+ const newPath = path.join(sessionsRoot, newName);
123
+
124
+ try {
125
+ migrateSessionDirPath(oldPath, newPath);
126
+ } catch (error) {
127
+ logger.warn("Failed to migrate legacy home session directory", {
128
+ oldPath,
129
+ newPath,
130
+ error: String(error),
131
+ });
126
132
  }
127
133
  }
128
134
  }
129
135
 
130
- function rerouteCollidingSessions(
131
- cwd: string,
132
- legacyDir: string,
133
- sessionsRoot: string,
134
- kind: "relative" | "absolute",
135
- ): void {
136
- const entries = fs.readdirSync(legacyDir);
137
- const currentCwd = resolveEquivalentPath(path.resolve(cwd));
138
- const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
139
- for (const entry of entries) {
140
- if (!entry.endsWith(".jsonl")) continue;
141
- const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
142
- if (!recordedCwd) continue;
143
- const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
144
- if (
145
- recordedCanonical === currentCwd ||
146
- !fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
147
- ) {
148
- continue;
149
- }
150
- const recordedNames = getDefaultSessionDirName(recordedCanonical);
151
- const recordedLegacyName =
152
- kind === "relative"
153
- ? recordedNames.legacyRelativeDirName
154
- : encodeLegacyAbsoluteSessionDirName(recordedCanonical);
155
- if (recordedLegacyName !== path.basename(legacyDir)) continue;
156
- moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
136
+ function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
137
+ const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
138
+ if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
139
+
140
+ try {
141
+ migrateSessionDirPath(legacyDir, sessionDir);
142
+ } catch (error) {
143
+ logger.warn("Failed to migrate legacy session directory", {
144
+ oldPath: legacyDir,
145
+ newPath: sessionDir,
146
+ error: String(error),
147
+ });
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Migrate a 17.2.5-17.2.8 hashed session dir back into its legacy path-based
153
+ * directory. The 17.2.9 revert restored the legacy names but dropped migration,
154
+ * stranding sessions written under the hashed scheme (issue #7677). Best-effort.
155
+ */
156
+ function migrateHashedSessionDir(hashedDirName: string, sessionDir: string, sessionsRoot: string): void {
157
+ const hashedDir = path.join(sessionsRoot, hashedDirName);
158
+ if (hashedDir === sessionDir || !fs.existsSync(hashedDir)) return;
159
+
160
+ try {
161
+ migrateSessionDirPath(hashedDir, sessionDir);
162
+ } catch (error) {
163
+ logger.warn("Failed to migrate hashed session directory", {
164
+ oldPath: hashedDir,
165
+ newPath: sessionDir,
166
+ error: String(error),
167
+ });
157
168
  }
158
169
  }
159
170
 
160
171
  export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
161
172
  const currentDirName = path.basename(sessionDir);
162
- const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
163
- if (
164
- currentDirName !== encodedDirName &&
165
- currentDirName !== legacyRelativeDirName &&
166
- currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
167
- ) {
173
+ const { encodedDirName } = getDefaultSessionDirName(cwd);
174
+ if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
168
175
  return undefined;
169
176
  }
170
177
  return path.dirname(sessionDir);
@@ -180,23 +187,11 @@ export function computeDefaultSessionDir(
180
187
  storage: SessionStorage,
181
188
  sessionsRoot: string = getSessionsDir(),
182
189
  ): string {
183
- const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
190
+ const { encodedDirName, hashedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
191
+ migrateHomeSessionDirs(sessionsRoot);
184
192
  const sessionDir = path.join(sessionsRoot, encodedDirName);
185
- const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
186
- { kind: "relative", name: legacyRelativeDirName },
187
- { kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
188
- ];
189
- for (const legacy of legacyDirs) {
190
- if (!legacy.name) continue;
191
- const legacyDir = path.join(sessionsRoot, legacy.name);
192
- if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
193
- try {
194
- rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
195
- migrateSessionDirPath(legacyDir, sessionDir);
196
- } catch {
197
- // Best effort
198
- }
199
- }
193
+ migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
194
+ migrateHashedSessionDir(hashedDirName, sessionDir, sessionsRoot);
200
195
  storage.ensureDirSync(sessionDir);
201
196
  return sessionDir;
202
197
  }
@@ -7,7 +7,7 @@ import { formatModelString } from "../config/model-resolver";
7
7
  import type { Settings, SkillsSettings } from "../config/settings";
8
8
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
9
9
  import { CustomToolAdapter } from "../extensibility/custom-tools/wrapper";
10
- import type { ExtensionRunner } from "../extensibility/extensions";
10
+ import type { ExtensionRunner, SourceInfo, ToolInfo } from "../extensibility/extensions";
11
11
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
12
12
  import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "../extensibility/skills";
13
13
  import { type LocalProtocolOptions, XD_URL_PREFIX } from "../internal-urls";
@@ -333,6 +333,33 @@ export class SessionTools {
333
333
  return Array.from(this.#toolRegistry.keys());
334
334
  }
335
335
 
336
+ /**
337
+ * Full metadata for every registered tool, including source provenance.
338
+ *
339
+ * Backs the `getAllTools()` ExtensionAPI method. Returns {@link ToolInfo}
340
+ * objects (not bare names) so extensions authored against upstream
341
+ * `@earendil-works/pi-coding-agent` — which promises `ToolInfo[]` — can read
342
+ * `sourceInfo.source` unchanged.
343
+ */
344
+ getAllToolInfos(): ToolInfo[] {
345
+ return Array.from(this.#toolRegistry, ([name, tool]) => {
346
+ const source = this.#builtInToolNames.has(name)
347
+ ? "builtin"
348
+ : isMCPToolName(name)
349
+ ? "mcp"
350
+ : this.#rpcHostToolNames.has(name)
351
+ ? "sdk"
352
+ : "extension";
353
+ const sourceInfo: SourceInfo = {
354
+ path: `<${source}:${name}>`,
355
+ source,
356
+ scope: "temporary",
357
+ origin: "top-level",
358
+ };
359
+ return { name, description: tool.description, parameters: tool.parameters, sourceInfo };
360
+ });
361
+ }
362
+
336
363
  #wrapRuntimeTool(tool: AgentTool): AgentTool {
337
364
  const wrapped = wrapToolWithMetaNotice(tool);
338
365
  const extensionRunner = this.#host.extensionRunner();
@@ -34,9 +34,13 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
34
34
  openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
35
35
  const antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
36
36
  const textVerbosity =
37
- model.api === "openai-codex-responses" || model.api === "openai-responses"
38
- ? settings.get("textVerbosity")
39
- : undefined;
37
+ model.api === "openai-codex-responses"
38
+ ? settings.isConfigured("textVerbosity")
39
+ ? settings.get("textVerbosity")
40
+ : undefined
41
+ : model.api === "openai-responses"
42
+ ? settings.get("textVerbosity")
43
+ : undefined;
40
44
  const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds"));
41
45
  const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds"));
42
46
  // Server-side fallback (opt-in): when the user enables it AND the