@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.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 (180) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-be1f2t8h.md} +92 -0
  3. package/dist/cli.js +9986 -9776
  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/settings-schema.d.ts +63 -8
  13. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  14. package/dist/types/cursor.d.ts +193 -8
  15. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  16. package/dist/types/edit/hashline/diff.d.ts +9 -1
  17. package/dist/types/edit/index.d.ts +8 -1
  18. package/dist/types/edit/streaming.d.ts +8 -1
  19. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  20. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  21. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  22. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  23. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  24. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  25. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  26. package/dist/types/lsp/client.d.ts +2 -0
  27. package/dist/types/main.d.ts +2 -0
  28. package/dist/types/mcp/manager.d.ts +33 -4
  29. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  30. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  31. package/dist/types/modes/components/session-selector.d.ts +6 -0
  32. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  33. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  34. package/dist/types/modes/components/usage-row.d.ts +2 -0
  35. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  36. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  37. package/dist/types/modes/interactive-mode.d.ts +4 -2
  38. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  39. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  40. package/dist/types/modes/theme/theme.d.ts +12 -1
  41. package/dist/types/modes/types.d.ts +2 -1
  42. package/dist/types/sdk.d.ts +1 -0
  43. package/dist/types/session/agent-session-events.d.ts +2 -0
  44. package/dist/types/session/agent-session-types.d.ts +36 -1
  45. package/dist/types/session/agent-session.d.ts +4 -2
  46. package/dist/types/session/claude-session-store.d.ts +13 -0
  47. package/dist/types/session/codex-session-store.d.ts +14 -0
  48. package/dist/types/session/credential-pin.d.ts +58 -0
  49. package/dist/types/session/foreign-session-import.d.ts +15 -0
  50. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  51. package/dist/types/session/foreign-session-store.d.ts +23 -0
  52. package/dist/types/session/session-advisors.d.ts +47 -2
  53. package/dist/types/session/session-entries.d.ts +19 -1
  54. package/dist/types/session/session-handoff.d.ts +4 -1
  55. package/dist/types/session/session-loader.d.ts +2 -0
  56. package/dist/types/session/session-maintenance.d.ts +1 -1
  57. package/dist/types/session/session-manager.d.ts +21 -0
  58. package/dist/types/session/session-tools.d.ts +25 -1
  59. package/dist/types/session/turn-recovery.d.ts +10 -4
  60. package/dist/types/system-prompt.d.ts +2 -1
  61. package/dist/types/task/executor.d.ts +11 -2
  62. package/dist/types/task/render.d.ts +7 -0
  63. package/dist/types/thinking.d.ts +18 -6
  64. package/dist/types/tools/acp-bridge.d.ts +28 -5
  65. package/dist/types/tools/grep.d.ts +17 -1
  66. package/dist/types/tools/hub/launch.d.ts +5 -1
  67. package/dist/types/tools/index.d.ts +4 -1
  68. package/dist/types/tools/path-utils.d.ts +19 -0
  69. package/dist/types/tools/read.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +2 -1
  71. package/dist/types/utils/changelog.d.ts +19 -0
  72. package/dist/types/utils/git.d.ts +10 -0
  73. package/dist/types/utils/jj.d.ts +11 -4
  74. package/dist/types/web/search/types.d.ts +2 -2
  75. package/package.json +12 -12
  76. package/src/advisor/runtime.ts +138 -12
  77. package/src/advisor/transcript-recorder.ts +56 -0
  78. package/src/auto-thinking/classifier.ts +48 -10
  79. package/src/cli/args.ts +6 -0
  80. package/src/cli/flag-tables.ts +2 -0
  81. package/src/cli/gallery-fixtures/fs.ts +25 -0
  82. package/src/cli/session-picker.ts +34 -17
  83. package/src/cli/ttsr-cli.ts +19 -1
  84. package/src/cli.ts +9 -0
  85. package/src/collab/guest.ts +37 -4
  86. package/src/collab/host.ts +1 -0
  87. package/src/commands/launch.ts +6 -0
  88. package/src/config/model-registry.ts +103 -25
  89. package/src/config/settings-schema.ts +65 -6
  90. package/src/config/settings.ts +25 -0
  91. package/src/cursor-bridge-tools.ts +81 -0
  92. package/src/cursor.ts +463 -12
  93. package/src/edit/edit-clipboard.ts +23 -0
  94. package/src/edit/hashline/diff.ts +49 -10
  95. package/src/edit/hashline/execute.ts +38 -6
  96. package/src/edit/hashline/filesystem.ts +27 -3
  97. package/src/edit/index.ts +12 -2
  98. package/src/edit/renderer.ts +4 -4
  99. package/src/edit/streaming.ts +17 -2
  100. package/src/export/html/template.js +1 -1
  101. package/src/extensibility/extensions/runner.ts +70 -0
  102. package/src/extensibility/extensions/types.ts +30 -0
  103. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  104. package/src/internal-urls/local-protocol.ts +3 -2
  105. package/src/internal-urls/mcp-protocol.ts +4 -1
  106. package/src/launch/terminal-output-worker-client.ts +53 -0
  107. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  108. package/src/launch/terminal-output-worker.ts +23 -0
  109. package/src/lsp/client.ts +18 -3
  110. package/src/lsp/index.ts +4 -0
  111. package/src/lsp/render.ts +1 -1
  112. package/src/main.ts +127 -56
  113. package/src/mcp/manager.ts +120 -18
  114. package/src/modes/acp/acp-agent.ts +15 -9
  115. package/src/modes/components/chat-transcript-builder.ts +30 -11
  116. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  117. package/src/modes/components/read-tool-group.ts +159 -17
  118. package/src/modes/components/session-selector.ts +19 -3
  119. package/src/modes/components/status-line/component.ts +381 -54
  120. package/src/modes/components/tool-execution.ts +78 -31
  121. package/src/modes/components/usage-row.ts +10 -5
  122. package/src/modes/controllers/event-controller.ts +26 -4
  123. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  124. package/src/modes/controllers/selector-controller.ts +94 -36
  125. package/src/modes/controllers/tan-command-controller.ts +14 -0
  126. package/src/modes/interactive-mode.ts +31 -17
  127. package/src/modes/rpc/rpc-client.ts +18 -0
  128. package/src/modes/rpc/rpc-mode.ts +16 -1
  129. package/src/modes/rpc/rpc-types.ts +11 -0
  130. package/src/modes/theme/theme.ts +151 -6
  131. package/src/modes/types.ts +2 -1
  132. package/src/modes/utils/ui-helpers.ts +38 -21
  133. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  134. package/src/prompts/system/plan-mode-active.md +2 -2
  135. package/src/prompts/system/system-prompt.md +3 -0
  136. package/src/prompts/system/xdev-mount-notice.md +1 -1
  137. package/src/prompts/tools/bash.md +15 -17
  138. package/src/prompts/tools/checkpoint.md +1 -1
  139. package/src/prompts/tools/glob.md +7 -6
  140. package/src/prompts/tools/grep.md +7 -6
  141. package/src/sdk.ts +178 -15
  142. package/src/session/agent-session-events.ts +1 -0
  143. package/src/session/agent-session-types.ts +40 -1
  144. package/src/session/agent-session.ts +302 -175
  145. package/src/session/claude-session-store.ts +426 -0
  146. package/src/session/codex-session-store.ts +673 -0
  147. package/src/session/credential-pin.ts +93 -0
  148. package/src/session/foreign-session-import.ts +52 -0
  149. package/src/session/foreign-session-jsonl.ts +29 -0
  150. package/src/session/foreign-session-store.ts +26 -0
  151. package/src/session/model-controls.ts +11 -3
  152. package/src/session/session-advisors.ts +140 -16
  153. package/src/session/session-entries.ts +21 -1
  154. package/src/session/session-handoff.ts +20 -3
  155. package/src/session/session-loader.ts +25 -9
  156. package/src/session/session-maintenance.ts +59 -11
  157. package/src/session/session-manager.ts +61 -0
  158. package/src/session/session-tools.ts +107 -5
  159. package/src/session/settings-stream-fn.ts +1 -0
  160. package/src/session/turn-recovery.ts +97 -51
  161. package/src/slash-commands/builtin-registry.ts +131 -3
  162. package/src/system-prompt.ts +3 -2
  163. package/src/task/executor.ts +17 -4
  164. package/src/task/render.ts +20 -4
  165. package/src/thinking.ts +29 -10
  166. package/src/tools/acp-bridge.ts +52 -8
  167. package/src/tools/browser.ts +5 -0
  168. package/src/tools/checkpoint.ts +0 -13
  169. package/src/tools/grep.ts +64 -8
  170. package/src/tools/hub/launch.ts +10 -15
  171. package/src/tools/index.ts +28 -4
  172. package/src/tools/path-utils.ts +88 -0
  173. package/src/tools/read.ts +3 -0
  174. package/src/tools/write.ts +9 -3
  175. package/src/tools/xdev.ts +39 -14
  176. package/src/utils/changelog.ts +144 -4
  177. package/src/utils/git.ts +61 -14
  178. package/src/utils/jj.ts +19 -13
  179. package/src/web/search/providers/codex.ts +34 -0
  180. package/src/web/search/types.ts +1 -1
