@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
@@ -6,6 +6,10 @@
6
6
  */
7
7
  import * as AIError from "@oh-my-pi/pi-ai/error";
8
8
  import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
9
+ import { withTimeoutSignal } from "../utils/fetch-timeout";
10
+
11
+ /** Per-request abort deadline for each OAuth discovery metadata fetch. */
12
+ const DISCOVERY_FETCH_TIMEOUT_MS = 10_000;
9
13
 
10
14
  export interface OAuthEndpoints {
11
15
  authorizationUrl: string;
@@ -346,7 +350,7 @@ function readMetadataScopes(metadata: Record<string, unknown>): string | undefin
346
350
  */
347
351
  export async function fetchResourceMetadataScopes(
348
352
  resourceMetadataUrl: string,
349
- opts?: { fetch?: FetchImpl },
353
+ opts?: { fetch?: FetchImpl; signal?: AbortSignal },
350
354
  ): Promise<string | undefined> {
351
355
  const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
352
356
  try {
@@ -354,6 +358,7 @@ export async function fetchResourceMetadataScopes(
354
358
  method: "GET",
355
359
  headers: { Accept: "application/json" },
356
360
  redirect: "follow",
361
+ signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
357
362
  });
358
363
  if (!resp.ok) return undefined;
359
364
  const meta = (await resp.json()) as Record<string, unknown>;
@@ -371,7 +376,7 @@ export async function discoverOAuthEndpoints(
371
376
  serverUrl: string,
372
377
  authServerUrl?: string,
373
378
  resourceMetadataUrl?: string,
374
- opts?: { fetch?: FetchImpl; protectedResource?: string; protectedScopes?: string },
379
+ opts?: { fetch?: FetchImpl; protectedResource?: string; protectedScopes?: string; signal?: AbortSignal },
375
380
  ): Promise<OAuthEndpoints | null> {
376
381
  const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
377
382
  const wellKnownPaths = [
@@ -402,6 +407,7 @@ export async function discoverOAuthEndpoints(
402
407
  method: "GET",
403
408
  headers: { Accept: "application/json" },
404
409
  redirect: "follow",
410
+ signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
405
411
  });
406
412
  if (metaResp.ok) {
407
413
  const meta = (await metaResp.json()) as Record<string, unknown>;
@@ -486,6 +492,7 @@ export async function discoverOAuthEndpoints(
486
492
  method: "GET",
487
493
  headers: { Accept: "application/json" },
488
494
  redirect: "follow",
495
+ signal: withTimeoutSignal(DISCOVERY_FETCH_TIMEOUT_MS, opts?.signal),
489
496
  });
490
497
 
491
498
  if (response.ok) {
@@ -521,6 +528,7 @@ export async function discoverOAuthEndpoints(
521
528
  fetch: fetchImpl,
522
529
  protectedResource: discoveredProtectedResource,
523
530
  protectedScopes: readMetadataScopes(metadata) ?? protectedScopes,
531
+ signal: opts?.signal,
524
532
  });
525
533
  if (discovered) return discovered;
526
534
  }
@@ -0,0 +1,29 @@
1
+ import { afterEach, describe, expect, it, vi } from "bun:test";
2
+ import { pollSmitheryCliAuthSession } from "./smithery-auth";
3
+
4
+ type FetchInput = string | URL | Request;
5
+ type FetchInit = RequestInit | BunFetchRequestInit;
6
+
7
+ describe("pollSmitheryCliAuthSession fetch cancellation", () => {
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ it("bounds each poll request with a timeout abort signal even without a caller signal", async () => {
13
+ const signals: AbortSignal[] = [];
14
+ const fetchStub = Object.assign(
15
+ async (_input: FetchInput, init?: FetchInit) => {
16
+ if (init?.signal instanceof AbortSignal) signals.push(init.signal);
17
+ return Response.json({ status: "pending" });
18
+ },
19
+ { preconnect: globalThis.fetch.preconnect },
20
+ );
21
+ vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
22
+
23
+ const result = await pollSmitheryCliAuthSession("sess-123");
24
+
25
+ expect(result.status).toBe("pending");
26
+ expect(signals).toHaveLength(1);
27
+ expect(signals[0]?.aborted).toBe(false);
28
+ });
29
+ });
@@ -7,13 +7,14 @@ import { withTimeoutSignal } from "../utils/fetch-timeout";
7
7
  const SMITHERY_AUTH_FILENAME = "smithery.json";
8
8
  const SMITHERY_URL = process.env.SMITHERY_URL || "https://smithery.ai";
9
9
  const SMITHERY_AUTH_TIMEOUT_MS = 10_000;
10
+ const SMITHERY_POLL_TIMEOUT_MS = 30_000;
10
11
 
11
12
  type SmitheryCliAuthSession = {
12
13
  sessionId: string;
13
14
  authUrl: string;
14
15
  };
15
16
 
16
- type SmitheryCliPollResponse = {
17
+ export type SmitheryCliPollResponse = {
17
18
  status: "pending" | "success" | "error";
18
19
  apiKey?: string;
19
20
  message?: string;
@@ -53,7 +54,7 @@ export async function pollSmitheryCliAuthSession(
53
54
  signal?: AbortSignal,
54
55
  ): Promise<SmitheryCliPollResponse> {
55
56
  const response = await fetch(`${SMITHERY_URL}/api/auth/cli/poll/${sessionId}`, {
56
- signal,
57
+ signal: withTimeoutSignal(SMITHERY_POLL_TIMEOUT_MS, signal),
57
58
  });
58
59
  if (!response.ok) {
59
60
  if (response.status === 404 || response.status === 410) {
@@ -142,9 +142,12 @@ export function startMemoryStartupTask(options: {
142
142
  return;
143
143
  }
144
144
 
145
- void runMemoryStartup({ session, settings, modelRegistry, agentDir, config: cfg }).catch(error => {
146
- logger.warn("Memory startup failed", { error: String(error) });
147
- });
145
+ const signal = session.beginLocalMemoryStartup?.() ?? new AbortController().signal;
146
+ void runMemoryStartup({ session, settings, modelRegistry, agentDir, config: cfg, signal })
147
+ .catch(error => {
148
+ if (!signal.aborted) logger.warn("Memory startup failed", { error: String(error) });
149
+ })
150
+ .finally(() => session.endLocalMemoryStartup?.(signal));
148
151
  }
149
152
 
150
153
  interface MemoryInstructionSession {
@@ -315,26 +318,32 @@ export function enqueueMemoryConsolidation(agentDir: string, cwd: string, source
315
318
  }
316
319
  }
317
320
 
318
- async function runMemoryStartup(options: {
321
+ interface MemoryStartupOptions {
319
322
  session: AgentSession;
320
323
  settings: Settings;
321
324
  modelRegistry: ModelRegistry;
322
325
  agentDir: string;
323
326
  config: MemoryRuntimeConfig;
324
- }): Promise<void> {
327
+ signal: AbortSignal;
328
+ }
329
+
330
+ function isMemoryStartupActive(options: MemoryStartupOptions): boolean {
331
+ return !options.signal.aborted && !options.session.isDisposed && options.settings.get("memory.backend") === "local";
332
+ }
333
+
334
+ async function runMemoryStartup(options: MemoryStartupOptions): Promise<void> {
335
+ if (!isMemoryStartupActive(options)) return;
325
336
  await runPhase1(options);
337
+ if (!isMemoryStartupActive(options)) return;
326
338
  await runPhase2(options);
339
+ if (!isMemoryStartupActive(options)) return;
327
340
  await refreshMemoryToolDeveloperInstructionsCacheAfterStartup(options.session, options.agentDir, options.settings);
341
+ if (!isMemoryStartupActive(options)) return;
328
342
  await options.session.refreshBaseSystemPrompt?.();
329
343
  }
330
344
 
331
- async function runPhase1(options: {
332
- session: AgentSession;
333
- settings: Settings;
334
- modelRegistry: ModelRegistry;
335
- agentDir: string;
336
- config: MemoryRuntimeConfig;
337
- }): Promise<void> {
345
+ async function runPhase1(options: MemoryStartupOptions): Promise<void> {
346
+ if (!isMemoryStartupActive(options)) return;
338
347
  const { session, modelRegistry, agentDir, config } = options;
339
348
  const db = openMemoryDb(getAgentDbPath(agentDir));
340
349
  const nowSec = unixNow();
@@ -344,6 +353,7 @@ async function runPhase1(options: {
344
353
 
345
354
  try {
346
355
  const threads = await collectThreads(session, currentThreadId);
356
+ if (!isMemoryStartupActive(options)) return;
347
357
  upsertThreads(db, threads);
348
358
 
349
359
  const phase1Model = await resolveMemoryModel({
@@ -364,6 +374,7 @@ async function runPhase1(options: {
364
374
  return;
365
375
  }
366
376
 
377
+ if (!isMemoryStartupActive(options)) return;
367
378
  const claims = claimStage1Jobs(db, {
368
379
  nowSec,
369
380
  threadScanLimit: config.threadScanLimit,
@@ -387,6 +398,7 @@ async function runPhase1(options: {
387
398
  };
388
399
 
389
400
  await runWithConcurrency(claims, config.stage1Concurrency, async claim => {
401
+ if (!isMemoryStartupActive(options)) return;
390
402
  const result = await runStage1Job({
391
403
  claim,
392
404
  model: phase1Model,
@@ -395,6 +407,7 @@ async function runPhase1(options: {
395
407
  config,
396
408
  metadata: session.agent?.metadataForProvider(phase1Model.provider),
397
409
  });
410
+ if (!isMemoryStartupActive(options)) return;
398
411
 
399
412
  if (result.kind === "failed") {
400
413
  logger.error("Memory phase1 stage1 job failed", {
@@ -460,13 +473,8 @@ async function runPhase1(options: {
460
473
  }
461
474
  }
462
475
 
463
- async function runPhase2(options: {
464
- session: AgentSession;
465
- settings: Settings;
466
- modelRegistry: ModelRegistry;
467
- agentDir: string;
468
- config: MemoryRuntimeConfig;
469
- }): Promise<void> {
476
+ async function runPhase2(options: MemoryStartupOptions): Promise<void> {
477
+ if (!isMemoryStartupActive(options)) return;
470
478
  const { session, modelRegistry, agentDir, config } = options;
471
479
  const cwd = session.sessionManager.getCwd();
472
480
  const db = openMemoryDb(getAgentDbPath(agentDir));
@@ -488,8 +496,10 @@ async function runPhase2(options: {
488
496
  const newWatermark = computeCompletionWatermark(claim.inputWatermark, outputs);
489
497
 
490
498
  await syncPhase2Artifacts(memoryRoot, outputs);
499
+ if (!isMemoryStartupActive(options)) return;
491
500
  if (outputs.length === 0) {
492
501
  await cleanupConsolidatedArtifacts(memoryRoot);
502
+ if (!isMemoryStartupActive(options)) return;
493
503
  const marked = markGlobalPhase2Succeeded(db, {
494
504
  ownershipToken: claim.ownershipToken,
495
505
  newWatermark,
@@ -502,6 +512,7 @@ async function runPhase2(options: {
502
512
  return;
503
513
  }
504
514
 
515
+ if (!isMemoryStartupActive(options)) return;
505
516
  const phase2Model = await resolveMemoryModel({
506
517
  modelRegistry,
507
518
  session,
@@ -529,8 +540,13 @@ async function runPhase2(options: {
529
540
  return;
530
541
  }
531
542
 
543
+ if (!isMemoryStartupActive(options)) return;
532
544
  let heartbeatLostOwnership = false;
533
545
  const heartbeat = setInterval(() => {
546
+ if (!isMemoryStartupActive(options)) {
547
+ clearInterval(heartbeat);
548
+ return;
549
+ }
534
550
  const ok = heartbeatGlobalJob(db, {
535
551
  ownershipToken: claim.ownershipToken,
536
552
  leaseSeconds: config.phase2LeaseSeconds,
@@ -544,13 +560,16 @@ async function runPhase2(options: {
544
560
  }, config.phase2HeartbeatSeconds * 1000);
545
561
 
546
562
  try {
563
+ if (!isMemoryStartupActive(options)) return;
547
564
  const consolidated = await runConsolidationModel({
548
565
  memoryRoot,
549
566
  model: phase2Model,
550
567
  apiKey: modelRegistry.resolver(phase2Model, session.sessionId),
551
568
  metadata: session.agent?.metadataForProvider(phase2Model.provider),
552
569
  });
570
+ if (!isMemoryStartupActive(options)) return;
553
571
  await applyConsolidation(memoryRoot, consolidated);
572
+ if (!isMemoryStartupActive(options)) return;
554
573
  if (heartbeatLostOwnership) {
555
574
  throw new Error("Phase2 lease ownership lost before completion");
556
575
  }
@@ -564,6 +583,7 @@ async function runPhase2(options: {
564
583
  throw new Error("Phase2 could not mark success: ownership lost");
565
584
  }
566
585
  } catch (error) {
586
+ if (!isMemoryStartupActive(options)) return;
567
587
  markPhase2FailureWithFallback(db, {
568
588
  claim,
569
589
  retryDelaySeconds: config.phase2RetryDelaySeconds,
@@ -1227,7 +1247,7 @@ async function resolveMemoryModel(options: {
1227
1247
 
1228
1248
  function loadMemoryConfig(settings: Settings): MemoryRuntimeConfig {
1229
1249
  return {
1230
- enabled: settings.get("memory.backend") === "local" || settings.get("memories.enabled") === true,
1250
+ enabled: settings.get("memory.backend") === "local",
1231
1251
  maxRolloutsPerStartup: settings.get("memories.maxRolloutsPerStartup") ?? DEFAULTS.maxRolloutsPerStartup,
1232
1252
  maxRolloutAgeDays: settings.get("memories.maxRolloutAgeDays") ?? DEFAULTS.maxRolloutAgeDays,
1233
1253
  minRolloutIdleHours: settings.get("memories.minRolloutIdleHours") ?? DEFAULTS.minRolloutIdleHours,
@@ -0,0 +1,2 @@
1
+ /** Built-in tools whose availability depends on the selected memory backend. */
2
+ export const MEMORY_BACKEND_TOOL_NAMES = ["retain", "recall", "reflect", "memory_edit", "learn"] as const;
@@ -62,6 +62,20 @@ const STATIC_INSTRUCTIONS = [
62
62
  "",
63
63
  ].join("\n");
64
64
 
65
+ async function installMnemopiState(session: AgentSession, config: MnemopiBackendConfig): Promise<MnemopiSessionState> {
66
+ const state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
67
+ const previous = setMnemopiSessionState(session, state);
68
+ await previous?.dispose();
69
+ try {
70
+ state.attachSessionListeners();
71
+ return state;
72
+ } catch (error) {
73
+ setMnemopiSessionState(session, undefined);
74
+ await state.dispose({ consolidate: false });
75
+ throw error;
76
+ }
77
+ }
78
+
65
79
  export const mnemopiBackend: MemoryBackend = {
66
80
  id: "mnemopi",
67
81
 
@@ -90,10 +104,7 @@ export const mnemopiBackend: MemoryBackend = {
90
104
  try {
91
105
  const config = await loadMnemopiConfigWithProviders(settings, agentDir, modelRegistry, sessionId);
92
106
  await Promise.all([loadMnemopi(), loadMnemopiCore()]);
93
- const state = new MnemopiSessionState({ sessionId, config, session });
94
- const previous = setMnemopiSessionState(session, state);
95
- await previous?.dispose();
96
- state.attachSessionListeners();
107
+ await installMnemopiState(session, config);
97
108
  } catch (error) {
98
109
  logger.warn("Mnemopi: backend startup failed; memory backend inert.", { error: String(error) });
99
110
  }
@@ -129,12 +140,19 @@ export const mnemopiBackend: MemoryBackend = {
129
140
  requireMnemopiCore().resetMemoryForTests();
130
141
  await Bun.sleep(0);
131
142
  await removeDbFiles(getMnemopiScopedDbPaths(config));
143
+ if (!session?.sessionId || previous?.aliasOf || session.settings.get("memory.backend") !== "mnemopi") return;
144
+ try {
145
+ await Promise.all([loadMnemopi(), loadMnemopiCore()]);
146
+ await installMnemopiState(session, config);
147
+ } catch (error) {
148
+ logger.warn("Mnemopi: clear rehydrate failed; memory backend inert.", { error: String(error) });
149
+ }
132
150
  },
133
151
 
134
152
  async enqueue(agentDir, _cwd, session): Promise<void> {
135
153
  try {
136
154
  let state = getMnemopiSessionState(session);
137
- if (!state && session) {
155
+ if (!state && session?.sessionId) {
138
156
  const config = await loadMnemopiConfigWithProviders(
139
157
  session.settings,
140
158
  agentDir,
@@ -142,8 +160,7 @@ export const mnemopiBackend: MemoryBackend = {
142
160
  session.sessionId,
143
161
  );
144
162
  await Promise.all([loadMnemopi(), loadMnemopiCore()]);
145
- state = new MnemopiSessionState({ sessionId: session.sessionId, config, session });
146
- setMnemopiSessionState(session, state);
163
+ state = await installMnemopiState(session, config);
147
164
  }
148
165
  await state?.consolidate({ full: true });
149
166
  } catch (error) {
@@ -164,6 +164,10 @@ function isPromptTurnInFlight(turn: PromptTurnState | undefined): turn is Prompt
164
164
  type ManagedSessionRecord = {
165
165
  session: AgentSession;
166
166
  mcpManager: MCPManager | undefined;
167
+ // Ordered queue of MCP tool refreshes for this record. Rebuilt per
168
+ // `#configureMcpServers` call; drained on reconfigure so a stale in-flight
169
+ // refresh can never land after a newer configuration's tools.
170
+ mcpRefreshChain: Promise<void> | undefined;
167
171
  promptTurn: PromptTurnState | undefined;
168
172
  promptQueue: PromptQueueState;
169
173
  liveMessageId: string | undefined;
@@ -1132,6 +1136,7 @@ export class AcpAgent implements Agent {
1132
1136
  return {
1133
1137
  session,
1134
1138
  mcpManager: undefined,
1139
+ mcpRefreshChain: undefined,
1135
1140
  promptTurn: undefined,
1136
1141
  promptQueue: { promise: Promise.resolve(), release: undefined },
1137
1142
  liveMessageId: undefined,
@@ -2275,6 +2280,24 @@ export class AcpAgent implements Agent {
2275
2280
  return;
2276
2281
  }
2277
2282
 
2283
+ const uiContext = createAcpExtensionUiContext(
2284
+ this.#connection,
2285
+ () => record.session.sessionId,
2286
+ this.#clientCapabilities,
2287
+ );
2288
+ if (this.#clientCapabilities?.elicitation?.form != null) {
2289
+ record.session.setUsageFallbackConfirmer(confirmation => {
2290
+ const reserve =
2291
+ confirmation.remainingPercent === undefined
2292
+ ? "inside the configured reserve margin"
2293
+ : `${confirmation.remainingPercent.toFixed(1)}% remaining`;
2294
+ return uiContext.confirm(
2295
+ "Coding-plan reserve reached",
2296
+ `${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
2297
+ );
2298
+ });
2299
+ }
2300
+
2278
2301
  const extensionRunner = record.session.extensionRunner;
2279
2302
  if (!extensionRunner) {
2280
2303
  record.extensionsConfigured = true;
@@ -2311,6 +2334,8 @@ export class AcpAgent implements Agent {
2311
2334
  },
2312
2335
  getThinkingLevel: () => record.session.thinkingLevel,
2313
2336
  setThinkingLevel: level => record.session.setThinkingLevel(level),
2337
+ getServiceTiers: () => record.session.serviceTierByFamily,
2338
+ setServiceTier: (family, tier) => record.session.setServiceTierFamily(family, tier),
2314
2339
  getSessionName: () => record.session.sessionManager.getSessionName(),
2315
2340
  setSessionName: async name => {
2316
2341
  await record.session.sessionManager.setSessionName(name, "user");
@@ -2355,15 +2380,7 @@ export class AcpAgent implements Agent {
2355
2380
  },
2356
2381
  compact: instructionsOrOptions => runExtensionCompact(record.session, instructionsOrOptions),
2357
2382
  },
2358
- // Per-session getter: `record.session.sessionId` reads through to
2359
- // `sessionManager.getSessionId()` (it's a getter, not a field), so an
2360
- // extension command that calls `ctx.newSession` / `ctx.switchSession`
2361
- // — both exposed in the block just above — mutates the underlying id
2362
- // mid-flight. Reading lazily on each elicitation matches every other
2363
- // `sessionUpdate` call in this file. Hoisting the factory to an
2364
- // `AcpAgent` field would still be wrong because it would also lose
2365
- // the per-`record` binding.
2366
- createAcpExtensionUiContext(this.#connection, () => record.session.sessionId, this.#clientCapabilities),
2383
+ uiContext,
2367
2384
  );
2368
2385
  await extensionRunner.emit({ type: "session_start" });
2369
2386
  record.extensionsConfigured = true;
@@ -2373,6 +2390,11 @@ export class AcpAgent implements Agent {
2373
2390
  if (record.mcpManager) {
2374
2391
  await record.mcpManager.disconnectAll();
2375
2392
  }
2393
+ // Drain any in-flight refresh queued by a previous configuration: a refresh
2394
+ // that already passed its manager guard could otherwise finish applying a
2395
+ // stale tool set after this reconfiguration installs the new one.
2396
+ await record.mcpRefreshChain;
2397
+ record.mcpRefreshChain = undefined;
2376
2398
  if (servers.length === 0) {
2377
2399
  record.mcpManager = undefined;
2378
2400
  await record.session.refreshMCPTools([]);
@@ -2380,6 +2402,30 @@ export class AcpAgent implements Agent {
2380
2402
  }
2381
2403
 
2382
2404
  const manager = new MCPManager(record.session.sessionManager.getCwd());
2405
+ // MCP servers connect and reconnect independently, so `onToolsChanged` can fire
2406
+ // several times back to back. Each firing is chained onto `record.mcpRefreshChain`
2407
+ // so refreshes apply in order, and each one re-reads `manager.getTools()` at the
2408
+ // time it actually runs rather than the snapshot from when it was queued — so a
2409
+ // refresh can never apply a stale, smaller tool set after a newer one already landed.
2410
+ // The returned promise propagates failures (the initial awaited refresh below must
2411
+ // fail session setup, as the pre-queue code did); the stored chain swallows them
2412
+ // after logging so background firings only warn and the chain never rejects.
2413
+ const enqueueMcpToolsRefresh = (): Promise<void> => {
2414
+ const run = (record.mcpRefreshChain ?? Promise.resolve()).then(async () => {
2415
+ if (record.mcpManager !== manager) return;
2416
+ await record.session.refreshMCPTools(manager.getTools());
2417
+ });
2418
+ record.mcpRefreshChain = run.catch(error => {
2419
+ logger.warn("ACP MCP tool refresh failed", {
2420
+ error: error instanceof Error ? error.message : String(error),
2421
+ });
2422
+ });
2423
+ return run;
2424
+ };
2425
+ manager.setOnToolsChanged(() => {
2426
+ // Failures are logged once via the stored chain's catch above.
2427
+ enqueueMcpToolsRefresh().catch(() => {});
2428
+ });
2383
2429
  const configs: MCPConfigMap = {};
2384
2430
  const sources: MCPSourceMap = {};
2385
2431
  for (const server of servers) {
@@ -2402,7 +2448,7 @@ export class AcpAgent implements Agent {
2402
2448
  }
2403
2449
 
2404
2450
  record.mcpManager = manager;
2405
- await record.session.refreshMCPTools(result.tools);
2451
+ await enqueueMcpToolsRefresh();
2406
2452
  }
2407
2453
 
2408
2454
  #toMcpConfig(server: McpServer): MCPServerConfig {
@@ -369,6 +369,7 @@ const todoStatusMap: Record<TodoStatus, "pending" | "in_progress" | "completed">
369
369
  in_progress: "in_progress",
370
370
  completed: "completed",
371
371
  abandoned: "completed",
372
+ blocked: "pending",
372
373
  };
373
374
 
374
375
  function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "completed" {
@@ -432,7 +433,13 @@ function extractTodoEntries(phases: unknown[]): Array<{ content: string; status:
432
433
  }
433
434
 
434
435
  function isTodoStatus(status: unknown): status is TodoStatus {
435
- return status === "pending" || status === "in_progress" || status === "completed" || status === "abandoned";
436
+ return (
437
+ status === "pending" ||
438
+ status === "in_progress" ||
439
+ status === "completed" ||
440
+ status === "abandoned" ||
441
+ status === "blocked"
442
+ );
436
443
  }
437
444
  export function buildToolCallStartUpdate(input: {
438
445
  toolCallId: string;
@@ -636,7 +636,7 @@ export class AgentHubOverlayComponent extends Container {
636
636
  if (ref.status === "running" && ref.session) {
637
637
  await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
638
638
  }
639
- await this.#lifecycle().release(ref.id);
639
+ await this.#lifecycle().release(ref.id, ref);
640
640
  } catch (error) {
641
641
  logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
642
642
  this.#notice = error instanceof Error ? error.message : String(error);
@@ -1,6 +1,7 @@
1
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
2
- import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
2
+ import { Container, getKeybindings, Input, Spacer, Text, type TUI, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
3
3
  import { theme } from "../../modes/theme/theme";
4
+ import { urlHyperlinkAlways, WidthAwareText } from "../../tui";
4
5
  import { openPath } from "../../utils/open";
5
6
  import { DynamicBorder } from "./dynamic-border";
6
7
 
@@ -75,13 +76,22 @@ export class LoginDialogComponent extends Container {
75
76
  * is offered as an additional local shortcut so narrow local terminals still
76
77
  * have a truncation-safe copy target (viewport clipping on a long authorize
77
78
  * URL silently drops trailing OAuth query parameters — e.g.
78
- * `code_challenge_method=S256`). The OSC 8 hyperlink carries the full URL
79
- * for terminals that support click-through.
79
+ * `code_challenge_method=S256`). Every physical URL row carries its own OSC 8
80
+ * link to the full URL, so clicking any wrapped fragment opens the same target.
80
81
  */
81
82
  showAuth(url: string, instructions?: string, launchUrl?: string): void {
82
83
  this.#contentContainer.clear();
83
84
  this.#contentContainer.addChild(new Spacer(1));
84
- this.#contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
85
+ this.#contentContainer.addChild(
86
+ new WidthAwareText(
87
+ contentWidth =>
88
+ wrapTextWithAnsi(url, contentWidth)
89
+ .map(row => theme.fg("accent", urlHyperlinkAlways(url, row)))
90
+ .join("\n"),
91
+ 1,
92
+ 0,
93
+ ),
94
+ );
85
95
 
86
96
  const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
87
97
  const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;