@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

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 +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
package/src/main.ts CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  type ScopedModel,
42
42
  } from "./config/model-resolver";
43
43
  import { ModelsConfigFile } from "./config/models-config";
44
- import { getDefault, type SettingPath, Settings, settings } from "./config/settings";
44
+ import { getDefault, type SettingPath, Settings, type SettingValue, settings } from "./config/settings";
45
45
  import { initializeWithSettings } from "./discovery";
46
46
  import {
47
47
  clearPluginRootsAndCaches,
@@ -74,6 +74,13 @@ import {
74
74
  import type { AgentSession } from "./session/agent-session";
75
75
  import type { AuthStorage } from "./session/auth-storage";
76
76
  import { describePendingToolCalls } from "./session/exit-diagnostics";
77
+ import {
78
+ createForeignSessionStore,
79
+ foreignSessionInfoToSessionInfo,
80
+ foreignSessionSourceName,
81
+ persistForeignSession,
82
+ } from "./session/foreign-session-import";
83
+ import type { ForeignSessionInfo, ForeignSessionSource, ForeignSessionStore } from "./session/foreign-session-store";
77
84
  import { resolveResumableSession, type SessionInfo } from "./session/session-listing";
78
85
  import { SessionManager } from "./session/session-manager";
79
86
  import { executeBuiltinSlashCommand } from "./slash-commands/builtin-registry";
@@ -83,14 +90,7 @@ import { createPersistedSubagentReviverFactory } from "./task/persisted-revive";
83
90
  import { createTelemetryExportConfig, initTelemetryExport, isTelemetryExportEnabled } from "./telemetry-export";
84
91
  import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking";
85
92
  import type { LspStartupServerInfo } from "./tools";
86
- import {
87
- getChangelogPath,
88
- parseChangelog,
89
- parseChangelogVersion,
90
- readLastChangelogVersion,
91
- selectStartupChangelog,
92
- writeLastChangelogVersion,
93
- } from "./utils/changelog";
93
+ import { getChangelogPath, resolveStartupChangelogForDisplay, type StartupChangelogSelection } from "./utils/changelog";
94
94
  import { EventBus } from "./utils/event-bus";
95
95
  import { withTimeoutSignal } from "./utils/fetch-timeout";
96
96
 
@@ -408,7 +408,7 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
408
408
  async function runInteractiveMode(
409
409
  session: AgentSession,
410
410
  version: string,
411
- changelogMarkdown: string | undefined,
411
+ startupChangelog: StartupChangelogSelection | undefined,
412
412
  notifs: (InteractiveModeNotify | null)[],
413
413
  versionCheckPromise: Promise<string | undefined>,
414
414
  initialMessages: string[],
@@ -426,7 +426,7 @@ async function runInteractiveMode(
426
426
  const mode = new InteractiveMode(
427
427
  session,
428
428
  version,
429
- changelogMarkdown,
429
+ startupChangelog,
430
430
  setExtensionUIContext,
431
431
  lspServers,
432
432
  mcpManager,
@@ -565,6 +565,27 @@ export class SessionResolutionError extends Error {
565
565
  }
566
566
  }
567
567
 
568
+ function resolveForeignSessionSource(
569
+ parsed: Pick<Args, "continue" | "fork" | "fromClaude" | "fromCodex" | "noSession" | "resume">,
570
+ ): ForeignSessionSource | undefined {
571
+ if (parsed.fromClaude && parsed.fromCodex) {
572
+ throw new SessionResolutionError("--from-claude and --from-codex cannot be used together");
573
+ }
574
+ const source = parsed.fromClaude ? "claude" : parsed.fromCodex ? "codex" : undefined;
575
+ if (!source) return undefined;
576
+ if (parsed.noSession) {
577
+ throw new SessionResolutionError(`--from-${source} requires session persistence`);
578
+ }
579
+ if (parsed.continue || parsed.resume || parsed.fork) {
580
+ throw new SessionResolutionError(`--from-${source} cannot be combined with --continue, --resume, or --fork`);
581
+ }
582
+ return source;
583
+ }
584
+
585
+ function isForeignSessionImport(parsed: Pick<Args, "fromClaude" | "fromCodex">): boolean {
586
+ return parsed.fromClaude === true || parsed.fromCodex === true;
587
+ }
588
+
568
589
  type MissingCwdMoveResult =
569
590
  | { status: "not-needed" }
570
591
  | { status: "declined" }
@@ -651,33 +672,19 @@ async function resolveScopedModels(
651
672
  );
652
673
  }
653
674
 
654
- async function getChangelogForDisplay(parsed: Args): Promise<string | undefined> {
655
- if (parsed.continue || parsed.resume) {
656
- return undefined;
657
- }
658
-
659
- const lastVersion = await readLastChangelogVersion();
660
- const parsedLastVersion = parseChangelogVersion(lastVersion);
661
- if (!parsedLastVersion) {
662
- await writeLastChangelogVersion(VERSION);
663
- return undefined;
664
- }
665
- if (lastVersion === VERSION) {
666
- // Steady state: user already saw the current version's changelog. Skip the file read + parse.
675
+ async function getChangelogForDisplay(
676
+ parsed: Args,
677
+ mode: SettingValue<"startup.changelogMode">,
678
+ ): Promise<StartupChangelogSelection | undefined> {
679
+ if (parsed.continue || parsed.resume || isForeignSessionImport(parsed)) {
667
680
  return undefined;
668
681
  }
669
682
 
670
- const changelogPath = getChangelogPath();
671
- const entries = await parseChangelog(changelogPath);
672
- const startupChangelog = selectStartupChangelog(entries, lastVersion, VERSION);
673
- if (startupChangelog.persistCurrentVersion) {
674
- await writeLastChangelogVersion(VERSION);
675
- }
676
- if (startupChangelog.markdown) {
677
- return startupChangelog.markdown;
678
- }
679
-
680
- return undefined;
683
+ return resolveStartupChangelogForDisplay({
684
+ mode,
685
+ currentVersion: VERSION,
686
+ changelogPath: getChangelogPath(),
687
+ });
681
688
  }
682
689
 
683
690
  const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -852,6 +859,7 @@ export async function buildSessionOptions(
852
859
  cwd: parsed.cwd ?? getProjectDir(),
853
860
  autoApprove: parsed.autoApprove ?? false,
854
861
  };
862
+ const restoringSession = Boolean(parsed.continue || parsed.resume || isForeignSessionImport(parsed));
855
863
  const cliDirs = parsed.addDir ?? [];
856
864
  const settingsDirs = activeSettings.get("workspace.additionalDirectories");
857
865
  if (cliDirs.length > 0 || settingsDirs.length > 0) {
@@ -882,7 +890,7 @@ export async function buildSessionOptions(
882
890
  options.providerPromptCacheKeySource = "explicit";
883
891
  } else {
884
892
  const header = sessionManager?.getHeader();
885
- const scopedModelOverride = scopedModels.length > 0 && !parsed.continue && !parsed.resume;
893
+ const scopedModelOverride = scopedModels.length > 0 && !restoringSession;
886
894
  const forkCacheShapeChanged =
887
895
  scopedModelOverride ||
888
896
  parsed.model !== undefined ||
@@ -939,7 +947,7 @@ export async function buildSessionOptions(
939
947
  options.thinkingLevel = resolved.thinkingLevel;
940
948
  }
941
949
  }
942
- } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
950
+ } else if (scopedModels.length > 0 && !restoringSession) {
943
951
  const remembered = activeSettings.getModelRole("default");
944
952
  if (remembered) {
945
953
  const rememberedSpec = resolveModelRoleValue(
@@ -1042,8 +1050,7 @@ export async function buildSessionOptions(
1042
1050
  // thinking suffix) after extensions register; seeding the fallback
1043
1051
  // scoped model's level here would override it in createAgentSession.
1044
1052
  !deferredDefaultRole &&
1045
- !parsed.continue &&
1046
- !parsed.resume
1053
+ !restoringSession
1047
1054
  ) {
1048
1055
  options.thinkingLevel = scopedModels[0].thinkingLevel;
1049
1056
  }
@@ -1119,6 +1126,7 @@ interface RunRootCommandDependencies {
1119
1126
  discoverAuthStorage?: typeof discoverAuthStorage;
1120
1127
  selectSession?: typeof selectSession;
1121
1128
  runAcpMode?: RunAcpMode;
1129
+ createForeignSessionStore?: (source: ForeignSessionSource) => ForeignSessionStore;
1122
1130
  settings?: Settings;
1123
1131
  forceSetupWizard?: boolean;
1124
1132
  }
@@ -1282,19 +1290,77 @@ export async function runRootCommand(
1282
1290
  // id from UUID-shaped values owned by later extension flags.
1283
1291
  normalizeContinueSessionArgs(parsedArgs, rawArgs);
1284
1292
 
1285
- // Create session manager based on CLI flags. SessionResolutionError signals a
1286
- // user-facing failure (unknown --resume/--fork id, non-interactive fork
1287
- // prompt, --fork with --no-session): print + exit cleanly instead of letting
1288
- // it surface as `[Uncaught Exception]` (see issue #2084).
1293
+ // Resolve native resume/fork flags or import one foreign transcript into a
1294
+ // fresh persisted OMP session before constructing the AgentSession.
1289
1295
  let sessionManager: SessionManager | undefined;
1296
+ let foreignSource: ForeignSessionSource | undefined;
1290
1297
  try {
1291
- sessionManager = await logger.time(
1292
- "createSessionManager",
1293
- createSessionManager,
1294
- parsedArgs,
1295
- cwd,
1296
- settingsInstance,
1297
- );
1298
+ foreignSource = resolveForeignSessionSource(parsedArgs);
1299
+ if (foreignSource) {
1300
+ if (isProtocolMode) {
1301
+ throw new SessionResolutionError(`--from-${foreignSource} is not supported in ${mode} mode`);
1302
+ }
1303
+ const sourceName = foreignSessionSourceName(foreignSource);
1304
+ const store = (deps.createForeignSessionStore ?? createForeignSessionStore)(foreignSource);
1305
+ let foreignSessions: ForeignSessionInfo[];
1306
+ try {
1307
+ foreignSessions = await logger.time(`list${sourceName}Sessions`, () => store.list());
1308
+ } catch (error) {
1309
+ const message = error instanceof Error ? error.message : String(error);
1310
+ throw new SessionResolutionError(`Failed to list ${sourceName} sessions: ${message}`);
1311
+ }
1312
+ if (foreignSessions.length === 0) {
1313
+ writeStartupNotice(parsedArgs, `${chalk.dim(`No ${sourceName} sessions found`)}\n`);
1314
+ stopStartupWatchdog();
1315
+ process.exit(0);
1316
+ }
1317
+ const choices = foreignSessions.map(foreignSessionInfoToSessionInfo);
1318
+ pauseStartupWatchdog();
1319
+ let selected: SessionInfo | null;
1320
+ try {
1321
+ selected = await logger.time(`select${sourceName}Session`, deps.selectSession ?? selectSession, choices, {
1322
+ title: `Import ${sourceName} Session`,
1323
+ scopeLabel: false,
1324
+ showCwd: true,
1325
+ allowDelete: false,
1326
+ allowGlobalScope: false,
1327
+ historySearch: false,
1328
+ });
1329
+ } finally {
1330
+ resumeStartupWatchdog();
1331
+ }
1332
+ if (!selected) {
1333
+ writeStartupNotice(parsedArgs, `${chalk.dim(`No ${sourceName} session selected`)}\n`);
1334
+ stopStartupWatchdog();
1335
+ process.exit(0);
1336
+ }
1337
+ const foreignSession = foreignSessions.find(
1338
+ session => session.id === selected.id && session.path === selected.path,
1339
+ );
1340
+ if (!foreignSession) {
1341
+ throw new SessionResolutionError(`Selected ${sourceName} session is no longer available`);
1342
+ }
1343
+ try {
1344
+ sessionManager = await logger.time(
1345
+ `import${sourceName}Session`,
1346
+ persistForeignSession,
1347
+ store,
1348
+ foreignSession,
1349
+ { fallbackCwd: cwd, sessionDir: parsedArgs.sessionDir },
1350
+ );
1351
+ } catch (error) {
1352
+ const message = error instanceof Error ? error.message : String(error);
1353
+ throw new SessionResolutionError(`Failed to import ${sourceName} session: ${message}`);
1354
+ }
1355
+ } else {
1356
+ sessionManager = await logger.time(
1357
+ "createSessionManager",
1358
+ createSessionManager,
1359
+ parsedArgs,
1360
+ cwd,
1361
+ settingsInstance,
1362
+ );
1363
+ }
1298
1364
  } catch (error: unknown) {
1299
1365
  if (error instanceof SessionResolutionError) {
1300
1366
  process.stderr.write(`${chalk.red(`Error: ${error.message}`)}\n`);
@@ -1306,7 +1372,7 @@ export async function runRootCommand(
1306
1372
  throw error;
1307
1373
  }
1308
1374
 
1309
- if (typeof parsedArgs.resume === "string" && sessionManager) {
1375
+ if ((typeof parsedArgs.resume === "string" || foreignSource) && sessionManager) {
1310
1376
  const previousCwd = cwd;
1311
1377
  cwd = await switchToResumedProject(sessionManager.getCwd(), settingsInstance, pluginPreloadPromise);
1312
1378
  if (cwd !== previousCwd) {
@@ -1372,7 +1438,7 @@ export async function runRootCommand(
1372
1438
  sessionManager = await SessionManager.open(selected.path);
1373
1439
  }
1374
1440
 
1375
- if (sessionManager && (parsedArgs.continue || parsedArgs.resume || parsedArgs.fork)) {
1441
+ if (sessionManager && (parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource)) {
1376
1442
  const pendingToolWarning = describePendingToolCalls(sessionManager.getBranch());
1377
1443
  if (pendingToolWarning) {
1378
1444
  logger.warn("Resumed session has pending tool calls", {
@@ -1515,7 +1581,7 @@ export async function runRootCommand(
1515
1581
  const showStartupSplash = shouldShowStartupSplash({
1516
1582
  configured: settingsInstance.get("startup.showSplash"),
1517
1583
  isInteractive,
1518
- resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1584
+ resuming: Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource),
1519
1585
  quiet: settingsInstance.get("startup.quiet"),
1520
1586
  timing: Boolean($env.PI_TIMING),
1521
1587
  stdinIsTTY: process.stdin.isTTY,
@@ -1580,7 +1646,12 @@ export async function runRootCommand(
1580
1646
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
1581
1647
  } else if (isInteractive) {
1582
1648
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1583
- const changelogMarkdown = await logger.time("main:getChangelogForDisplay", getChangelogForDisplay, parsedArgs);
1649
+ const startupChangelog = await logger.time(
1650
+ "main:getChangelogForDisplay",
1651
+ getChangelogForDisplay,
1652
+ parsedArgs,
1653
+ settingsInstance.get("startup.changelogMode"),
1654
+ );
1584
1655
 
1585
1656
  const modelScopeNotification = buildModelScopeNotification(
1586
1657
  scopedModels,
@@ -1605,14 +1676,14 @@ export async function runRootCommand(
1605
1676
  await runInteractiveMode(
1606
1677
  session,
1607
1678
  VERSION,
1608
- changelogMarkdown,
1679
+ startupChangelog,
1609
1680
  notifs,
1610
1681
  versionCheckPromise,
1611
1682
  initialArgs.messages,
1612
1683
  setToolUIContext,
1613
1684
  lspServers,
1614
1685
  mcpManager,
1615
- Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork),
1686
+ Boolean(parsedArgs.continue || parsedArgs.resume || parsedArgs.fork || foreignSource),
1616
1687
  deps.forceSetupWizard === true,
1617
1688
  showStartupSplash,
1618
1689
  eventBus,
@@ -94,6 +94,14 @@ const STARTUP_TIMEOUT_MS = 250;
94
94
  const RECONNECT_BURST_WINDOW_MS = 30_000;
95
95
  const RECONNECT_BURST_LIMIT = 5;
96
96
 
97
+ /**
98
+ * Bounded buffer for notifications received before any listener attaches.
99
+ * Mirrors {@link IrcBus}'s `MAILBOX_CAP` — drop-oldest on overflow. Drained
100
+ * into the first {@link MCPManager.addNotificationListener} subscriber, then
101
+ * cleared; subsequent frames deliver directly to attached listeners.
102
+ */
103
+ const NOTIFICATION_BUFFER_CAP = 100;
104
+
97
105
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
98
106
  const tracked: TrackedPromise<T> = { promise, status: "pending" };
99
107
  promise.then(
@@ -194,8 +202,14 @@ export class MCPManager {
194
202
  #sources = new Map<string, SourceMeta>();
195
203
  #authStorage: AuthStorage | null = null;
196
204
  #authHandler?: MCPAuthHandler;
197
- #onNotification?: (serverName: string, method: string, params: unknown) => void;
198
- #onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void;
205
+ #notificationListeners = new Set<(serverName: string, method: string, params: unknown) => void>();
206
+ /**
207
+ * Notifications received before any listener attached, to be drained on
208
+ * the first {@link addNotificationListener} call. Bounded by
209
+ * {@link NOTIFICATION_BUFFER_CAP}, drop-oldest on overflow.
210
+ */
211
+ #pendingNotifications: Array<{ server: string; method: string; params: unknown }> = [];
212
+ #onToolsChanged?: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void | Promise<void>;
199
213
  #onResourcesChanged?: (serverName: string, uri: string) => void;
200
214
  #onPromptsChanged?: (serverName: string) => void;
201
215
  #notificationsEnabled = false;
@@ -219,16 +233,66 @@ export class MCPManager {
219
233
  ) {}
220
234
 
221
235
  /**
222
- * Set a callback to receive all server notifications.
236
+ * Register a listener for server-initiated MCP notifications.
237
+ *
238
+ * The listener is called for every JSON-RPC notification received from any
239
+ * connected server, AFTER the manager's own handling of known methods
240
+ * (`notifications/tools/list_changed`, `notifications/resources/list_changed`,
241
+ * `notifications/resources/updated`, `notifications/prompts/list_changed`).
242
+ * For list-change methods the internal refresh promise is awaited before
243
+ * fanout, so listeners observe up-to-date manager and tool state. Unknown
244
+ * or server-custom methods are also delivered, letting consumers bridge
245
+ * server-initiated events into session-level behavior (e.g. an extension
246
+ * injecting a steer via `pi.sendMessage`).
247
+ *
248
+ * Notifications received before any listener attached are buffered
249
+ * (bounded FIFO, cap {@link NOTIFICATION_BUFFER_CAP}, drop-oldest) and
250
+ * drained into the first subscriber — matches {@link setOnPromptsChanged}'s
251
+ * replay-on-attach and {@link IrcBus}'s mailbox semantics.
252
+ *
253
+ * Returns an unsubscribe function; call it to remove the listener.
254
+ *
255
+ * Multiple listeners are allowed; each is invoked with independent error
256
+ * isolation — a listener that throws does not prevent other listeners from
257
+ * firing.
223
258
  */
224
- setOnNotification(handler: (serverName: string, method: string, params: unknown) => void): void {
225
- this.#onNotification = handler;
259
+ addNotificationListener(listener: (serverName: string, method: string, params: unknown) => void): () => void {
260
+ const wasEmpty = this.#notificationListeners.size === 0;
261
+ this.#notificationListeners.add(listener);
262
+
263
+ // Drain startup-buffered notifications into the first attaching listener.
264
+ if (wasEmpty && this.#pendingNotifications.length > 0) {
265
+ const pending = this.#pendingNotifications.splice(0);
266
+ for (const frame of pending) {
267
+ try {
268
+ listener(frame.server, frame.method, frame.params);
269
+ } catch (error) {
270
+ logger.debug("MCP notification listener threw during buffered drain", {
271
+ path: `mcp:${frame.server}`,
272
+ method: frame.method,
273
+ error,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ return () => {
280
+ this.#notificationListeners.delete(listener);
281
+ };
226
282
  }
227
283
 
228
284
  /**
229
285
  * Set a callback to fire when any server's tools change.
286
+ *
287
+ * May return a Promise; if so, {@link refreshServerTools} awaits it so that
288
+ * downstream consumers (e.g. `mcp_notification` listeners for
289
+ * `notifications/tools/list_changed`) observe not just the manager's
290
+ * refreshed tool set but also any session-level rebind driven by the
291
+ * handler (`session.refreshMCPTools`). Other callsites (initial connect,
292
+ * disconnect, reconnect) invoke the handler synchronously — their downstream
293
+ * chains don't need to serialize on the rebind.
230
294
  */
231
- setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void): void {
295
+ setOnToolsChanged(handler: (tools: CustomTool<TSchema, MCPToolDetails>[]) => void | Promise<void>): void {
232
296
  this.#onToolsChanged = handler;
233
297
  }
234
298
 
@@ -488,7 +552,7 @@ export class MCPManager {
488
552
  this.reconnectServer(name, options);
489
553
  const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
490
554
  this.#replaceServerTools(name, customTools);
491
- this.#onToolsChanged?.(this.#tools);
555
+ void this.#onToolsChanged?.(this.#tools);
492
556
  void this.toolCache?.set(name, config, serverTools);
493
557
 
494
558
  onStatus?.({ type: "connected", serverName: name });
@@ -597,7 +661,7 @@ export class MCPManager {
597
661
  sortMCPToolsByName(this.#tools);
598
662
  }
599
663
 
600
- #triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): void {
664
+ #triggerNotificationRefresh(serverName: string, kind: "tools" | "resources" | "prompts"): Promise<void> {
601
665
  const refresh = (() => {
602
666
  switch (kind) {
603
667
  case "tools":
@@ -608,22 +672,33 @@ export class MCPManager {
608
672
  return this.refreshServerPrompts(serverName);
609
673
  }
610
674
  })();
611
- void refresh.catch(error => {
675
+ return refresh.catch(error => {
612
676
  logger.debug("Failed MCP notification refresh", { path: `mcp:${serverName}`, kind, error });
613
677
  });
614
678
  }
615
- #handleServerNotification(serverName: string, method: string, params: unknown): void {
679
+ async #handleServerNotification(serverName: string, method: string, params: unknown): Promise<void> {
616
680
  logger.debug("MCP notification received", { path: `mcp:${serverName}`, method });
617
681
 
682
+ // Only trigger refresh if the connection is already stored — during the
683
+ // initial connect handshake, notifications may arrive before
684
+ // `#connections.set()` completes, and `refreshServer*` would no-op
685
+ // anyway. Skipping the await in that case preserves arrival order
686
+ // across concurrently-dispatched notifications (an awaited refresh,
687
+ // even a no-op, yields a microtask that lets later frames overtake).
688
+ const connectionKnown = this.#connections.has(serverName);
689
+ let refreshPromise: Promise<void> | undefined;
618
690
  switch (method) {
619
691
  case MCPNotificationMethods.TOOLS_LIST_CHANGED:
620
- this.#triggerNotificationRefresh(serverName, "tools");
692
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "tools");
621
693
  break;
622
694
  case MCPNotificationMethods.RESOURCES_LIST_CHANGED:
623
- this.#triggerNotificationRefresh(serverName, "resources");
695
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "resources");
624
696
  break;
625
697
  case MCPNotificationMethods.RESOURCES_UPDATED: {
626
- const uri = (params as { uri?: string })?.uri;
698
+ const uri =
699
+ params && typeof params === "object" && "uri" in params && typeof params.uri === "string"
700
+ ? params.uri
701
+ : undefined;
627
702
  const subscribed = this.#subscribedResources.get(serverName);
628
703
  if (uri && subscribed?.has(uri)) {
629
704
  this.#onResourcesChanged?.(serverName, uri);
@@ -631,13 +706,40 @@ export class MCPManager {
631
706
  break;
632
707
  }
633
708
  case MCPNotificationMethods.PROMPTS_LIST_CHANGED:
634
- this.#triggerNotificationRefresh(serverName, "prompts");
709
+ if (connectionKnown) refreshPromise = this.#triggerNotificationRefresh(serverName, "prompts");
635
710
  break;
636
711
  default:
637
712
  break;
638
713
  }
639
714
 
640
- this.#onNotification?.(serverName, method, params);
715
+ // Await internal refresh so listeners see the manager's post-refresh
716
+ // state (satisfies the documented "AFTER the manager's own handling"
717
+ // contract on `addNotificationListener` — otherwise an extension acting
718
+ // on `tools/list_changed` could hit stale `getTools()`).
719
+ if (refreshPromise) {
720
+ await refreshPromise;
721
+ }
722
+
723
+ // Buffer for late-attaching subscribers when no listener exists yet.
724
+ if (this.#notificationListeners.size === 0) {
725
+ this.#pendingNotifications.push({ server: serverName, method, params });
726
+ if (this.#pendingNotifications.length > NOTIFICATION_BUFFER_CAP) {
727
+ this.#pendingNotifications.shift();
728
+ }
729
+ return;
730
+ }
731
+
732
+ for (const listener of this.#notificationListeners) {
733
+ try {
734
+ listener(serverName, method, params);
735
+ } catch (error) {
736
+ logger.debug("MCP notification listener threw", {
737
+ path: `mcp:${serverName}`,
738
+ method,
739
+ error,
740
+ });
741
+ }
742
+ }
641
743
  }
642
744
 
643
745
  /** Handle server-to-client JSON-RPC requests (e.g. ping, roots/list). */
@@ -781,7 +883,7 @@ export class MCPManager {
781
883
  // Remove tools from this server and notify consumers
782
884
  const hadTools = this.#tools.some(t => t.mcpServerName === name);
783
885
  this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
784
- if (hadTools) this.#onToolsChanged?.(this.#tools);
886
+ if (hadTools) void this.#onToolsChanged?.(this.#tools);
785
887
 
786
888
  // Notify prompt consumers so stale commands are cleared
787
889
  if (connection?.prompts?.length) this.#onPromptsChanged?.(name);
@@ -1022,7 +1124,7 @@ export class MCPManager {
1022
1124
  const customTools = MCPTool.fromTools(connection, serverTools, reconnect);
1023
1125
  void this.toolCache?.set(name, config, serverTools);
1024
1126
  this.#replaceServerTools(name, customTools);
1025
- this.#onToolsChanged?.(this.#tools);
1127
+ void this.#onToolsChanged?.(this.#tools);
1026
1128
  void this.#loadServerResourcesAndPrompts(name, connection);
1027
1129
  return connection;
1028
1130
  } catch (error) {
@@ -1075,7 +1177,7 @@ export class MCPManager {
1075
1177
 
1076
1178
  // Replace tools from this server
1077
1179
  this.#replaceServerTools(name, customTools);
1078
- this.#onToolsChanged?.(this.#tools);
1180
+ await this.#onToolsChanged?.(this.#tools);
1079
1181
  }
1080
1182
 
1081
1183
  /**
@@ -651,12 +651,17 @@ export class AcpAgent implements Agent {
651
651
  });
652
652
  }
653
653
 
654
- // For `thinking` the lifetime subscription pushes post-bootstrap; only
655
- // push here when it's not yet installed so pre-bootstrap callers still
656
- // see the change without a post-bootstrap duplicate.
657
- const thinkingHandledBySubscription =
658
- params.configId === THINKING_CONFIG_ID && record.lifetimeUnsubscribe !== undefined;
659
- if (!thinkingHandledBySubscription) {
654
+ // For `model`/`thinking`, `#setModelById`/`#setThinkingLevelById` change
655
+ // the session model/thinking level through AgentSession, which now emits
656
+ // a lifetime event (`model_changed`/`thinking_level_changed`) that
657
+ // `#handleLifetimeEvent` turns into a push once the subscription is
658
+ // installed. Only push here when that subscription is not yet
659
+ // installed, so pre-bootstrap callers still see the change without a
660
+ // post-bootstrap duplicate.
661
+ const handledBySubscription =
662
+ (params.configId === THINKING_CONFIG_ID || params.configId === MODEL_CONFIG_ID) &&
663
+ record.lifetimeUnsubscribe !== undefined;
664
+ if (!handledBySubscription) {
660
665
  await this.#pushConfigOptionUpdate(record);
661
666
  }
662
667
  return { configOptions: this.#buildConfigOptions(record.session) };
@@ -1150,14 +1155,15 @@ export class AcpAgent implements Agent {
1150
1155
  }
1151
1156
 
1152
1157
  async #handleLifetimeEvent(record: ManagedSessionRecord, event: AgentSessionEvent): Promise<void> {
1153
- if (event.type !== "thinking_level_changed") {
1158
+ if (event.type !== "thinking_level_changed" && event.type !== "model_changed") {
1154
1159
  return;
1155
1160
  }
1156
1161
  try {
1157
1162
  await this.#pushConfigOptionUpdate(record);
1158
1163
  } catch (error) {
1159
- logger.warn("Failed to push thinking-level config_option_update", {
1164
+ logger.warn("Failed to push config_option_update after a lifetime event", {
1160
1165
  sessionId: record.session.sessionId,
1166
+ eventType: event.type,
1161
1167
  error,
1162
1168
  });
1163
1169
  }
@@ -1575,7 +1581,7 @@ export class AcpAgent implements Agent {
1575
1581
  #buildThinkingOptions(session: AgentSession): Array<{ value: string; name: string; description?: string }> {
1576
1582
  return [
1577
1583
  { value: THINKING_OFF, name: "Off" },
1578
- { value: AUTO_THINKING, name: "Auto", description: "Auto-detect per prompt (low–xhigh)" },
1584
+ { value: AUTO_THINKING, name: "Auto", description: "Auto-detect per prompt" },
1579
1585
  ...session.getAvailableThinkingLevels().map(level => ({
1580
1586
  value: level,
1581
1587
  name: level,