@oh-my-pi/pi-coding-agent 17.1.7 → 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 (246) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-be1f2t8h.md} +127 -0
  3. package/dist/cli.js +9554 -12379
  4. package/dist/template-8vdv6xb4.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +8 -11
  10. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  11. package/dist/types/async/job-manager.d.ts +11 -0
  12. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  13. package/dist/types/cleanse/agent.d.ts +19 -0
  14. package/dist/types/cleanse/balance.d.ts +7 -0
  15. package/dist/types/cleanse/checkers.d.ts +13 -0
  16. package/dist/types/cleanse/index.d.ts +16 -0
  17. package/dist/types/cleanse/loop.d.ts +16 -0
  18. package/dist/types/cleanse/parsers.d.ts +13 -0
  19. package/dist/types/cleanse/progress.d.ts +14 -0
  20. package/dist/types/cleanse/types.d.ts +64 -0
  21. package/dist/types/cli/args.d.ts +2 -0
  22. package/dist/types/cli/session-picker.d.ts +14 -7
  23. package/dist/types/collab/guest.d.ts +21 -2
  24. package/dist/types/commands/cleanse.d.ts +23 -0
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/config/settings-schema.d.ts +63 -8
  27. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  28. package/dist/types/cursor.d.ts +193 -8
  29. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  30. package/dist/types/edit/hashline/diff.d.ts +9 -1
  31. package/dist/types/edit/index.d.ts +8 -1
  32. package/dist/types/edit/streaming.d.ts +8 -1
  33. package/dist/types/export/html/index.d.ts +2 -0
  34. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  35. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  36. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  37. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +2 -0
  38. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  39. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  40. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  41. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  42. package/dist/types/lsp/client.d.ts +2 -0
  43. package/dist/types/main.d.ts +2 -0
  44. package/dist/types/mcp/manager.d.ts +33 -4
  45. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  47. package/dist/types/modes/components/session-selector.d.ts +6 -0
  48. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  50. package/dist/types/modes/components/usage-row.d.ts +2 -0
  51. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  52. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  53. package/dist/types/modes/interactive-mode.d.ts +8 -5
  54. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  55. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  56. package/dist/types/modes/theme/theme.d.ts +12 -1
  57. package/dist/types/modes/types.d.ts +2 -1
  58. package/dist/types/sdk.d.ts +1 -0
  59. package/dist/types/session/agent-session-events.d.ts +2 -0
  60. package/dist/types/session/agent-session-types.d.ts +36 -1
  61. package/dist/types/session/agent-session.d.ts +4 -2
  62. package/dist/types/session/async-job-delivery.d.ts +8 -0
  63. package/dist/types/session/claude-session-store.d.ts +13 -0
  64. package/dist/types/session/codex-session-store.d.ts +14 -0
  65. package/dist/types/session/credential-pin.d.ts +58 -0
  66. package/dist/types/session/foreign-session-import.d.ts +15 -0
  67. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  68. package/dist/types/session/foreign-session-store.d.ts +23 -0
  69. package/dist/types/session/model-controls.d.ts +3 -3
  70. package/dist/types/session/session-advisors.d.ts +47 -2
  71. package/dist/types/session/session-entries.d.ts +19 -1
  72. package/dist/types/session/session-handoff.d.ts +4 -1
  73. package/dist/types/session/session-loader.d.ts +2 -0
  74. package/dist/types/session/session-maintenance.d.ts +1 -1
  75. package/dist/types/session/session-manager.d.ts +21 -0
  76. package/dist/types/session/session-tools.d.ts +25 -1
  77. package/dist/types/session/turn-recovery.d.ts +10 -4
  78. package/dist/types/system-prompt.d.ts +2 -1
  79. package/dist/types/task/executor.d.ts +11 -2
  80. package/dist/types/task/isolation-ownership.d.ts +34 -0
  81. package/dist/types/task/render.d.ts +7 -0
  82. package/dist/types/thinking.d.ts +18 -6
  83. package/dist/types/tools/acp-bridge.d.ts +28 -5
  84. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  85. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  86. package/dist/types/tools/grep.d.ts +17 -1
  87. package/dist/types/tools/hub/launch.d.ts +5 -1
  88. package/dist/types/tools/index.d.ts +4 -1
  89. package/dist/types/tools/path-utils.d.ts +19 -0
  90. package/dist/types/tools/read.d.ts +2 -0
  91. package/dist/types/tools/xdev.d.ts +2 -1
  92. package/dist/types/tui/output-block.d.ts +5 -5
  93. package/dist/types/utils/changelog.d.ts +19 -0
  94. package/dist/types/utils/git.d.ts +10 -0
  95. package/dist/types/utils/jj.d.ts +11 -4
  96. package/dist/types/web/search/types.d.ts +2 -2
  97. package/package.json +16 -12
  98. package/scripts/bundle-dist.ts +3 -1
  99. package/src/advisor/advise-tool.ts +0 -11
  100. package/src/advisor/runtime.ts +138 -24
  101. package/src/advisor/transcript-recorder.ts +56 -0
  102. package/src/async/job-manager.ts +30 -7
  103. package/src/auto-thinking/classifier.ts +48 -10
  104. package/src/cleanse/agent.ts +226 -0
  105. package/src/cleanse/balance.ts +79 -0
  106. package/src/cleanse/checkers.ts +996 -0
  107. package/src/cleanse/index.ts +190 -0
  108. package/src/cleanse/loop.ts +51 -0
  109. package/src/cleanse/parsers.ts +726 -0
  110. package/src/cleanse/progress.ts +50 -0
  111. package/src/cleanse/prompts/assignment.md +47 -0
  112. package/src/cleanse/types.ts +72 -0
  113. package/src/cli/args.ts +6 -0
  114. package/src/cli/flag-tables.ts +2 -0
  115. package/src/cli/gallery-fixtures/fs.ts +25 -0
  116. package/src/cli/session-picker.ts +34 -17
  117. package/src/cli/ttsr-cli.ts +19 -1
  118. package/src/cli/update-cli.ts +3 -0
  119. package/src/cli/usage-cli.ts +29 -4
  120. package/src/cli/worktree-cli.ts +28 -11
  121. package/src/cli-commands.ts +1 -0
  122. package/src/cli.ts +11 -1
  123. package/src/collab/guest.ts +37 -4
  124. package/src/collab/host.ts +1 -0
  125. package/src/commands/cleanse.ts +45 -0
  126. package/src/commands/launch.ts +6 -0
  127. package/src/config/model-registry.ts +103 -25
  128. package/src/config/settings-schema.ts +65 -6
  129. package/src/config/settings.ts +25 -0
  130. package/src/cursor-bridge-tools.ts +81 -0
  131. package/src/cursor.ts +463 -12
  132. package/src/discovery/claude-plugins.ts +144 -34
  133. package/src/edit/edit-clipboard.ts +23 -0
  134. package/src/edit/hashline/diff.ts +49 -10
  135. package/src/edit/hashline/execute.ts +38 -6
  136. package/src/edit/hashline/filesystem.ts +27 -3
  137. package/src/edit/index.ts +12 -2
  138. package/src/edit/renderer.ts +4 -4
  139. package/src/edit/streaming.ts +17 -2
  140. package/src/export/html/index.ts +17 -10
  141. package/src/export/html/template.js +1 -1
  142. package/src/extensibility/extensions/runner.ts +70 -0
  143. package/src/extensibility/extensions/types.ts +30 -0
  144. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  145. package/src/extensibility/legacy-pi-coding-agent-shim.ts +12 -13
  146. package/src/internal-urls/local-protocol.ts +3 -2
  147. package/src/internal-urls/mcp-protocol.ts +4 -1
  148. package/src/launch/broker.ts +14 -4
  149. package/src/launch/terminal-output-worker-client.ts +53 -0
  150. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  151. package/src/launch/terminal-output-worker.ts +23 -0
  152. package/src/lsp/client.ts +18 -3
  153. package/src/lsp/index.ts +4 -0
  154. package/src/lsp/render.ts +1 -1
  155. package/src/main.ts +127 -56
  156. package/src/mcp/manager.ts +120 -18
  157. package/src/modes/acp/acp-agent.ts +27 -18
  158. package/src/modes/acp/acp-event-mapper.ts +38 -4
  159. package/src/modes/components/chat-transcript-builder.ts +30 -11
  160. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  161. package/src/modes/components/read-tool-group.ts +159 -17
  162. package/src/modes/components/session-selector.ts +19 -3
  163. package/src/modes/components/settings-selector.ts +9 -1
  164. package/src/modes/components/status-line/component.ts +381 -54
  165. package/src/modes/components/tool-execution.ts +78 -31
  166. package/src/modes/components/usage-row.ts +10 -5
  167. package/src/modes/controllers/event-controller.ts +26 -4
  168. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  169. package/src/modes/controllers/selector-controller.ts +94 -36
  170. package/src/modes/controllers/tan-command-controller.ts +14 -0
  171. package/src/modes/interactive-mode.ts +67 -64
  172. package/src/modes/prompt-action-autocomplete.ts +5 -3
  173. package/src/modes/rpc/rpc-client.ts +18 -0
  174. package/src/modes/rpc/rpc-mode.ts +16 -1
  175. package/src/modes/rpc/rpc-types.ts +11 -0
  176. package/src/modes/theme/theme.ts +151 -6
  177. package/src/modes/types.ts +2 -1
  178. package/src/modes/utils/ui-helpers.ts +38 -21
  179. package/src/prompts/goals/guided-goal-interview.md +41 -6
  180. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  181. package/src/prompts/system/plan-mode-active.md +2 -2
  182. package/src/prompts/system/system-prompt.md +3 -0
  183. package/src/prompts/system/vibe-mode-active.md +4 -1
  184. package/src/prompts/system/xdev-mount-notice.md +1 -1
  185. package/src/prompts/tools/bash.md +15 -17
  186. package/src/prompts/tools/browser.md +1 -0
  187. package/src/prompts/tools/checkpoint.md +1 -1
  188. package/src/prompts/tools/glob.md +7 -6
  189. package/src/prompts/tools/goal.md +1 -1
  190. package/src/prompts/tools/grep.md +7 -6
  191. package/src/sdk.ts +178 -15
  192. package/src/session/agent-session-events.ts +1 -0
  193. package/src/session/agent-session-types.ts +40 -1
  194. package/src/session/agent-session.ts +330 -179
  195. package/src/session/async-job-delivery.ts +8 -0
  196. package/src/session/claude-session-store.ts +426 -0
  197. package/src/session/codex-session-store.ts +673 -0
  198. package/src/session/credential-pin.ts +93 -0
  199. package/src/session/foreign-session-import.ts +52 -0
  200. package/src/session/foreign-session-jsonl.ts +29 -0
  201. package/src/session/foreign-session-store.ts +26 -0
  202. package/src/session/model-controls.ts +15 -7
  203. package/src/session/session-advisors.ts +142 -23
  204. package/src/session/session-entries.ts +21 -1
  205. package/src/session/session-handoff.ts +20 -3
  206. package/src/session/session-history-format.ts +31 -6
  207. package/src/session/session-loader.ts +25 -9
  208. package/src/session/session-maintenance.ts +59 -11
  209. package/src/session/session-manager.ts +61 -0
  210. package/src/session/session-tools.ts +107 -5
  211. package/src/session/settings-stream-fn.ts +1 -0
  212. package/src/session/turn-recovery.ts +97 -51
  213. package/src/slash-commands/builtin-registry.ts +136 -5
  214. package/src/system-prompt.ts +3 -2
  215. package/src/task/executor.ts +17 -4
  216. package/src/task/isolation-ownership.ts +106 -0
  217. package/src/task/render.ts +20 -4
  218. package/src/task/worktree.ts +8 -0
  219. package/src/thinking.ts +29 -10
  220. package/src/tools/acp-bridge.ts +52 -8
  221. package/src/tools/ask.ts +3 -3
  222. package/src/tools/ast-edit.ts +9 -2
  223. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  224. package/src/tools/browser/tab-worker.ts +12 -35
  225. package/src/tools/browser.ts +7 -2
  226. package/src/tools/checkpoint.ts +0 -13
  227. package/src/tools/gh-renderer.ts +3 -3
  228. package/src/tools/grep.ts +64 -8
  229. package/src/tools/hub/launch.ts +10 -15
  230. package/src/tools/index.ts +38 -5
  231. package/src/tools/path-utils.ts +88 -0
  232. package/src/tools/read.ts +3 -0
  233. package/src/tools/write.ts +44 -3
  234. package/src/tools/xdev.ts +39 -14
  235. package/src/tui/code-cell.ts +4 -4
  236. package/src/tui/output-block.ts +25 -8
  237. package/src/utils/changelog.ts +144 -4
  238. package/src/utils/git.ts +61 -14
  239. package/src/utils/jj.ts +19 -13
  240. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  241. package/src/web/search/providers/codex.ts +34 -0
  242. package/src/web/search/render.ts +2 -2
  243. package/src/web/search/types.ts +1 -1
  244. package/dist/types/goals/guided-setup.d.ts +0 -30
  245. package/src/goals/guided-setup.ts +0 -171
  246. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Session-file persistence of the OAuth account that served a session.
