@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -187,6 +187,17 @@ export class HindsightRetainQueue {
187
187
  }
188
188
  }
189
189
 
190
+ /** Rolling hash of messages[0, count) for retention-cache validation (see #lastRetainedPrefixKey). */
191
+ function retentionPrefixKey(messages: HindsightMessage[], count: number): string {
192
+ let key = "";
193
+ for (let i = 0; i < count; i++) {
194
+ const m = messages[i];
195
+ if (m === undefined) break;
196
+ key = Bun.hash(`${key}\u0000${m.role}\u0000${m.content}`).toString(36);
197
+ }
198
+ return key;
199
+ }
200
+
190
201
  /** Per-session Hindsight runtime state owned by its AgentSession. */
191
202
  export class HindsightSessionState {
192
203
  /** Session id used for retain-queue metadata. */
@@ -202,6 +213,17 @@ export class HindsightSessionState {
202
213
  session: AgentSession;
203
214
  banksSet: Set<string>;
204
215
  lastRetainedTurn: number;
216
+ #lastRetainedMessageIndex: number = 0;
217
+ #cachedTranscript: string = "";
218
+ // Rolling hash of ALL messages in [0, #lastRetainedMessageIndex) at cache
219
+ // time. The incremental full-session cache assumes the branch is append-only;
220
+ // a rewind, branch switch, compaction, or in-place edit rewrites the prefix
221
+ // without changing the session id. Re-hashing the current prefix at use time
222
+ // makes the cache self-healing: on ANY prefix change (not just the boundary
223
+ // message) we rebuild the full transcript instead of retaining stale content
224
+ // or silently retaining nothing forever. Hashing is orders of magnitude
225
+ // cheaper than the re-formatting this cache avoids.
226
+ #lastRetainedPrefixKey: string = "";
205
227
  hasRecalledForFirstTurn: boolean;
206
228
  lastRecallSnippet?: string;
207
229
  /** Cached `<mental_models>` block injected into developer instructions. */
@@ -236,6 +258,9 @@ export class HindsightSessionState {
236
258
  this.session = options.session;
237
259
  this.banksSet = options.banksSet;
238
260
  this.lastRetainedTurn = options.lastRetainedTurn ?? 0;
261
+ this.#lastRetainedMessageIndex = 0;
262
+ this.#cachedTranscript = "";
263
+ this.#lastRetainedPrefixKey = "";
239
264
  this.hasRecalledForFirstTurn = options.hasRecalledForFirstTurn ?? false;
240
265
  this.aliasOf = options.aliasOf;
241
266
  this.retainQueue = new HindsightRetainQueue(this);
@@ -243,12 +268,18 @@ export class HindsightSessionState {
243
268
 
244
269
  setSessionId(sessionId: string): void {
245
270
  this.sessionId = sessionId;
271
+ this.#lastRetainedMessageIndex = 0;
272
+ this.#cachedTranscript = "";
273
+ this.#lastRetainedPrefixKey = "";
246
274
  }
247
275
 
248
276
  resetConversationTracking(): void {
249
277
  this.lastRetainedTurn = 0;
250
278
  this.hasRecalledForFirstTurn = false;
251
279
  this.lastRecallSnippet = undefined;
280
+ this.#lastRetainedMessageIndex = 0;
281
+ this.#cachedTranscript = "";
282
+ this.#lastRetainedPrefixKey = "";
252
283
  }
253
284
 
254
285
  enqueueRetain(content: string, context?: string): void {
@@ -285,21 +316,35 @@ export class HindsightSessionState {
285
316
  async retainSession(messages: HindsightMessage[]): Promise<void> {
286
317
  const retainedAt = new Date();
287
318
  const retainFullWindow = this.config.retainMode === "full-session";
288
- let target: HindsightMessage[];
289
319
  let documentId: string;
320
+ let transcript: string;
321
+ let nextCachedTranscript: string | undefined;
290
322
 
291
323
  if (retainFullWindow) {
292
- target = messages;
293
324
  documentId = this.sessionId;
325
+ const boundary = this.#lastRetainedMessageIndex;
326
+ if (boundary > messages.length || retentionPrefixKey(messages, boundary) !== this.#lastRetainedPrefixKey) {
327
+ this.#lastRetainedMessageIndex = 0;
328
+ this.#cachedTranscript = "";
329
+ this.#lastRetainedPrefixKey = "";
330
+ }
331
+ const newMessages = messages.slice(this.#lastRetainedMessageIndex);
332
+ const { transcript: newPart } = prepareRetentionTranscript(newMessages, true);
333
+ if (!newPart) return;
334
+ nextCachedTranscript = this.#cachedTranscript ? `${this.#cachedTranscript}\n\n${newPart}` : newPart;
335
+ transcript = nextCachedTranscript;
294
336
  } else {
295
337
  const windowTurns = this.config.retainEveryNTurns + this.config.retainOverlapTurns;
296
- target = sliceLastTurnsByUserBoundary(messages, windowTurns);
338
+ const target = sliceLastTurnsByUserBoundary(messages, windowTurns);
297
339
  documentId = `${this.sessionId}-${retainedAt.getTime()}`;
340
+ this.#lastRetainedMessageIndex = 0;
341
+ this.#cachedTranscript = "";
342
+ this.#lastRetainedPrefixKey = "";
343
+ const { transcript: windowTranscript } = prepareRetentionTranscript(target, true);
344
+ if (!windowTranscript) return;
345
+ transcript = windowTranscript;
298
346
  }
299
347
 
300
- const { transcript } = prepareRetentionTranscript(target, true);
301
- if (!transcript) return;
302
-
303
348
  await ensureBankExists(this.client, this.bankId, this.config, this.banksSet);
304
349
  await this.client.retain(this.bankId, transcript, {
305
350
  documentId,
@@ -309,6 +354,11 @@ export class HindsightSessionState {
309
354
  timestamp: retainedAt,
310
355
  async: true,
311
356
  });
357
+ if (nextCachedTranscript !== undefined) {
358
+ this.#cachedTranscript = nextCachedTranscript;
359
+ this.#lastRetainedMessageIndex = messages.length;
360
+ this.#lastRetainedPrefixKey = retentionPrefixKey(messages, messages.length);
361
+ }
312
362
  }
313
363
 
314
364
  async maybeRetainOnAgentEnd(): Promise<void> {
@@ -341,6 +391,14 @@ export class HindsightSessionState {
341
391
  async forceRetainCurrentSession(): Promise<void> {
342
392
  const messages = extractMessages(this.session.sessionManager);
343
393
  if (messages.length === 0) return;
394
+ // Forced retains are user-initiated rebuilds (`/memory enqueue`): drop the
395
+ // incremental cache so the full transcript is reformatted and resent even
396
+ // when no new messages arrived since the last auto-retain — otherwise a
397
+ // rebuild could never recover an upstream document that was deleted or a
398
+ // previous async retain that never materialized.
399
+ this.#lastRetainedMessageIndex = 0;
400
+ this.#cachedTranscript = "";
401
+ this.#lastRetainedPrefixKey = "";
344
402
  try {
345
403
  await this.retainSession(messages);
346
404
  this.lastRetainedTurn = messages.filter(m => m.role === "user").length;
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { isEnoent } from "@oh-my-pi/pi-utils";
5
5
  import { AgentRegistry } from "../registry/agent-registry";
6
+ import { isMarkdownPath } from "../utils/lang-from-path";
6
7
  import { buildDirectoryResource } from "./filesystem-resource";
7
8
  import { parseInternalUrl } from "./parse";
8
9
  import { validateRelativePath } from "./skill-protocol";
@@ -43,8 +44,8 @@ function shortLocalRoot(options: LocalProtocolOptions): string {
43
44
  }
44
45
 
45
46
  function getContentType(filePath: string): InternalResource["contentType"] {
47
+ if (isMarkdownPath(filePath)) return "text/markdown";
46
48
  const ext = path.extname(filePath).toLowerCase();
47
- if (ext === ".md") return "text/markdown";
48
49
  if (ext === ".json") return "application/json";
49
50
  return "text/plain";
50
51
  }
@@ -4,6 +4,7 @@ import { getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
4
4
  import { getMemoryRoot } from "../memories";
5
5
  import { getMnemopiSessionState, type MnemopiScopedMemoryHit, type MnemopiSessionState } from "../mnemopi/state";
6
6
  import { AgentRegistry } from "../registry/agent-registry";
7
+ import { isMarkdownPath } from "../utils/lang-from-path";
7
8
  import { buildDirectoryResource } from "./filesystem-resource";
8
9
  import { validateRelativePath } from "./skill-protocol";
9
10
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
@@ -117,8 +118,7 @@ async function tryResolveInRoot(url: InternalUrl, memoryRoot: string): Promise<I
117
118
  }
118
119
 
119
120
  const content = await Bun.file(realTargetPath).text();
120
- const ext = path.extname(realTargetPath).toLowerCase();
121
- const contentType: InternalResource["contentType"] = ext === ".md" ? "text/markdown" : "text/plain";
121
+ const contentType: InternalResource["contentType"] = isMarkdownPath(realTargetPath) ? "text/markdown" : "text/plain";
122
122
 
123
123
  return {
124
124
  url: url.href,
@@ -12,12 +12,12 @@ import * as fs from "node:fs/promises";
12
12
  import * as path from "node:path";
13
13
  import { isEnoent } from "@oh-my-pi/pi-utils";
14
14
  import { getActiveSkills } from "../extensibility/skills";
15
+ import { isMarkdownPath } from "../utils/lang-from-path";
15
16
  import { buildDirectoryResource } from "./filesystem-resource";
16
17
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
17
18
 
18
19
  function getContentType(filePath: string): InternalResource["contentType"] {
19
- const ext = path.extname(filePath).toLowerCase();
20
- if (ext === ".md") return "text/markdown";
20
+ if (isMarkdownPath(filePath)) return "text/markdown";
21
21
  return "text/plain";
22
22
  }
23
23
 
@@ -30,6 +30,7 @@ import {
30
30
  statRemotePath,
31
31
  writeRemoteFile,
32
32
  } from "../ssh/file-transfer";
33
+ import { isMarkdownPath } from "../utils/lang-from-path";
33
34
  import type {
34
35
  InternalResource,
35
36
  InternalUrl,
@@ -44,11 +45,11 @@ const SSH_TEXT_MAX_BYTES = 1024 * 1024;
44
45
 
45
46
  /** POSIX-aware content type from the last path segment's extension. */
46
47
  function contentTypeFor(remotePath: string): InternalResource["contentType"] {
48
+ if (isMarkdownPath(remotePath)) return "text/markdown";
47
49
  const slash = remotePath.lastIndexOf("/");
48
50
  const base = slash === -1 ? remotePath : remotePath.slice(slash + 1);
49
51
  const dot = base.lastIndexOf(".");
50
52
  const ext = dot <= 0 ? "" : base.slice(dot).toLowerCase();
51
- if (ext === ".md") return "text/markdown";
52
53
  if (ext === ".json") return "application/json";
53
54
  return "text/plain";
54
55
  }
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { $which, isEnoent } from "@oh-my-pi/pi-utils";
4
4
  import { isSettingsInitialized, settings } from "../config/settings";
5
5
  import { getDefault } from "../config/settings-schema";
6
+ import { isMarkdownPath } from "../utils/lang-from-path";
6
7
  import { parseInternalUrl } from "./parse";
7
8
  import { validateRelativePath } from "./skill-protocol";
8
9
  import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, WriteContext } from "./types";
@@ -114,8 +115,8 @@ function toVaultValidationError(error: unknown): Error {
114
115
  }
115
116
 
116
117
  function getContentType(filePath: string): ContentType {
118
+ if (isMarkdownPath(filePath)) return "text/markdown";
117
119
  const ext = path.extname(filePath).toLowerCase();
118
- if (ext === ".md") return "text/markdown";
119
120
  if (ext === ".json") return "application/json";
120
121
  return "text/plain";
121
122
  }
@@ -0,0 +1,59 @@
1
+ class OmpPcmSourceProcessor extends AudioWorkletProcessor {
2
+ constructor() {
3
+ super();
4
+ this.frames = [];
5
+ this.frameOffset = 0;
6
+ this.queuedSamples = 0;
7
+ this.muted = false;
8
+ this.maxQueuedSamples = Math.floor(sampleRate * 0.75);
9
+ this.port.onmessage = event => {
10
+ const message = event.data;
11
+ if (message.type === "mute") {
12
+ this.muted = message.muted;
13
+ this.clear();
14
+ return;
15
+ }
16
+ if (message.type !== "audio" || this.muted || !(message.samples instanceof Float32Array)) return;
17
+ let samples = message.samples;
18
+ if (samples.length > this.maxQueuedSamples) samples = samples.slice(samples.length - this.maxQueuedSamples);
19
+ this.frames.push(samples);
20
+ this.queuedSamples += samples.length;
21
+ while (this.queuedSamples > this.maxQueuedSamples && this.frames.length > 1) {
22
+ const stale = this.frames.shift();
23
+ this.queuedSamples -= stale.length - this.frameOffset;
24
+ this.frameOffset = 0;
25
+ }
26
+ };
27
+ }
28
+
29
+ clear() {
30
+ this.frames.length = 0;
31
+ this.frameOffset = 0;
32
+ this.queuedSamples = 0;
33
+ }
34
+
35
+ process(_inputs, outputs) {
36
+ const output = outputs[0];
37
+ const channel = output && output[0];
38
+ if (!channel) return true;
39
+ channel.fill(0);
40
+ if (this.muted) return true;
41
+ let outputOffset = 0;
42
+ while (outputOffset < channel.length && this.frames.length > 0) {
43
+ const frame = this.frames[0];
44
+ const available = frame.length - this.frameOffset;
45
+ const count = Math.min(available, channel.length - outputOffset);
46
+ channel.set(frame.subarray(this.frameOffset, this.frameOffset + count), outputOffset);
47
+ outputOffset += count;
48
+ this.frameOffset += count;
49
+ this.queuedSamples -= count;
50
+ if (this.frameOffset === frame.length) {
51
+ this.frames.shift();
52
+ this.frameOffset = 0;
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+ }
58
+
59
+ registerProcessor("omp-pcm-source", OmpPcmSourceProcessor);
@@ -0,0 +1,221 @@
1
+ (() => {
2
+ const INPUT_SAMPLE_RATE = 16_000;
3
+ const LEVEL_INTERVAL_MS = 50;
4
+ const OPEN_TIMEOUT_MS = 20_000;
5
+ let context;
6
+ let peer;
7
+ let channel;
8
+ let sourceNode;
9
+ let sourceTrack;
10
+ let remoteSource;
11
+ let analyser;
12
+ let levelWaveform;
13
+ let levelTimer;
14
+ let disconnectTimer;
15
+ let workletUrl;
16
+ let closing = false;
17
+ let muted = false;
18
+ let openSettled = false;
19
+ let openResolve;
20
+ let openReject;
21
+ let previousInputSample;
22
+ let inputPhase = 0;
23
+ const openPromise = new Promise((resolve, reject) => {
24
+ openResolve = resolve;
25
+ openReject = reject;
26
+ });
27
+
28
+ function reportFailure(message) {
29
+ if (closing) return;
30
+ if (!openSettled) {
31
+ openSettled = true;
32
+ openReject(new Error(message));
33
+ }
34
+ Promise.resolve(window.__ompLiveFailure(message)).catch(() => {});
35
+ }
36
+
37
+ function waitForIceGathering() {
38
+ if (peer.iceGatheringState === "complete") return Promise.resolve();
39
+ return new Promise((resolve, reject) => {
40
+ const timeout = setTimeout(() => {
41
+ peer.removeEventListener("icegatheringstatechange", onChange);
42
+ reject(new Error("Timed out gathering WebRTC ICE candidates"));
43
+ }, 10_000);
44
+ function onChange() {
45
+ if (peer.iceGatheringState !== "complete") return;
46
+ clearTimeout(timeout);
47
+ peer.removeEventListener("icegatheringstatechange", onChange);
48
+ resolve();
49
+ }
50
+ peer.addEventListener("icegatheringstatechange", onChange);
51
+ });
52
+ }
53
+
54
+ function decodePcm(base64) {
55
+ const encoded = atob(base64);
56
+ const bytes = new Uint8Array(encoded.length);
57
+ for (let index = 0; index < encoded.length; index++) bytes[index] = encoded.charCodeAt(index);
58
+ const view = new DataView(bytes.buffer);
59
+ const samples = new Float32Array(bytes.length / 4);
60
+ for (let index = 0; index < samples.length; index++) samples[index] = view.getFloat32(index * 4, true);
61
+ return samples;
62
+ }
63
+
64
+ function resample(samples) {
65
+ if (samples.length === 0) return new Float32Array();
66
+ const step = INPUT_SAMPLE_RATE / context.sampleRate;
67
+ const prefix = previousInputSample === undefined ? 0 : 1;
68
+ const input = new Float32Array(samples.length + prefix);
69
+ if (prefix === 1) input[0] = previousInputSample;
70
+ input.set(samples, prefix);
71
+ if (prefix === 0) inputPhase = 0;
72
+ const available = input.length - 1;
73
+ if (available <= 0) {
74
+ previousInputSample = input[0];
75
+ return new Float32Array();
76
+ }
77
+ const outputLength = Math.max(0, Math.ceil((available - inputPhase) / step));
78
+ const output = new Float32Array(outputLength);
79
+ let written = 0;
80
+ while (inputPhase < available) {
81
+ const index = Math.floor(inputPhase);
82
+ const fraction = inputPhase - index;
83
+ output[written++] = input[index] + (input[index + 1] - input[index]) * fraction;
84
+ inputPhase += step;
85
+ }
86
+ inputPhase -= available;
87
+ previousInputSample = input[input.length - 1];
88
+ return written === output.length ? output : output.slice(0, written);
89
+ }
90
+
91
+ function reportLevel() {
92
+ if (!analyser || !levelWaveform) return;
93
+ analyser.getFloatTimeDomainData(levelWaveform);
94
+ let sum = 0;
95
+ for (const sample of levelWaveform) sum += sample * sample;
96
+ const level = Math.min(1, Math.max(0, Math.sqrt(sum / levelWaveform.length)));
97
+ Promise.resolve(window.__ompLiveOutputLevel(level)).catch(() => {});
98
+ }
99
+
100
+ function attachRemoteTrack(event) {
101
+ if (closing || event.track.kind !== "audio") return;
102
+ if (remoteSource) remoteSource.disconnect();
103
+ const stream = new MediaStream([event.track]);
104
+ remoteSource = context.createMediaStreamSource(stream);
105
+ analyser = context.createAnalyser();
106
+ analyser.fftSize = 1024;
107
+ levelWaveform = new Float32Array(analyser.fftSize);
108
+ remoteSource.connect(analyser);
109
+ analyser.connect(context.destination);
110
+ clearInterval(levelTimer);
111
+ levelTimer = setInterval(reportLevel, LEVEL_INTERVAL_MS);
112
+ }
113
+
114
+ async function start(workletSource) {
115
+ if (peer) throw new Error("Live browser runtime already started");
116
+ context = new AudioContext({ latencyHint: "interactive" });
117
+ await context.resume();
118
+ workletUrl = URL.createObjectURL(new Blob([workletSource], { type: "text/javascript" }));
119
+ await context.audioWorklet.addModule(workletUrl);
120
+ sourceNode = new AudioWorkletNode(context, "omp-pcm-source", {
121
+ numberOfInputs: 0,
122
+ numberOfOutputs: 1,
123
+ outputChannelCount: [1],
124
+ });
125
+ const destination = context.createMediaStreamDestination();
126
+ sourceNode.connect(destination);
127
+ sourceTrack = destination.stream.getAudioTracks()[0];
128
+ sourceTrack.enabled = !muted;
129
+
130
+ peer = new RTCPeerConnection();
131
+ peer.addTrack(sourceTrack, destination.stream);
132
+ peer.ontrack = attachRemoteTrack;
133
+ peer.onconnectionstatechange = () => {
134
+ clearTimeout(disconnectTimer);
135
+ if (peer.connectionState === "failed" || peer.connectionState === "closed") {
136
+ reportFailure(`WebRTC peer connection ${peer.connectionState}`);
137
+ } else if (peer.connectionState === "disconnected") {
138
+ disconnectTimer = setTimeout(() => {
139
+ if (peer && peer.connectionState === "disconnected") reportFailure("WebRTC peer connection disconnected");
140
+ }, 2_000);
141
+ }
142
+ };
143
+ channel = peer.createDataChannel("oai-events");
144
+ channel.onopen = () => {
145
+ if (openSettled) return;
146
+ openSettled = true;
147
+ openResolve();
148
+ };
149
+ channel.onerror = () => reportFailure("Live data channel failed");
150
+ channel.onclose = () => reportFailure("Live data channel closed unexpectedly");
151
+ channel.onmessage = event => {
152
+ if (typeof event.data !== "string") return;
153
+ Promise.resolve(window.__ompLiveServerEvent(event.data)).catch(() => {});
154
+ };
155
+
156
+ const offer = await peer.createOffer();
157
+ await peer.setLocalDescription(offer);
158
+ await waitForIceGathering();
159
+ if (!peer.localDescription || !peer.localDescription.sdp) throw new Error("Browser produced an empty WebRTC offer");
160
+ return peer.localDescription.sdp;
161
+ }
162
+
163
+ async function acceptAnswer(sdp) {
164
+ if (!peer) throw new Error("Live browser runtime has not started");
165
+ await peer.setRemoteDescription({ type: "answer", sdp });
166
+ }
167
+
168
+ async function waitForOpen() {
169
+ let timeout;
170
+ try {
171
+ await Promise.race([
172
+ openPromise,
173
+ new Promise((_, reject) => {
174
+ timeout = setTimeout(() => reject(new Error("Timed out waiting for the live data channel to open")), OPEN_TIMEOUT_MS);
175
+ }),
176
+ ]);
177
+ } finally {
178
+ clearTimeout(timeout);
179
+ }
180
+ }
181
+
182
+ function send(payload) {
183
+ if (!channel || channel.readyState !== "open") throw new Error("Live data channel is not open");
184
+ channel.send(payload);
185
+ }
186
+
187
+ function pushAudio(base64) {
188
+ if (!sourceNode || muted) return;
189
+ const output = resample(decodePcm(base64));
190
+ if (output.length === 0) return;
191
+ sourceNode.port.postMessage({ type: "audio", samples: output }, [output.buffer]);
192
+ }
193
+
194
+ function setMuted(nextMuted) {
195
+ muted = nextMuted;
196
+ previousInputSample = undefined;
197
+ inputPhase = 0;
198
+ if (sourceTrack) sourceTrack.enabled = !muted;
199
+ if (sourceNode) sourceNode.port.postMessage({ type: "mute", muted });
200
+ }
201
+
202
+ async function close() {
203
+ if (closing) return;
204
+ closing = true;
205
+ clearInterval(levelTimer);
206
+ clearTimeout(disconnectTimer);
207
+ if (channel) channel.close();
208
+ if (peer) peer.close();
209
+ if (remoteSource) remoteSource.disconnect();
210
+ if (analyser) analyser.disconnect();
211
+ if (sourceNode) {
212
+ sourceNode.port.postMessage({ type: "mute", muted: true });
213
+ sourceNode.disconnect();
214
+ }
215
+ if (sourceTrack) sourceTrack.stop();
216
+ if (workletUrl) URL.revokeObjectURL(workletUrl);
217
+ if (context && context.state !== "closed") await context.close();
218
+ }
219
+
220
+ window.ompCodexLive = { start, acceptAnswer, waitForOpen, send, pushAudio, setMuted, close };
221
+ })();