@@ -1,25 +1,23 @@
1
- Runs commands in a persistent shell session.
1
+ Runs commands in a persistent shell.
2
2
 
3
- Use ONLY for: single binary call or short pipeline that COMPUTES a fact (`wc -l`, `sort | uniq -c`, `comm`, `diff`).
4
- {{#if hasLaunch}}Services, watchers, debuggers, REPLs → `hub` (`op:"start"`).{{/if}}
5
- {{#if hasEval}}Inline scripts, heredocs, shell control flow, `$(…)`, multi-stage pipelines, `&&`-chains, quote/JSON escaping → `eval` cells.{{else}}Inline scripts, heredocs, shell control flow, `$(…)`, multi-stage pipelines, `&&`-chains → purpose-built tool or checked-in script.{{/if}}
3
+ Use ONLY for one binary or a short pipeline that computes a fact (`wc -l`, `sort | uniq -c`, `diff`).
4
+ {{#if hasEval}}Inline scripts, heredocs, `$(…)`, complex control flow/quoting, and non-trivial pipelines → `eval`.{{else}}Inline scripts, heredocs, `$()`, and complex control flow → a purpose-built tool or checked-in script.{{/if}}
6
5
 
7
6
  <instruction>
8
- - `cwd` sets working dir (not `cd dir && …`). `env: { NAME: "…" }` for multiline/quote-heavy values; `"$NAME"` to expand.
9
- - `pty: true` only for real terminal needs (`sudo`, `ssh`); default `false`.
10
- - Multiple calls run concurrently; NEVER split order-dependent commands chain with `&&` in one call (`;` only to continue past failure).
11
- - Internal URIs (`skill://`, `agent://`, …) auto-resolve to FS paths.
12
- {{#if hasShellBuiltins}}- aux utils available: mkdir, head, tail, wc, sort, ls, find, grep, rg, fd, cat, uniq, base64, cmp, md5sum, sha{1,224,256,384,512}sum, b2sum, basename, dirname, readlink, realpath, touch, stat, date, mktemp, seq, yes, printenv, truncate, tac, nproc, uname, whoami, hostname, which, diff, cut, tee, tr, paste, comm, sed, xargs, jq, rm, mv, ln, ts, sponge, ifne, isutf8, combine{{#unless isWindows}}, errno{{/unless}}{{/if}}
13
- {{#if asyncEnabled}}- `async: true` defers reporting for finite commands needing no later input.{{/if}}
7
+ - Set `cwd` instead of `cd`; use `env: { NAME: "…" }` for multiline/quote-heavy values.
8
+ - `pty: true` only for terminal interaction (`sudo`, `ssh`).
9
+ - Order-dependent commands use `&&` in one call; independent calls may run concurrently.
10
+ - Internal URIs (`skill://`, `agent://`, …) auto-resolve to paths.
11
+ {{#if hasShellBuiltins}}- aux utils available: mkdir, wc, sort, comm, diff, uniq, base64, cmp, md5sum, sha{1,224,256,384,512}sum, b2sum, basename, dirname, readlink, realpath, touch, stat, date, mktemp, seq, yes, printenv, truncate, tac, nproc, uname, whoami, hostname, which, cut, tee, tr, paste, sed, xargs, jq, rm, mv, ln, ts, sponge, ifne, isutf8, combine{{#unless isWindows}}, errno{{/unless}}{{/if}}
12
+ {{#if asyncEnabled}}- `async: true` defers a finite command's result; it does not extend `timeout`.{{/if}}
14
13
  </instruction>
15
14
 
16
15
  <critical>
17
- {{#if hasGrep}}- NEVER shell out to search: `grep`/`rg` built-in `grep`.{{/if}}
18
- {{#if hasRead}}{{#if hasGlob}}- NEVER use `ls` or `find` `ls` `read`, `find` → `glob`. NON-NEGOTIABLE.{{/if}}{{/if}}
19
- - Avoid head/tail/redirections: stderr merged, output auto-truncated, full capture at `artifact://<id>`.
20
- {{#if hasLaunch}}- NEVER launch daemons/watchers/servers/debuggers/REPLs through bash use `hub` (`op:"start"`).{{/if}}
16
+ {{#if hasGrep}}- NEVER use shell `grep`/`rg`; use built-in `grep`.{{/if}}
17
+ {{#if hasRead}}{{#if hasGlob}}- List directories with `read` and find paths with `glob`; NEVER use `ls`/`find`.{{/if}}{{/if}}
18
+ - Avoid `head`, `tail`, and redirection: output is captured, truncated, and linked as `artifact://<id>`.
19
+ {{#if hasLaunch}}- Services, watchers, debuggers, and REPLs MUST use `hub` (`op:"start"`).{{/if}}
21
20
  </critical>
22
21
 
23
- {{#if asyncEnabled}}- `timeout`: nonzero clamped 1–3600, killed on elapse. `async: true` defers reporting only, doesn't extend timeout.{{/if}}
24
- {{#if autoBackgroundEnabled}}- Long foreground calls may auto-background; result arrives as follow-up — NOT a failure. Need inline? Raise timeout{{#if asyncEnabled}} or `async: true`{{/if}}.{{/if}}
25
- - Long output truncated, test/lint filtered to failures. Footer links full capture. No footer = what you see is exact output.
22
+ {{#if autoBackgroundEnabled}}Long foreground calls may auto-background and deliver later. Need inline? Raise `timeout`.{{/if}}
23
+ No truncation footer means the displayed output is complete.
@@ -5,7 +5,7 @@ Use this when you need to investigate with many intermediate tool calls (read/gr
5
5
  Rules:
6
6
  - You MUST call `rewind` before yielding after starting a checkpoint.
7
7
  - You NEVER call `checkpoint` while another checkpoint is active.
8
- - Not available in subagents.
8
+ - Disabled by default in subagents. To enable, list `checkpoint` or `rewind` in the agent definition's `tools:` frontmatter (the sister tool is auto-included; requires `checkpoint.enabled` setting).
9
9
 
10
10
  Typical flow:
11
11
  1. `checkpoint(goal: …)`
@@ -1,15 +1,16 @@
1
- Globs files and directories via fast pattern matching, any codebase size.
1
+ Globs files, directories, and path-backed internal URLs with fast pattern matching.
2
2
 
3
3
  <instruction>
4
- - `path`: a glob, file, or directory. Search several at once by passing a semicolon-delimited list (`src/**/*.ts; test/**/*.ts`).
5
- - `gitignore` (default `true`) hides `.gitignore` matches. Set `gitignore: false` to find `.env*`, `*.log`, fresh build outputs, or anything your repo ignores.
6
- - `hidden` (default `true`); combine with `gitignore: false` to surface dotfiles also gitignored.
4
+ - `path`: glob, file, directory, or path-backed internal URL; separate targets with `;` (`src/**/*.ts; test/**/*.ts`).
5
+ - `memory://` glob patterns are supported. `ssh://` has no local path; use `read`. Other internal URLs accept exact paths only.
6
+ - `gitignore` defaults `true`. Set `false` for ignored files such as `.env*`, logs, or build output.
7
+ - `hidden` defaults `true`; pair it with `gitignore: false` for ignored dotfiles.
7
8
  </instruction>
8
9
 
9
10
  <output>
10
- Matching paths sorted by mtime (newest first), grouped under `# <dir>/` headers with basenames below; directories get a trailing `/`.
11
+ Matches are newest-first and grouped by directory; directories end in `/`.
11
12
  </output>
12
13
 
13
14
  <avoid>
14
- Open-ended searches needing multiple rounds of globbing/searching: you MUST use the Task tool instead.
15
+ Open-ended multi-round discovery Task + scout.
15
16
  </avoid>
@@ -1,12 +1,13 @@
1
- Greps files using regex (Rust regex + PCRE2).
1
+ Searches files and internal URLs with Rust regex plus PCRE2 fallback.
2
2
 
3
3
  <instruction>
4
- - `path`: scope to known path (e.g. `src`); pass several as delimited list (`src; tests`).
5
- Line selector on one file (`src/foo.ts:50-100`); selectors never choose search root.
6
- - Cross-line patterns from literal `\n` or `\\n` in `pattern`.
4
+ - Scope `path` to known files, directories, globs, or internal URLs; separate roots with `;`.
5
+ - Broad searches can time out; scope them narrowly or use `glob` first.
6
+ - One-file line selector: `src/foo.ts:50-100` (selectors never choose the search root).
7
+ - Literal `\n` or `\\n` enables cross-line patterns.
7
8
  </instruction>
8
9
 
9
10
  <critical>
10
- - MUST use this over bash when searching!
11
- - Open-ended multi-round search Task tool + scout subagent, NOT chained `grep` calls.
11
+ - MUST use this instead of shell `grep`/`rg`.
12
+ - Open-ended multi-round search MUST use Task + scout, not chained calls.
12
13
  </critical>
package/src/sdk.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  discoverWatchdogFiles,
34
34
  formatActiveRepoWatchdogPrompt,
35
35
  formatAdvisorContextPrompt,
36
+ loadAdvisorTranscriptCosts,
36
37
  } from "./advisor";
37
38
  import { AsyncJobManager } from "./async";
38
39
  import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
@@ -60,7 +61,8 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate
60
61
  import { applyProviderGlobalsFromSettings } from "./config/provider-globals";
61
62
  import { buildServiceTierByFamily } from "./config/service-tier";
62
63
  import { Settings, type SkillsSettings } from "./config/settings";
63
- import { CursorExecHandlers } from "./cursor";
64
+ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "./cursor";
65
+ import { createBridgeEditTool, createBridgeGrepFactory } from "./cursor-bridge-tools";
64
66
  import "./discovery";
65
67
  import { initializeWithSettings } from "./discovery";
66
68
  import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
@@ -607,6 +609,10 @@ export type * from "./extensibility/extensions";
607
609
  export type { Skill } from "./extensibility/skills";
608
610
  export type { FileSlashCommand } from "./extensibility/slash-commands";
609
611
  export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp";
612
+ // Agent registry: pass a private instance per `createAgentSession` when
613
+ // embedding several concurrent top-level sessions in one process (the default
614
+ // global registry admits only one "Main" per process generation).
615
+ export { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
610
616
  export type { Tool } from "./tools";
611
617
  export { buildDirectoryTree, buildWorkspaceTree, type DirectoryTree, type WorkspaceTree } from "./workspace-tree";
612
618
 
@@ -1772,7 +1778,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1772
1778
  getArtifactsDir,
1773
1779
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1774
1780
  };
1775
- if (options.localProtocolOptions) {
1781
+ if (options.localProtocolOptions && !options.parentTaskPrefix) {
1776
1782
  LocalProtocolHandler.setOverride(options.localProtocolOptions);
1777
1783
  }
1778
1784
  toolSession.getArtifactsDir = getArtifactsDir;
@@ -2513,6 +2519,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2513
2519
  () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
2514
2520
  settings,
2515
2521
  localProtocolOptions,
2522
+ () => (hasSession ? session.getAsyncJobSnapshot() : null),
2516
2523
  );
2517
2524
 
2518
2525
  credentialDisabledTarget = extensionRunner;
@@ -2582,6 +2589,43 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2582
2589
  for (const tool of toolRegistry.values()) {
2583
2590
  toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
2584
2591
  }
2592
+ // Cursor's own client owns file edits, so `edit` is not advertised to the
2593
+ // model (commit 8ba0498eb: full-file `write` is used instead). The exec
2594
+ // bridge is a different consumer: the server sends native `pi_edit`
2595
+ // frames regardless of the advertised catalog, and answering them needs
2596
+ // a real tool.
2597
+ //
2598
+ // It must be a `replace`-mode instance. `PiEditExecArgs` carries
2599
+ // `old_text`/`new_text` pairs, which is exactly `replace`'s schema and
2600
+ // nothing else's — under the default `hashline` mode the frame's args do
2601
+ // not match the tool's parameters at all. The registry instance follows
2602
+ // the session's configured mode, so the bridge builds its own.
2603
+ //
2604
+ // The grant is captured HERE, before the Cursor branch below deletes
2605
+ // `edit` from the registry, and independently of the session's provider:
2606
+ // a session that starts on another provider can switch to Cursor later,
2607
+ // and the roster is built once, at session creation. Reading the registry
2608
+ // at frame time would see the switched-to state, not the grant.
2609
+ const editWasGranted = toolRegistry.has("edit");
2610
+ // Built on first use rather than eagerly: a session that never reaches
2611
+ // Cursor never constructs it.
2612
+ let cursorBridgeEditTool: AgentTool | undefined;
2613
+ const getCursorBridgeEditTool = (): AgentTool | undefined => {
2614
+ // Only when the session actually granted `edit`. `createTools` omits
2615
+ // it entirely for a restricted tool set, and the bridge answers native
2616
+ // frames that arrive regardless of the advertised catalog — so
2617
+ // building one unconditionally would hand a read-only agent a
2618
+ // mutating tool it was denied (the issue #5680 escalation).
2619
+ if (!editWasGranted) return undefined;
2620
+ cursorBridgeEditTool ??= createBridgeEditTool(toolSession, extensionRunner);
2621
+ return cursorBridgeEditTool;
2622
+ };
2623
+ // Whether this session granted a file-writing tool. Same capture-early
2624
+ // reasoning, plus `write` may be auto-registered further down as an xdev
2625
+ // transport. The exec bridge answers native `delete` and
2626
+ // resource-download frames that mutate files without running a registry
2627
+ // tool, so it needs the grant as the session actually made it.
2628
+ const cursorCanMutateFiles = editWasGranted || toolRegistry.has("write");
2585
2629
  if (model?.provider === "cursor") {
2586
2630
  toolRegistry.delete("edit");
2587
2631
  builtInRegistryToolNames.delete("edit");
@@ -2624,15 +2668,52 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2624
2668
  if (!state) return undefined;
2625
2669
  return resolveMountedXdevExecutable(state, name);
2626
2670
  };
2671
+ // Cursor's resource frames ask what THIS client's servers advertise; only
2672
+ // live connections have any. Built once: the advisor bridges answer from
2673
+ // the same connections the primary does.
2674
+ const cursorMcpResources: CursorMcpResourceAdapter | undefined = mcpManager && {
2675
+ serverNames: () => mcpManager.getConnectedServers(),
2676
+ getServerResources: async name => {
2677
+ // The manager registers a server's tools before its background
2678
+ // resource load finishes, so a frame arriving in that window
2679
+ // would read an empty cache and report "advertises nothing".
2680
+ await mcpManager.ensureServerResources(name);
2681
+ return mcpManager.getServerResources(name);
2682
+ },
2683
+ readServerResource: (name, uri) => mcpManager.readServerResource(name, uri),
2684
+ };
2627
2685
  const cursorExecHandlers = new CursorExecHandlers({
2628
2686
  cwd,
2687
+ // The session's cwd moves (`/cd`, resume, branch restore) while this
2688
+ // bridge is built once at startup. Path-confining frames — the native
2689
+ // `delete` and a `download_path` resource read — resolve against
2690
+ // whichever of the two they are given, so without the live resolver the
2691
+ // primary would write into the workspace the session has left while
2692
+ // reporting success for the path the server asked about. The advisor
2693
+ // bridge already passes one.
2694
+ getCwd: () => sessionManager.getCwd(),
2629
2695
  tools: toolRegistry,
2630
2696
  getExecutableTool: resolveDeviceTool,
2697
+ // `pi_edit` needs the `replace`-mode instance specifically, and the
2698
+ // registry may still hold the session's own `edit` (any mode) when
2699
+ // this session did not start on Cursor.
2700
+ getEditReplaceTool: getCursorBridgeEditTool,
2631
2701
  getToolContext: () => toolContextStore.getContext(),
2702
+ mcpResources: cursorMcpResources,
2632
2703
  emitEvent: event => cursorEventEmitter?.(event),
2633
2704
  getTodoPhases: () => session.getTodoPhases(),
2634
2705
  setTodoPhases: phases => session.setTodoPhases(phases),
2635
2706
  persistTodoPhases: phases => sessionManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases }),
2707
+ // `pi_grep` carries its own context width and match cap, which the
2708
+ // shared grep instance fixed at construction cannot express. Gated on
2709
+ // the grant: the factory builds a fresh tool and `executeTool` prefers
2710
+ // it over the registry, so installing it unconditionally would let a
2711
+ // session without `grep` search anyway.
2712
+ createGrepTool: toolRegistry.has("grep") ? createBridgeGrepFactory(toolSession, extensionRunner) : undefined,
2713
+ // The native `delete` and resource-download frames mutate files
2714
+ // without running a registry tool, so this grant is the only thing
2715
+ // standing between a restricted session and a workspace write.
2716
+ allowDirectFileMutation: cursorCanMutateFiles,
2636
2717
  });
2637
2718
 
2638
2719
  // Resolve the inline-descriptors setting against the session-start model.
@@ -2793,6 +2874,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2793
2874
  }
2794
2875
  }
2795
2876
  }
2877
+ // Checkpoint and rewind are a pair: `createTools` auto-includes the sister
2878
+ // tool in the registry, but an explicit `toolNames` list would otherwise
2879
+ // drop it from the ACTIVE set — leaving the agent able to checkpoint but
2880
+ // unable to rewind (or vice versa). Mirror the pairing here. Unlike the
2881
+ // manage_skill/learn mirror above, this is a safety pairing — it applies
2882
+ // to restricted sessions too.
2883
+ if (explicitlyRequestedToolNames) {
2884
+ if (builtInToolNames.includes("checkpoint") && !explicitlyRequestedToolNames.includes("rewind")) {
2885
+ explicitlyRequestedToolNames.push("rewind");
2886
+ } else if (builtInToolNames.includes("rewind") && !explicitlyRequestedToolNames.includes("checkpoint")) {
2887
+ explicitlyRequestedToolNames.push("checkpoint");
2888
+ }
2889
+ }
2796
2890
  const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
2797
2891
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
2798
2892
  const defaultInactiveToolNames = new Set(
@@ -3112,7 +3206,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3112
3206
  advisorToolBuilds.push(BUILTIN_TOOLS[name as keyof typeof BUILTIN_TOOLS](advisorToolSession));
3113
3207
  }
3114
3208
  const built = await Promise.all(advisorToolBuilds);
3115
- const advisorTools: Tool[] = built.filter((tool): tool is Tool => tool != null).map(wrapToolWithMetaNotice);
3209
+ // Wrapped like every registry tool: `ExtensionToolWrapper` is where the
3210
+ // approval mode, per-tool `tools.approval.<tool>` policies and
3211
+ // `autoApprove` are enforced. The advisor's loop and its Cursor exec
3212
+ // bridge both run these instances directly, so a raw one would execute a
3213
+ // `bash`/`write` the user configured as `ask` or `deny`. Meta-notice
3214
+ // first, matching the registry's wrap order.
3215
+ const advisorTools: Tool[] = built
3216
+ .filter((tool): tool is Tool => tool != null)
3217
+ .map(tool => new ExtensionToolWrapper(wrapToolWithMetaNotice(tool), extensionRunner) as Tool);
3116
3218
 
3117
3219
  const advisorWatchdogPrompts = [...watchdogFiles];
3118
3220
  if (activeRepoContext) {
@@ -3126,6 +3228,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3126
3228
  // Owned only when this session created the manager; subagents receive a
3127
3229
  // parent's manager via `options.mcpManager` and MUST NOT disconnect it.
3128
3230
  const ownedMcpManager = options.mcpManager ? undefined : mcpManager;
3231
+ // A resumed session already has advisor turns on disk; without this the status
3232
+ // line would restart its `(adv)` total at zero for the rest of the session.
3233
+ const initialAdvisorCosts = await loadAdvisorTranscriptCosts(sessionManager.getSessionFile());
3129
3234
  session = new AgentSession({
3130
3235
  advisorWatchdogPrompt,
3131
3236
  advisorContextPrompt,
@@ -3140,6 +3245,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3140
3245
  planYolo: options.planYolo,
3141
3246
  serviceTierByFamily: initialServiceTierByFamily,
3142
3247
  sessionManager,
3248
+ initialAdvisorCosts,
3143
3249
  settings,
3144
3250
  autoApprove: options.autoApprove,
3145
3251
  evalKernelOwnerId,
@@ -3218,6 +3324,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3218
3324
  providerPromptCacheKeySource,
3219
3325
  parentEvalSessionId: options.parentEvalSessionId,
3220
3326
  advisorTools,
3327
+ // Same per-call `grep` seam the primary bridge gets, built against the
3328
+ // advisor's own tool session so a `pi_grep` frame's context width and
3329
+ // match cap are honored there too.
3330
+ advisorCreateGrepTool: createBridgeGrepFactory(advisorToolSession, extensionRunner),
3331
+ // Same `replace`-mode requirement as the primary bridge; the advisor
3332
+ // path gates it on the advisor's own `edit` grant.
3333
+ advisorCreateEditTool: () => createBridgeEditTool(advisorToolSession, extensionRunner),
3334
+ // The advisor's bridge tools are wrapped for approval, but the wrapper
3335
+ // reads the mode and per-tool policies only from the execute-time
3336
+ // context — the primary bridge passes the same store.
3337
+ advisorGetToolContext: () => toolContextStore.getContext(),
3338
+ // Same live connections the primary bridge reads; an advisor's
3339
+ // resource frame would otherwise report every server as empty.
3340
+ advisorMcpResources: cursorMcpResources,
3221
3341
  titleSystemPrompt: options.titleSystemPrompt,
3222
3342
  });
3223
3343
  hasSession = true;
@@ -3245,6 +3365,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3245
3365
  throw new Error(`Agent "${resolvedAgentId}" was replaced during session initialization.`);
3246
3366
  }
3247
3367
  hasRegistered = true;
3368
+ // MCP notification bridge cleanup — assigned when the bridge is wired below,
3369
+ // invoked from the dispose wrapper AND registered as a postmortem so both
3370
+ // explicit-dispose (SDK embedders that reuse the process across sessions) and
3371
+ // process-exit paths tear the listener down. Nulled after use so the closure
3372
+ // graph (`extensionRunner`, `session`) can be GC'd instead of retained by the
3373
+ // process-global postmortem list.
3374
+ let unsubscribeMcpNotifications: (() => void) | undefined;
3375
+ let unregisterMcpPostmortem: (() => void) | undefined;
3376
+
3248
3377
  {
3249
3378
  const originalDispose = session.dispose.bind(session);
3250
3379
  session.dispose = async () => {
@@ -3275,6 +3404,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3275
3404
  } finally {
3276
3405
  unregisterUnlessParked();
3277
3406
  unsubscribeCredentialDisabled?.();
3407
+ unsubscribeMcpNotifications?.();
3408
+ unregisterMcpPostmortem?.();
3409
+ // Drop refs so the process-global postmortem list doesn't retain
3410
+ // the bridge closure past explicit dispose.
3411
+ unsubscribeMcpNotifications = undefined;
3412
+ unregisterMcpPostmortem = undefined;
3278
3413
  }
3279
3414
  };
3280
3415
  }
@@ -3447,19 +3582,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3447
3582
  }
3448
3583
  }
3449
3584
 
3450
- // Wire MCP manager callbacks to session for reactive tool updates.
3451
- // Skip when reusing a parent's manager the parent owns the callbacks.
3585
+ // MCP manager wiring has two ownership models:
3586
+ // * Single-slot callbacks (tools/prompts/resources changed)exactly one
3587
+ // owner per manager. When reusing a parent's manager (subagent path,
3588
+ // see task/executor.ts), the parent already owns these slots so we
3589
+ // MUST NOT overwrite them. Guarded by `!options.mcpManager`.
3590
+ // * Notification listener — multi-listener by design. Every session with
3591
+ // an MCP manager (fresh OR reused) needs its own bridge to its own
3592
+ // `extensionRunner` so extensions loaded in that session receive frames.
3593
+ // Guarded only by `mcpManager` (see the second `if` below).
3452
3594
  if (mcpManager && !options.mcpManager) {
3453
- mcpManager.setOnToolsChanged(tools => {
3454
- void (async () => {
3455
- try {
3456
- await session.refreshMCPTools(tools);
3457
- } catch (error) {
3458
- logger.warn("MCP tool refresh failed", {
3459
- error: error instanceof Error ? error.message : String(error),
3460
- });
3461
- }
3462
- })();
3595
+ mcpManager.setOnToolsChanged(async tools => {
3596
+ try {
3597
+ await session.refreshMCPTools(tools);
3598
+ } catch (error) {
3599
+ logger.warn("MCP tool refresh failed", {
3600
+ error: error instanceof Error ? error.message : String(error),
3601
+ });
3602
+ }
3463
3603
  });
3464
3604
  // Wire prompt refresh → rebuild MCP prompt slash commands
3465
3605
  mcpManager.setOnPromptsChanged(serverName => {
@@ -3492,6 +3632,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3492
3632
  });
3493
3633
  }
3494
3634
 
3635
+ if (mcpManager) {
3636
+ // Bridge server-initiated notifications to this session's extension
3637
+ // handlers. Multi-listener registration: fresh-manager and reused-manager
3638
+ // sessions both install their own listener here, so a subagent's
3639
+ // extensions get frames even though the parent owns the single-slot
3640
+ // tool/prompt/resource callbacks above. MCPManager fires known
3641
+ // list/update refreshes internally, then invokes all registered
3642
+ // listeners with (server, method, params) for every frame (including
3643
+ // server-custom methods). Two-layer buffering protects the startup
3644
+ // race: MCPManager buffers frames received before the first
3645
+ // `addNotificationListener` subscriber (drains here); ExtensionRunner
3646
+ // buffers frames received before `initialize()` and drains them on
3647
+ // init. Both drop-oldest under pressure at cap 100.
3648
+ unsubscribeMcpNotifications = mcpManager.addNotificationListener((server, method, params) => {
3649
+ void extensionRunner.emitMcpNotification({ server, method, params });
3650
+ });
3651
+ // postmortem.register returns a cancel function; capture it so explicit
3652
+ // session.dispose can remove this from the global list (see finally above).
3653
+ unregisterMcpPostmortem = postmortem.register("mcp-notification-listener-cleanup", () =>
3654
+ unsubscribeMcpNotifications?.(),
3655
+ );
3656
+ }
3657
+
3495
3658
  startDeferredMCPDiscovery?.(session);
3496
3659
 
3497
3660
  return {
@@ -47,6 +47,7 @@ export type AgentSessionEvent =
47
47
  }
48
48
  | { type: "retry_fallback_applied"; from: string; to: string; role: string }
49
49
  | { type: "retry_fallback_succeeded"; model: string; role: string }
50
+ | { type: "model_changed" }
50
51
  | { type: "ttsr_triggered"; rules: Rule[] }
51
52
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
52
53
  | { type: "todo_auto_clear" }
@@ -1,4 +1,11 @@
1
- import type { Agent, AgentMessage, AgentTool, StreamFn, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import type {
2
+ Agent,
3
+ AgentMessage,
4
+ AgentTool,
5
+ AgentToolContext,
6
+ StreamFn,
7
+ ThinkingLevel,
8
+ } from "@oh-my-pi/pi-agent-core";
2
9
  import type {
3
10
  Context,
4
11
  Effort,
@@ -17,6 +24,7 @@ import type { AsyncJob, AsyncJobDeliveryState, AsyncJobManager } from "../async"
17
24
  import type { ModelRegistry } from "../config/model-registry";
18
25
  import type { PromptTemplate } from "../config/prompt-templates";
19
26
  import type { Settings, SkillsSettings } from "../config/settings";
27
+ import type { CursorMcpResourceAdapter } from "../cursor";
20
28
  import type { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
21
29
  import type { TtsrManager } from "../export/ttsr";
22
30
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
@@ -161,6 +169,8 @@ export interface AgentSessionConfig {
161
169
  sideStreamFn?: StreamFn;
162
170
  /** Stream wrapper for advisor requests. */
163
171
  advisorStreamFn?: StreamFn;
172
+ /** Advisor spend already recorded for the session being opened, restored on resume. */
173
+ initialAdvisorCosts?: ReadonlyMap<string, number>;
164
174
  /** Prefer websocket transport for OpenAI Codex requests when supported. */
165
175
  preferWebsockets?: boolean;
166
176
  /** Provider payload hook used by the active session request path. */
@@ -207,6 +217,35 @@ export interface AgentSessionConfig {
207
217
  providerPromptCacheKeySource?: "explicit" | "fork";
208
218
  /** Full advisor toolset built against an advisor-scoped tool session. */
209
219
  advisorTools?: AgentTool[];
220
+ /**
221
+ * Build a `grep` honoring a Cursor `pi_grep` frame's own context width and
222
+ * match cap, against the advisor-scoped tool session. Without it an advisor
223
+ * running on Cursor silently drops both fields.
224
+ */
225
+ advisorCreateGrepTool?(options: { context?: number; totalMatchLimit?: number }): AgentTool | undefined;
226
+ /**
227
+ * Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs, against the
228
+ * advisor-scoped tool session. The advisor's ordinary instance follows the
229
+ * configured `edit.mode` and rejects the frame's `old_text`/`new_text` pairs.
230
+ */
231
+ advisorCreateEditTool?(): AgentTool | undefined;
232
+ /**
233
+ * The execute-time context the advisor's bridge tools resolve approval from.
234
+ *
235
+ * `ExtensionToolWrapper` reads `tools.approvalMode`, per-tool
236
+ * `tools.approval.<tool>` policies and `autoApprove` only from this context;
237
+ * with none it defaults to `yolo` with empty policies, so a bridge tool would
238
+ * run a native frame the user configured `ask` or `deny` for.
239
+ */
240
+ advisorGetToolContext?: () => AgentToolContext | undefined;
241
+ /**
242
+ * The live MCP connections the advisor's Cursor resource frames answer from.
243
+ *
244
+ * Advisors share the session's connections and may be granted tools from
245
+ * those same servers; without this their `list_mcp_resources` reports an
246
+ * empty catalog and every `read_mcp_resource` a `not_found`.
247
+ */
248
+ advisorMcpResources?: CursorMcpResourceAdapter;
210
249
  /** Preloaded watchdog prompt content for the advisor. */
211
250
  advisorWatchdogPrompt?: string;
212
251
  /** Shared advisor instructions loaded from WATCHDOG.yml. */