3
+ *
4
+ * Provider prompt caches are account-scoped (Anthropic bills a full cache
5
+ * re-write after an account flip), and the auth store's session-sticky routing
6
+ * is process-local when a remote auth broker is configured — the broker
7
+ * store's KV cache is in-memory, so sticky rows die with the CLI process.
8
+ * Resuming a session in a fresh process then re-ranks accounts by usage
9
+ * headroom, which is biased *away* from the account that just served the
10
+ * session (it has the highest recent burn), cold-missing the entire prefix.
11
+ *
12
+ * These helpers close the loop through the session file itself: after each
13
+ * assistant turn the serving account is recorded as a `credential_pin` entry,
14
+ * and on session adoption the pin is matched against the stored accounts and
15
+ * seeded back into the auth store with the session's effective last-use
16
+ * timestamp, so the provider's warm-window semantics still decide whether to
17
+ * stick or re-rank.
18
+ */
19
+
20
+ import type { AuthStorage } from "./auth-storage";
21
+ import type { SessionManager } from "./session-manager";
22
+
23
+ /** Account fields shared by `OAuthAccountIdentity` and `OAuthAccountSummary`. */
24
+ interface CredentialPinIdentity {
25
+ accountId?: string;
26
+ email?: string;
27
+ projectId?: string;
28
+ orgId?: string;
29
+ }
30
+
31
+ /**
32
+ * Stable identifier for a provider account within its billing scope. The
33
+ * digest covers the full scope tuple — the same account in two orgs (Anthropic
34
+ * multi-subscription) or projects (Gemini) is two distinct cache domains and
35
+ * must produce two distinct pins. The digest input is the persisted contract
36
+ * for `CredentialPinEntry.hash` — changing it orphans every recorded pin.
37
+ *
38
+ * Hashing avoids embedding raw emails/uuids in session files, but an unsalted
39
+ * digest of a guessable email is still linkable — treat exported sessions
40
+ * accordingly.
41
+ *
42
+ * Returns `undefined` when the identity carries no account key at all.
43
+ */
44
+ export function credentialPinHash(provider: string, identity: CredentialPinIdentity): string | undefined {
45
+ if (!identity.accountId && !identity.email) return undefined;
46
+ const key = [
47
+ provider,
48
+ identity.accountId ?? "",
49
+ identity.email ?? "",
50
+ identity.orgId ?? "",
51
+ identity.projectId ?? "",
52
+ ].join("\0");
53
+ return new Bun.CryptoHasher("sha256").update(key).digest("hex");
54
+ }
55
+
56
+ /**
57
+ * Record the account that served the latest assistant turn for `provider`.
58
+ * Appends a `credential_pin` entry only when the account differs from the
59
+ * branch's latest pin, so steady-state sessions add a single entry; the
60
+ * effective last-use time is derived from later assistant turns on read
61
+ * (see `SessionManager.getCredentialPins`).
62
+ */
63
+ export function recordCredentialPin(
64
+ authStorage: AuthStorage,
65
+ sessionManager: SessionManager,
66
+ sessionId: string,
67
+ provider: string,
68
+ ): void {
69
+ const identity = authStorage.getOAuthAccountIdentity(provider, sessionId);
70
+ if (!identity) return;
71
+ const hash = credentialPinHash(provider, identity);
72
+ if (!hash || sessionManager.getCredentialPins().get(provider)?.hash === hash) return;
73
+ sessionManager.appendCredentialPin(provider, hash);
74
+ }
75
+
76
+ /**
77
+ * Re-pin the accounts recorded in the session file onto the auth store's
78
+ * session stickiness. No-op per provider when the account is gone (logged out)
79
+ * or when a live sticky already exists (same-process branch/session switches
80
+ * must not clobber fresher routing). Seeds with the session's effective
81
+ * last-use time so stale resumes still fall through to usage ranking.
82
+ */
83
+ export function seedCredentialPins(authStorage: AuthStorage, sessionManager: SessionManager, sessionId: string): void {
84
+ for (const [provider, pin] of sessionManager.getCredentialPins()) {
85
+ const accounts = authStorage.listOAuthAccounts(provider, sessionId);
86
+ if (accounts.length === 0 || accounts.some(account => account.active)) continue;
87
+ const match = accounts.find(account => credentialPinHash(provider, account) === pin.hash);
88
+ if (!match) continue;
89
+ authStorage.pinSessionOAuthAccount(provider, sessionId, match.credentialId, {
90
+ lastUsedAtMs: pin.lastUsedAt,
91
+ });
92
+ }
93
+ }
@@ -0,0 +1,52 @@
1
+ import { directoryExists } from "@oh-my-pi/pi-utils";
2
+ import { ClaudeSessionStore } from "./claude-session-store";
3
+ import { CodexSessionStore } from "./codex-session-store";
4
+ import type { ForeignSessionInfo, ForeignSessionSource, ForeignSessionStore } from "./foreign-session-store";
5
+ import type { SessionInfo } from "./session-listing";
6
+ import type { SessionManager } from "./session-manager";
7
+
8
+ /** Construct the importer for a supported foreign session source. */
9
+ export function createForeignSessionStore(source: ForeignSessionSource): ForeignSessionStore {
10
+ return source === "claude" ? new ClaudeSessionStore() : new CodexSessionStore();
11
+ }
12
+
13
+ /** Display name for a supported foreign session source. */
14
+ export function foreignSessionSourceName(source: ForeignSessionSource): string {
15
+ return source === "claude" ? "Claude" : "Codex";
16
+ }
17
+
18
+ /** Convert lightweight foreign metadata for the existing session picker. */
19
+ export function foreignSessionInfoToSessionInfo(info: ForeignSessionInfo): SessionInfo {
20
+ const firstMessage = info.firstMessage ?? "(no messages)";
21
+ return {
22
+ path: info.path,
23
+ id: info.id,
24
+ cwd: info.cwd,
25
+ title: info.title,
26
+ created: info.created,
27
+ modified: info.modified,
28
+ messageCount: info.messageCount ?? 0,
29
+ size: 0,
30
+ firstMessage,
31
+ allMessagesText: firstMessage,
32
+ };
33
+ }
34
+
35
+ /** Import and persist one foreign session under a fresh OMP session identity. */
36
+ export async function persistForeignSession(
37
+ store: ForeignSessionStore,
38
+ info: ForeignSessionInfo,
39
+ options?: { fallbackCwd?: string; sessionDir?: string; suppressBreadcrumb?: boolean },
40
+ ): Promise<SessionManager> {
41
+ const imported = await store.load(info);
42
+ imported.appendCustomEntry("foreign_session_import", {
43
+ source: info.source,
44
+ sourceId: info.id,
45
+ sourcePath: info.path,
46
+ sourceCwd: info.cwd,
47
+ });
48
+ if (options?.fallbackCwd && !(await directoryExists(imported.getCwd()))) {
49
+ await imported.moveTo(options.fallbackCwd);
50
+ }
51
+ return await imported.persistCopy(options);
52
+ }
@@ -0,0 +1,29 @@
1
+ import { isRecord, readLines } from "@oh-my-pi/pi-utils";
2
+
3
+ /** One readable object record from a foreign JSONL transcript. */
4
+ export interface ForeignJsonRecord {
5
+ readonly value: Record<string, unknown>;
6
+ readonly line: number;
7
+ }
8
+
9
+ /** Stream valid object records while tolerating malformed or truncated lines. */
10
+ export async function* readForeignJsonRecords(filePath: string): AsyncGenerator<ForeignJsonRecord> {
11
+ const decoder = new TextDecoder();
12
+ let line = 0;
13
+ for await (const bytes of readLines(Bun.file(filePath).stream())) {
14
+ line += 1;
15
+ try {
16
+ const value: unknown = JSON.parse(decoder.decode(bytes));
17
+ if (isRecord(value)) yield { value, line };
18
+ } catch {
19
+ // A partially-written line must not hide the readable transcript prefix.
20
+ }
21
+ }
22
+ }
23
+
24
+ /** Read every valid object record from a foreign JSONL transcript. */
25
+ export async function collectForeignJsonRecords(filePath: string): Promise<ForeignJsonRecord[]> {
26
+ const records: ForeignJsonRecord[] = [];
27
+ for await (const record of readForeignJsonRecords(filePath)) records.push(record);
28
+ return records;
29
+ }
@@ -0,0 +1,26 @@
1
+ import type { SessionManager } from "./session-manager";
2
+
3
+ /** External coding-agent session source supported by OMP imports. */
4
+ export type ForeignSessionSource = "claude" | "codex";
5
+
6
+ /** Lightweight source metadata used to choose a foreign session before loading its transcript. */
7
+ export interface ForeignSessionInfo {
8
+ readonly source: ForeignSessionSource;
9
+ readonly id: string;
10
+ readonly path: string;
11
+ readonly cwd: string;
12
+ readonly title?: string;
13
+ readonly created: Date;
14
+ readonly modified: Date;
15
+ readonly messageCount?: number;
16
+ readonly firstMessage?: string;
17
+ }
18
+
19
+ /** Lists and converts sessions owned by another coding agent. */
20
+ export interface ForeignSessionStore {
21
+ readonly source: ForeignSessionSource;
22
+ /** Lists source sessions without parsing complete transcripts. */
23
+ list(): Promise<ForeignSessionInfo[]>;
24
+ /** Converts one source session into a non-persistent OMP session. */
25
+ load(session: ForeignSessionInfo): Promise<SessionManager>;
26
+ }
@@ -3,6 +3,7 @@ import type { Model, ProviderSessionState, ServiceTier, ServiceTierByFamily, Ser
3
3
  import {
4
4
  clearAnthropicFastModeFallback,
5
5
  Effort,
6
+ isAnthropicFastModeFallbackDisabled,
6
7
  realizesPriorityServiceTier,
7
8
  resolveModelServiceTier,
8
9
  serviceTierFamily,
@@ -581,9 +582,9 @@ export class ModelControls {
581
582
 
582
583
  /**
583
584
  * Classify the current user turn and set the effective thinking level for it.
584
- * Bounded by a timeout + abort; on any failure (no smol model, timeout, parse
585
- * error) it falls back to the provisional concrete level and continues. Never
586
- * throws into the turn, and never clears `#autoThinking` (auto stays active).
585
+ * Bounded by a timeout + abort; on failure it preserves the last classified
586
+ * level, or uses the provisional concrete level before the first resolution.
587
+ * Never throws into the turn, and never clears `#autoThinking`.
587
588
  */
588
589
  async applyAutoThinkingLevel(promptText: string, generation: number): Promise<void> {
589
590
  const model = this.#model;
@@ -596,8 +597,8 @@ export class ModelControls {
596
597
  let resolved: Effort | undefined;
597
598
  if (this.#host.magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
598
599
  // The user explicitly asked for maximum thinking; bypass the classifier
599
- // (and its xhigh auto ceiling) and jump straight to the highest
600
- // supported level for this model.
600
+ // (and the `providers.autoThinkingMaxEffort` ceiling) and jump straight
601
+ // to the highest supported level for this model.
601
602
  resolved = clampAutoThinkingEffort(model, Effort.Max);
602
603
  } else {
603
604
  const controller = new AbortController();
@@ -625,7 +626,7 @@ export class ModelControls {
625
626
 
626
627
  const effort = clampThinkingLevelToCeiling(
627
628
  model,
628
- resolved ?? resolveProvisionalAutoLevel(model),
629
+ resolved ?? this.#autoResolvedLevel ?? resolveProvisionalAutoLevel(model),
629
630
  this.#thinkingLevelCeiling,
630
631
  );
631
632
  if (effort === undefined) return;
@@ -666,7 +667,11 @@ export class ModelControls {
666
667
  */
667
668
  isFastModeActive(): boolean {
668
669
  const model = this.#model;
669
- return !!model && realizesPriorityServiceTier(this.effectiveServiceTier(model), model);
670
+ if (!model || !realizesPriorityServiceTier(this.effectiveServiceTier(model), model)) return false;
671
+ if (model.provider === "anthropic") {
672
+ return !isAnthropicFastModeFallbackDisabled(this.#host.providerSessionState, model);
673
+ }
674
+ return true;
670
675
  }
671
676
 
672
677
  /**
@@ -731,6 +736,9 @@ export class ModelControls {
731
736
  if (this.#serviceTierByFamily[family] === "priority") this.setServiceTierFamily(family, undefined);
732
737
  return true;
733
738
  }
739
+ if (family === "anthropic" && this.#serviceTierByFamily.anthropic === "priority") {
740
+ clearAnthropicFastModeFallback(this.#host.providerSessionState);
741
+ }
734
742
  this.setServiceTierFamily(family, "priority");
735
743
  return true;
736
744
  }
@@ -2,6 +2,7 @@ import {
2
2
  Agent,
3
3
  type AgentMessage,
4
4
  type AgentTool,
5
+ type AgentToolContext,
5
6
  AppendOnlyContextManager,
6
7
  type CompactionSummaryMessage,
7
8
  countTokens,
@@ -16,9 +17,11 @@ import {
16
17
  compactionContextTokens,
17
18
  createCompactionSummaryMessage,
18
19
  estimateTokens,
20
+ NativeCompactionError,
19
21
  prepareCompaction,
20
22
  type SessionMessageEntry,
21
23
  shouldCompact,
24
+ shouldUseProviderNativeCompaction,
22
25
  } from "@oh-my-pi/pi-agent-core/compaction";
23
26
  import type {
24
27
  AssistantMessage,
@@ -48,7 +51,6 @@ import {
48
51
  type AdvisorSeverity,
49
52
  AdvisorTranscriptRecorder,
50
53
  advisorTranscriptFilename,
51
- annotateForStaleness,
52
54
  buildAdvisorQuarantineSourceText,
53
55
  formatAdvisorBatchContent,
54
56
  getOrCreateAdvisorProviderSessionId,
@@ -68,7 +70,8 @@ import {
68
70
  import { MODEL_ROLES } from "../config/model-roles";
69
71
  import { serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
70
72
  import type { Settings } from "../config/settings";
71
- import { CursorExecHandlers } from "../cursor";
73
+ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "../cursor";
74
+ import { bridgeToolMap } from "../cursor-bridge-tools";
72
75
  import { estimateToolSchemaTokens } from "../modes/utils/context-usage";
73
76
  import type { PlanModeState } from "../plan-mode/state";
74
77
  import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
@@ -178,12 +181,44 @@ interface AdvisorRuntimeDescriptor {
178
181
  export interface SessionAdvisorsOptions {
179
182
  enabled: boolean;
180
183
  tools?: AgentTool[];
184
+ /**
185
+ * Build a `grep` honoring a Cursor `pi_grep` frame's own context width and
186
+ * match cap. The advisor's tools are fixed instances carrying session
187
+ * defaults, so without this an advisor running against Cursor silently
188
+ * drops both fields — the same gap the primary bridge closes.
189
+ */
190
+ createGrepTool?(options: { context?: number; totalMatchLimit?: number }): AgentTool | undefined;
191
+ /**
192
+ * Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs. The
193
+ * advisor's own instance follows the configured `edit.mode` (`hashline` by
194
+ * default), whose schema the frame's `old_text`/`new_text` pairs do not
195
+ * match, so without this every native advisor edit fails validation.
196
+ */
197
+ createEditTool?(): AgentTool | undefined;
198
+ /**
199
+ * The execute-time context the bridge's tools resolve approval from.
200
+ *
201
+ * `ExtensionToolWrapper` reads the approval mode, per-tool policies and
202
+ * `autoApprove` only from here; with none it falls back to `yolo` and empty
203
+ * policies, so a native frame would run past a configured `ask` or `deny`.
204
+ */
205
+ getToolContext?: () => AgentToolContext | undefined;
206
+ /**
207
+ * The live MCP connections Cursor's resource frames answer from.
208
+ *
209
+ * Advisors share the session's connections and may hold tools from those
210
+ * same servers, so without this their frames report that every server
211
+ * advertises nothing.
212
+ */
213
+ mcpResources?: CursorMcpResourceAdapter;
181
214
  watchdogPrompt?: string;
182
215
  sharedInstructions?: string;
183
216
  contextPrompt?: string;
184
217
  configs?: AdvisorConfig[];
185
218
  streamFn?: StreamFn;
186
219
  transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
220
+ /** Advisor spend already persisted for this session, restored on resume. */
221
+ initialCosts?: ReadonlyMap<string, number>;
187
222
  }
188
223
 
189
224
  /** Options accepted when an advisor injects a primary-session message. */
@@ -222,7 +257,11 @@ export interface SessionAdvisorsHost {
222
257
  hasPendingNextTurnMessages(): boolean;
223
258
  convertToLlmForSideRequest(messages: AgentMessage[]): Message[];
224
259
  effectiveServiceTier(model: Model): ServiceTier | undefined;
225
- resolveContextPromotionTarget(currentModel: Model, contextWindow: number): Promise<Model | undefined>;
260
+ resolveContextPromotionTarget(
261
+ currentModel: Model,
262
+ contextWindow: number,
263
+ signal: AbortSignal,
264
+ ): Promise<Model | undefined>;
226
265
  resolveCompactionModelCandidates(preferredModel: Model | null | undefined, availableModels: Model[]): Model[];
227
266
  resolveRetryFallbackRole(currentSelector: string, currentModel?: Model | null): string | undefined;
228
267
  findRetryFallbackCandidates(
@@ -245,6 +284,10 @@ export class SessionAdvisors {
245
284
  readonly #host: SessionAdvisorsHost;
246
285
  #advisorEnabled: boolean;
247
286
  #advisorTools: AgentTool[] | undefined;
287
+ #advisorCreateGrepTool: SessionAdvisorsOptions["createGrepTool"];
288
+ #advisorCreateEditTool: SessionAdvisorsOptions["createEditTool"];
289
+ #advisorGetToolContext: SessionAdvisorsOptions["getToolContext"];
290
+ #advisorMcpResources: SessionAdvisorsOptions["mcpResources"];
248
291
  #advisorWatchdogPrompt: string | undefined;
249
292
  #advisorSharedInstructions: string | undefined;
250
293
  #advisorContextPrompt: string | undefined;
@@ -267,12 +310,17 @@ export class SessionAdvisors {
267
310
  this.#host = host;
268
311
  this.#advisorEnabled = options.enabled;
269
312
  this.#advisorTools = options.tools;
313
+ this.#advisorCreateGrepTool = options.createGrepTool;
314
+ this.#advisorCreateEditTool = options.createEditTool;
315
+ this.#advisorGetToolContext = options.getToolContext;
316
+ this.#advisorMcpResources = options.mcpResources;
270
317
  this.#advisorWatchdogPrompt = options.watchdogPrompt;
271
318
  this.#advisorSharedInstructions = options.sharedInstructions;
272
319
  this.#advisorContextPrompt = options.contextPrompt;
273
320
  this.#advisorConfigs = options.configs;
274
321
  this.#advisorStreamFn = options.streamFn;
275
322
  this.#transformProviderContext = options.transformProviderContext;
323
+ if (options.initialCosts) this.#advisorCosts = new Map(options.initialCosts);
276
324
  if (this.#advisorEnabled) this.#buildAdvisorRuntime();
277
325
  }
278
326
 
@@ -314,6 +362,15 @@ export class SessionAdvisors {
314
362
  this.#stopAdvisorRuntime();
315
363
  }
316
364
 
365
+ /**
366
+ * Pause advisor work while old-session recorder feeds remain attached, then
367
+ * detach only after any active prompt has settled.
368
+ */
369
+ async drainAndDetachRecorders(): Promise<void> {
370
+ await Promise.all(this.#advisors.map(advisor => advisor.runtime.pauseForSessionTransition()));
371
+ await this.detachAndCloseRecorders();
372
+ }
373
+
317
374
  /** Detaches and drains recorder feeds before transcript artifacts are removed. */
318
375
  async detachAndCloseRecorders(): Promise<void> {
319
376
  const closes: Promise<void>[] = [];
@@ -326,6 +383,14 @@ export class SessionAdvisors {
326
383
  await Promise.all(closes);
327
384
  }
328
385
 
386
+ /** Reattach recorder feeds and resume work after a rolled-back or preserving transition. */
387
+ reattachRecorderFeeds(): void {
388
+ for (const advisor of this.#advisors) {
389
+ if (!advisor.agentUnsubscribe) this.#attachAdvisorRecorderFeed(advisor);
390
+ advisor.runtime.resumeAfterSessionTransition();
391
+ }
392
+ }
393
+
329
394
  /** Re-primes advisor transcript views across a conversation boundary. */
330
395
  resetSessionState(options: { preserveCost?: boolean } = {}): void {
331
396
  this.#resetAdvisorSessionState(options.preserveCost === true);
@@ -336,6 +401,11 @@ export class SessionAdvisors {
336
401
  this.#advisorCosts.clear();
337
402
  }
338
403
 
404
+ /** Replace the ledger with the spend recorded for the session becoming active. */
405
+ restoreCost(costs: ReadonlyMap<string, number>): void {
406
+ this.#advisorCosts = new Map(costs);
407
+ }
408
+
339
409
  /**
340
410
  * Rebind every live advisor to the active primary conversation's provider
341
411
  * identity (session id, prompt-cache key, credential + metadata resolvers,
@@ -690,11 +760,29 @@ export class SessionAdvisors {
690
760
  // to delete workspace files it was never granted (issue #5680 review).
691
761
  const advisorCanMutateFiles = advisorToolMap.has("write") || advisorToolMap.has("edit");
692
762
  if (advisorCanMutateFiles) availableAdvisorToolNames.add("delete");
763
+ // `pi_edit` speaks `replace`'s `old_text`/`new_text` schema, which the
764
+ // advisor's ordinary `EditTool` (built at the session's configured
765
+ // `edit.mode`, `hashline` by default) does not accept. The bridge map
766
+ // swaps in a `replace` instance for the exec channel only — the
767
+ // advisor's own loop keeps the tool it was given — and only when
768
+ // `edit` was actually granted.
693
769
  const advisorCursorExecHandlers = new CursorExecHandlers({
694
770
  cwd: this.#host.sessionManager.getCwd(),
695
771
  getCwd: () => this.#host.sessionManager.getCwd(),
696
- tools: advisorToolMap,
697
- allowNativeDelete: advisorCanMutateFiles,
772
+ tools: bridgeToolMap(advisorToolMap, this.#advisorCreateEditTool),
773
+ // Approval mode, per-tool policies and `autoApprove` live only on
774
+ // this context; without it every bridge tool resolves as `yolo`.
775
+ getToolContext: this.#advisorGetToolContext,
776
+ allowDirectFileMutation: advisorCanMutateFiles,
777
+ // Gated on the advisor's own grant: the factory builds a fresh
778
+ // tool, so handing it over unconditionally would give a roster
779
+ // without `grep` a search tool it was denied.
780
+ createGrepTool: advisorToolMap.has("grep") ? this.#advisorCreateGrepTool : undefined,
781
+ // Advisors share the session's live MCP connections, so their
782
+ // resource frames answer from the same catalog the primary sees.
783
+ // Not gated on a tool grant: reading what a server advertises is
784
+ // not the same permission as calling one of its tools.
785
+ mcpResources: this.#advisorMcpResources,
698
786
  });
699
787
  const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
700
788
  const advisorStreamFn: StreamFn = (requestModel, context, options) =>
@@ -785,10 +873,13 @@ export class SessionAdvisors {
785
873
  const runtime = new AdvisorRuntime(advisorAgentFacade, {
786
874
  snapshotMessages: () => this.#host.agent.state.messages,
787
875
  enqueueAdvice: (note, severity) => this.#routeAdvice(advisorRef, note, severity),
788
- maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
876
+ maintainContext: (incomingTokens, signal) =>
877
+ this.#maintainAdvisorContext(advisorRef, incomingTokens, signal),
789
878
  obfuscator: this.#host.obfuscator,
879
+ getModelIdentity: () => formatModelString(advisorRef.agent.state.model),
790
880
  beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
791
- onTurnError: (error, failedMessages) => this.#recoverAdvisorTurn(advisorRef, error, failedMessages),
881
+ onTurnError: (error, failedMessages, signal) =>
882
+ this.#recoverAdvisorTurn(advisorRef, error, failedMessages, signal),
792
883
  onTurnSuccess: async () => {
793
884
  const fallback = advisorRef.retryFallback;
794
885
  if (!advisorRef.retryFallbackPendingSuccess || !fallback) return;
@@ -890,10 +981,6 @@ export class SessionAdvisors {
890
981
  logger.debug("advisor advice suppressed by emission guard", { severity, advisor: advisor.name });
891
982
  return;
892
983
  }
893
- // When newer primary turns already arrived while the advisor model was
894
- // processing this batch, the advice was generated without seeing them.
895
- // Append a lightweight staleness caveat so the primary can weigh recency.
896
- const deliveredNote = annotateForStaleness(note, advisor.runtime.hasFreshBacklog);
897
984
  // The implicit single ("default") advisor stamps no source name, so its
898
985
  // agent-facing `<advisory>` bytes stay identical to the pre-multi-advisor path.
899
986
  const source = advisor.slug ? advisor.name : undefined;
@@ -911,10 +998,10 @@ export class SessionAdvisors {
911
998
  interruptImmuneTurnActive: interrupting && this.#isAdvisorInterruptImmuneTurnActive(),
912
999
  });
913
1000
  if (channel === "aside") {
914
- this.#host.yieldQueue.enqueue("advisor", { note: deliveredNote, severity, advisor: source });
1001
+ this.#host.yieldQueue.enqueue("advisor", { note, severity, advisor: source });
915
1002
  return;
916
1003
  }
917
- const notes: AdvisorNote[] = [{ note: deliveredNote, severity, advisor: source }];
1004
+ const notes: AdvisorNote[] = [{ note, severity, advisor: source }];
918
1005
  const content = formatAdvisorBatchContent(notes);
919
1006
  const details = { notes } satisfies AdvisorMessageDetails;
920
1007
  if (channel === "preserve") {
@@ -1020,7 +1107,7 @@ export class SessionAdvisors {
1020
1107
  }
1021
1108
 
1022
1109
  /** Restore an advisor's configured primary once its fallback cooldown expires. */
1023
- async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor): Promise<void> {
1110
+ async #maybeRestoreAdvisorRetryFallbackPrimary(advisor: ActiveAdvisor, signal: AbortSignal): Promise<void> {
1024
1111
  const fallback = advisor.retryFallback;
1025
1112
  if (!fallback || getRetryFallbackRevertPolicy(this.#host.settings) !== "cooldown-expiry") return;
1026
1113
 
@@ -1048,8 +1135,9 @@ export class SessionAdvisors {
1048
1135
  const primaryModel =
1049
1136
  resolvedPrimary.model ?? this.#host.modelRegistry.find(originalSelector.provider, originalSelector.id);
1050
1137
  if (!primaryModel) return;
1051
- const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId);
1138
+ const apiKey = await this.#host.modelRegistry.getApiKey(primaryModel, advisor.providerSessionId, { signal });
1052
1139
  if (!apiKey) return;
1140
+ signal.throwIfAborted();
1053
1141
 
1054
1142
  const thinkingToApply =
1055
1143
  advisor.thinkingLevel === fallback.lastAppliedThinkingLevel
@@ -1069,6 +1157,7 @@ export class SessionAdvisors {
1069
1157
  advisor: ActiveAdvisor,
1070
1158
  error: unknown,
1071
1159
  failedMessages: readonly AgentMessage[],
1160
+ signal: AbortSignal,
1072
1161
  ): Promise<boolean> {
1073
1162
  if (error instanceof AdvisorOutputQuarantinedError) return false;
1074
1163
 
@@ -1091,6 +1180,7 @@ export class SessionAdvisors {
1091
1180
  retryAfterMs: extractRetryHint(undefined, message),
1092
1181
  baseUrl: currentModel.baseUrl,
1093
1182
  modelId: currentModel.id,
1183
+ signal,
1094
1184
  },
1095
1185
  );
1096
1186
  return outcome.switched;
@@ -1122,6 +1212,7 @@ export class SessionAdvisors {
1122
1212
  retryAfterMs,
1123
1213
  baseUrl: currentModel.baseUrl,
1124
1214
  modelId: currentModel.id,
1215
+ signal,
1125
1216
  },
1126
1217
  );
1127
1218
  if (outcome.switched) return true;
@@ -1139,8 +1230,9 @@ export class SessionAdvisors {
1139
1230
  const resolved = resolveModelOverride([selector.raw], this.#host.modelRegistry, this.#host.settings);
1140
1231
  const candidate = resolved.model ?? this.#host.modelRegistry.find(selector.provider, selector.id);
1141
1232
  if (!candidate || modelsAreEqual(candidate, currentModel)) continue;
1142
- const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId);
1233
+ const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisor.providerSessionId, { signal });
1143
1234
  if (!apiKey) continue;
1235
+ signal.throwIfAborted();
1144
1236
 
1145
1237
  const originalThinkingLevel = advisor.thinkingLevel;
1146
1238
  const requestedThinkingLevel = selector.thinkingLevel ?? originalThinkingLevel;
@@ -1168,13 +1260,18 @@ export class SessionAdvisors {
1168
1260
  return false;
1169
1261
  }
1170
1262
 
1171
- async #promoteAdvisorContextModel(advisor: ActiveAdvisor, currentModel: Model): Promise<boolean> {
1263
+ async #promoteAdvisorContextModel(
1264
+ advisor: ActiveAdvisor,
1265
+ currentModel: Model,
1266
+ signal: AbortSignal,
1267
+ ): Promise<boolean> {
1172
1268
  const promotionSettings = this.#host.settings.getGroup("contextPromotion");
1173
1269
  if (!promotionSettings.enabled) return false;
1174
1270
  const contextWindow = currentModel.contextWindow ?? 0;
1175
1271
  if (contextWindow <= 0) return false;
1176
- const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow);
1272
+ const targetModel = await this.#host.resolveContextPromotionTarget(currentModel, contextWindow, signal);
1177
1273
  if (!targetModel) return false;
1274
+ signal.throwIfAborted();
1178
1275
 
1179
1276
  // Preserve this advisor's own thinking level (a configured `model:...:high`
1180
1277
  // keeps its suffix across a promotion); only the model changes.
@@ -1198,8 +1295,12 @@ export class SessionAdvisors {
1198
1295
  }
1199
1296
  }
1200
1297
 
1201
- async #maintainAdvisorContext(advisor: ActiveAdvisor, incomingTokens: number): Promise<boolean> {
1202
- await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor);
1298
+ async #maintainAdvisorContext(
1299
+ advisor: ActiveAdvisor,
1300
+ incomingTokens: number,
1301
+ signal: AbortSignal,
1302
+ ): Promise<boolean> {
1303
+ await this.#maybeRestoreAdvisorRetryFallbackPrimary(advisor, signal);
1203
1304
  const agent = advisor.agent;
1204
1305
 
1205
1306
  const compactionSettings = this.#host.settings.getGroup("compaction");
@@ -1234,7 +1335,7 @@ export class SessionAdvisors {
1234
1335
  }
1235
1336
 
1236
1337
  // 1. Try promotion first
1237
- if (await this.#promoteAdvisorContextModel(advisor, advisorModel)) {
1338
+ if (await this.#promoteAdvisorContextModel(advisor, advisorModel, signal)) {
1238
1339
  // Promotion succeeded, check if new model has enough space
1239
1340
  const newModel = agent.state.model;
1240
1341
  const newWindow = newModel.contextWindow ?? 0;
@@ -1301,6 +1402,7 @@ export class SessionAdvisors {
1301
1402
 
1302
1403
  let compactResult: CompactionResult | undefined;
1303
1404
  let lastError: unknown;
1405
+ let nativeCompactionFailure: { error: NativeCompactionError; provider: string } | undefined;
1304
1406
  // Instrument the advisor's overflow-compaction one-shot like the primary
1305
1407
  // compaction path so the advisor model's maintenance call also emits spans.
1306
1408
  const telemetry = resolveTelemetry(agent.telemetry, advisorProviderSessionId);
@@ -1312,8 +1414,16 @@ export class SessionAdvisors {
1312
1414
  });
1313
1415
 
1314
1416
  for (const candidate of candidates) {
1315
- const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId);
1417
+ const apiKey = await this.#host.modelRegistry.getApiKey(candidate, advisorProviderSessionId, { signal });
1316
1418
  if (!apiKey) continue;
1419
+ if (
1420
+ nativeCompactionFailure &&
1421
+ (candidate.provider !== nativeCompactionFailure.provider ||
1422
+ !shouldUseProviderNativeCompaction(candidate, compactionSettings))
1423
+ ) {
1424
+ throw nativeCompactionFailure.error;
1425
+ }
1426
+
1317
1427
  // The advisor overflow-compaction one-shot bypasses the advisor `Agent`,
1318
1428
  // so its installed metadata resolver never runs. Emit the same
1319
1429
  // `metadata.user_id` identity here (resolved per candidate provider,
@@ -1329,7 +1439,7 @@ export class SessionAdvisors {
1329
1439
  candidate,
1330
1440
  this.#host.modelRegistry.resolver(candidate, advisorProviderSessionId),
1331
1441
  undefined,
1332
- undefined,
1442
+ signal,
1333
1443
  {
1334
1444
  thinkingLevel: advisorCompactionThinkingLevel,
1335
1445
  convertToLlm: messages => this.#host.convertToLlmForSideRequest(messages),
@@ -1344,10 +1454,19 @@ export class SessionAdvisors {
1344
1454
  );
1345
1455
  break;
1346
1456
  } catch (error) {
1457
+ if (signal.aborted) throw error;
1458
+ const id = AIError.classify(error, candidate.api);
1459
+ if (error instanceof NativeCompactionError && !AIError.is(id, AIError.Flag.AuthFailed)) {
1460
+ nativeCompactionFailure ??= { error, provider: candidate.provider };
1461
+ lastError = nativeCompactionFailure.error;
1462
+ continue;
1463
+ }
1347
1464
  lastError = error;
1348
1465
  }
1349
1466
  }
1350
1467
 
1468
+ if (!compactResult && nativeCompactionFailure) throw nativeCompactionFailure.error;
1469
+
1351
1470
  if (!compactResult) {
1352
1471
  logger.warn("Advisor compaction failed, falling back to re-prime", { error: String(lastError) });
1353
1472
  return true;