@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5

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 (112) hide show
  1. package/CHANGELOG.md +86 -43
  2. package/dist/cli.js +3540 -3521
  3. package/dist/types/advisor/advise-tool.d.ts +3 -0
  4. package/dist/types/advisor/runtime.d.ts +7 -1
  5. package/dist/types/async/job-manager.d.ts +2 -0
  6. package/dist/types/config/model-registry.d.ts +7 -0
  7. package/dist/types/config/model-resolver.d.ts +8 -0
  8. package/dist/types/config/models-config-schema.d.ts +10 -0
  9. package/dist/types/config/models-config.d.ts +6 -0
  10. package/dist/types/dap/client.d.ts +10 -0
  11. package/dist/types/dap/types.d.ts +6 -5
  12. package/dist/types/extensibility/extensions/runner.d.ts +4 -2
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
  14. package/dist/types/launch/spawn-options.d.ts +10 -0
  15. package/dist/types/launch/spawn-options.test.d.ts +1 -0
  16. package/dist/types/modes/components/status-line/component.d.ts +2 -3
  17. package/dist/types/modes/components/status-line/types.d.ts +3 -1
  18. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  19. package/dist/types/modes/components/transcript-container.d.ts +4 -3
  20. package/dist/types/modes/components/tree-selector.d.ts +6 -2
  21. package/dist/types/modes/interactive-mode.d.ts +2 -0
  22. package/dist/types/modes/print-mode.d.ts +4 -0
  23. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  24. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  25. package/dist/types/modes/types.d.ts +2 -0
  26. package/dist/types/sdk.d.ts +2 -0
  27. package/dist/types/session/agent-session.d.ts +19 -1
  28. package/dist/types/session/messages.d.ts +6 -0
  29. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
  30. package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
  31. package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
  32. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  33. package/dist/types/tools/gh.d.ts +5 -3
  34. package/dist/types/tools/hub/index.d.ts +1 -1
  35. package/dist/types/tools/hub/jobs.d.ts +1 -0
  36. package/dist/types/tools/hub/types.d.ts +2 -0
  37. package/dist/types/tools/tool-timeouts.d.ts +1 -1
  38. package/dist/types/utils/git.d.ts +33 -0
  39. package/dist/types/web/search/providers/codex.d.ts +1 -1
  40. package/package.json +12 -12
  41. package/src/advisor/__tests__/advisor.test.ts +150 -0
  42. package/src/advisor/advise-tool.ts +4 -0
  43. package/src/advisor/runtime.ts +38 -14
  44. package/src/async/job-manager.ts +3 -0
  45. package/src/cli/bench-cli.ts +8 -2
  46. package/src/cli/dry-balance-cli.ts +1 -0
  47. package/src/config/model-registry.ts +89 -8
  48. package/src/config/model-resolver.ts +78 -14
  49. package/src/config/models-config-schema.ts +1 -0
  50. package/src/config/settings.ts +3 -1
  51. package/src/dap/client.ts +168 -1
  52. package/src/dap/config.ts +51 -1
  53. package/src/dap/session.ts +575 -234
  54. package/src/dap/types.ts +6 -5
  55. package/src/discovery/agents.ts +2 -2
  56. package/src/extensibility/extensions/runner.ts +6 -4
  57. package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
  58. package/src/launch/broker.ts +34 -31
  59. package/src/launch/client.ts +7 -1
  60. package/src/launch/spawn-options.test.ts +31 -0
  61. package/src/launch/spawn-options.ts +17 -0
  62. package/src/lsp/types.ts +5 -1
  63. package/src/main.ts +17 -4
  64. package/src/mcp/transports/stdio.test.ts +9 -1
  65. package/src/modes/components/ask-dialog.ts +137 -73
  66. package/src/modes/components/status-line/component.ts +2 -2
  67. package/src/modes/components/status-line/segments.ts +20 -3
  68. package/src/modes/components/status-line/types.ts +3 -1
  69. package/src/modes/components/tool-execution.ts +5 -0
  70. package/src/modes/components/transcript-container.ts +23 -122
  71. package/src/modes/components/tree-selector.ts +10 -4
  72. package/src/modes/controllers/event-controller.ts +1 -2
  73. package/src/modes/controllers/input-controller.ts +1 -1
  74. package/src/modes/controllers/selector-controller.ts +29 -8
  75. package/src/modes/interactive-mode.ts +40 -8
  76. package/src/modes/noninteractive-dispose.test.ts +12 -1
  77. package/src/modes/print-mode.ts +21 -9
  78. package/src/modes/rpc/rpc-input.ts +38 -0
  79. package/src/modes/rpc/rpc-mode.ts +7 -2
  80. package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
  81. package/src/modes/types.ts +2 -0
  82. package/src/modes/utils/ui-helpers.ts +3 -2
  83. package/src/prompts/tools/browser.md +3 -2
  84. package/src/prompts/tools/debug.md +2 -7
  85. package/src/prompts/tools/github.md +6 -1
  86. package/src/prompts/tools/hub.md +4 -2
  87. package/src/prompts/tools/task-async-contract.md +1 -0
  88. package/src/prompts/tools/task.md +9 -2
  89. package/src/sdk.ts +38 -6
  90. package/src/session/agent-session.ts +395 -162
  91. package/src/session/messages.test.ts +91 -0
  92. package/src/session/messages.ts +248 -110
  93. package/src/slash-commands/builtin-registry.ts +1 -0
  94. package/src/task/executor.ts +59 -33
  95. package/src/task/index.ts +46 -9
  96. package/src/task/worktree.ts +10 -0
  97. package/src/tools/browser/aria/aria-snapshot.ts +36 -8
  98. package/src/tools/browser/cmux/cmux-tab.ts +2 -1
  99. package/src/tools/browser/cmux/socket-client.ts +139 -3
  100. package/src/tools/browser/run-cancellation.ts +4 -0
  101. package/src/tools/browser/tab-protocol.ts +2 -0
  102. package/src/tools/browser/tab-supervisor.ts +21 -11
  103. package/src/tools/browser/tab-worker.ts +199 -33
  104. package/src/tools/debug.ts +3 -0
  105. package/src/tools/gh.ts +40 -2
  106. package/src/tools/hub/index.ts +4 -1
  107. package/src/tools/hub/jobs.ts +42 -1
  108. package/src/tools/hub/types.ts +2 -0
  109. package/src/tools/tool-timeouts.ts +1 -1
  110. package/src/utils/git.ts +237 -0
  111. package/src/web/search/index.ts +9 -5
  112. package/src/web/search/providers/codex.ts +195 -99
@@ -7,9 +7,10 @@ import type { OutputMeta } from "./output-meta.js";
7
7
  /** Runs `gh --json` for issue data, retrying without optional stateReason on older gh releases. */
8
8
  export declare function githubIssueJsonWithStateReasonFallback<T>(cwd: string, args: readonly string[], signal: AbortSignal | undefined, options?: git.GhCommandOptions): Promise<T>;
9
9
  declare const githubSchema: import("arktype/internal/variants/object.ts").ObjectType<{
10
- op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
10
+ op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
11
11
  repo?: string | undefined;
12
12
  branch?: string | undefined;
13
+ path?: string | undefined;
13
14
  pr?: string | string[] | undefined;
14
15
  force?: boolean | undefined;
15
16
  forceWithLease?: boolean | undefined;
@@ -219,14 +220,15 @@ export declare class GithubTool implements AgentTool<typeof githubSchema, GhTool
219
220
  private readonly session;
220
221
  readonly name = "github";
221
222
  readonly approval: (args: unknown) => ToolApprovalDecision;
222
- readonly summary = "Interact with GitHub issues, pull requests, and repositories";
223
+ readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
223
224
  readonly loadMode = "discoverable";
224
225
  readonly label = "GitHub";
225
226
  readonly description: string;
226
227
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
227
- op: "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
228
+ op: "file_read" | "pr_checkout" | "pr_create" | "pr_push" | "repo_view" | "run_watch" | "search_code" | "search_commits" | "search_issues" | "search_prs" | "search_repos";
228
229
  repo?: string | undefined;
229
230
  branch?: string | undefined;
231
+ path?: string | undefined;
230
232
  pr?: string | string[] | undefined;
231
233
  force?: boolean | undefined;
232
234
  forceWithLease?: boolean | undefined;
@@ -117,7 +117,7 @@ export declare class HubTool implements AgentTool<typeof hubSchema, HubDetails>
117
117
  timeout?: number | undefined;
118
118
  }, {}>;
119
119
  readonly strict = true;
120
- readonly interruptible = true;
120
+ readonly interruptible: (params: Partial<HubParams>) => boolean;
121
121
  readonly loadMode = "essential";
122
122
  readonly examples: readonly ToolExample<typeof hubSchema.infer>[];
123
123
  constructor(session: ToolSession);
@@ -44,6 +44,7 @@ interface TrackedJobLike {
44
44
  status: string;
45
45
  label: string;
46
46
  startTime: number;
47
+ latestDetails?: Record<string, unknown>;
47
48
  resultText?: string;
48
49
  errorText?: string;
49
50
  }
@@ -30,6 +30,8 @@ export interface JobSnapshot {
30
30
  status: "running" | "completed" | "failed" | "cancelled";
31
31
  label: string;
32
32
  durationMs: number;
33
+ /** Effective task model selector, including an explicit reasoning suffix when configured. */
34
+ resolvedModel?: string;
33
35
  resultText?: string;
34
36
  errorText?: string;
35
37
  }
@@ -35,7 +35,7 @@ export declare const TOOL_TIMEOUTS: {
35
35
  readonly lsp: {
36
36
  readonly default: 20;
37
37
  readonly min: 5;
38
- readonly max: 60;
38
+ readonly max: 300;
39
39
  };
40
40
  readonly debug: {
41
41
  readonly default: 30;
@@ -219,6 +219,39 @@ export declare function fetch(cwd: string, remote: string, source: string, targe
219
219
  export declare function readTree(cwd: string, treeish: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<void>;
220
220
  /** Write the current index as a tree and return its object id. */
221
221
  export declare function writeTree(cwd: string, options?: Pick<CommandOptions, "env" | "signal">): Promise<string>;
222
+ /** Outcome of {@link detachGitDir}. */
223
+ export type DetachGitDirResult =
224
+ /** `worktreeRoot` had no `.git`; nothing to detach. */
225
+ "no-git"
226
+ /** `.git` already resolves to an independent object DB — left untouched. */
227
+ | "independent"
228
+ /** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
229
+ | "detached";
230
+ /**
231
+ * Sever a copied/mounted working tree from the git metadata it shares with a
232
+ * source checkout, turning it into a standalone repository that borrows the
233
+ * source object database through `objects/info/alternates`.
234
+ *
235
+ * Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
236
+ * copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
237
+ * worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
238
+ * the copy still resolves HEAD/index/refs through the source repo — a task's
239
+ * `git checkout`/`commit` inside the isolation then mutates the *parent*
240
+ * checkout. The rcopy `git worktree add` path leaks the other way: task
241
+ * branches land in the shared ref namespace and stack on each other.
242
+ *
243
+ * After detaching, the working tree keeps its files verbatim while:
244
+ * - HEAD, refs, and the index are frozen to the snapshot at call time;
245
+ * - all commits/branches the task creates stay private to the isolation;
246
+ * - objects resolve against `sourceCommonDir` via alternates, so history reads
247
+ * and later `git fetch <merged>` object transfer keep working;
248
+ * - the source checkout's HEAD, branch, index, and working tree are untouched.
249
+ *
250
+ * A full-copy `.git` (non-worktree source) already owns its object DB and is
251
+ * returned as `"independent"` without modification. `worktreeRoot` without a
252
+ * `.git` yields `"no-git"`.
253
+ */
254
+ export declare function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult>;
222
255
  /** Run `git show` on a revision. */
223
256
  export declare const show: ((cwd: string, revision: string, options?: {
224
257
  format?: string;
@@ -25,7 +25,7 @@ export interface CodexSearchParams {
25
25
  */
26
26
  export declare function searchCodex(params: SearchParams): Promise<SearchResponse>;
27
27
  /**
28
- * Checks if Codex web search is available.
28
+ * Checks whether Codex web search has an API key or OAuth credential.
29
29
  */
30
30
  export declare function hasCodexSearch(authStorage: AuthStorage): Promise<boolean>;
31
31
  /** Search provider for OpenAI Codex web search. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "17.0.4",
4
+ "version": "17.0.5",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -52,17 +52,17 @@
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.0.4",
56
- "@oh-my-pi/omp-stats": "17.0.4",
57
- "@oh-my-pi/pi-agent-core": "17.0.4",
58
- "@oh-my-pi/pi-ai": "17.0.4",
59
- "@oh-my-pi/pi-catalog": "17.0.4",
60
- "@oh-my-pi/pi-mnemopi": "17.0.4",
61
- "@oh-my-pi/pi-natives": "17.0.4",
62
- "@oh-my-pi/pi-tui": "17.0.4",
63
- "@oh-my-pi/pi-utils": "17.0.4",
64
- "@oh-my-pi/pi-wire": "17.0.4",
65
- "@oh-my-pi/snapcompact": "17.0.4",
55
+ "@oh-my-pi/hashline": "17.0.5",
56
+ "@oh-my-pi/omp-stats": "17.0.5",
57
+ "@oh-my-pi/pi-agent-core": "17.0.5",
58
+ "@oh-my-pi/pi-ai": "17.0.5",
59
+ "@oh-my-pi/pi-catalog": "17.0.5",
60
+ "@oh-my-pi/pi-mnemopi": "17.0.5",
61
+ "@oh-my-pi/pi-natives": "17.0.5",
62
+ "@oh-my-pi/pi-tui": "17.0.5",
63
+ "@oh-my-pi/pi-utils": "17.0.5",
64
+ "@oh-my-pi/pi-wire": "17.0.5",
65
+ "@oh-my-pi/snapcompact": "17.0.5",
66
66
  "@opentelemetry/api": "^1.9.1",
67
67
  "@opentelemetry/api-logs": "^0.220.0",
68
68
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "bun:test";
2
2
  import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core";
3
3
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
4
4
  import * as AIError from "@oh-my-pi/pi-ai/error";
5
+ import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
5
6
  import type { TUI } from "@oh-my-pi/pi-tui";
6
7
  import { type } from "arktype";
7
8
  import type { ModelRegistry } from "../../config/model-registry";
@@ -519,6 +520,58 @@ describe("advisor", () => {
519
520
  expect(message.content).toBe(originalContent);
520
521
  });
521
522
 
523
+ it("keeps advise when Cursor emits exec-resolved native tools outside the grant (issue #5900)", () => {
524
+ const message = {
525
+ role: "assistant",
526
+ content: [
527
+ { type: "text", text: "Investigating the networking design." },
528
+ {
529
+ type: "toolCall",
530
+ id: "tc-grep",
531
+ name: "grep",
532
+ arguments: { pattern: "backoff" },
533
+ [kCursorExecResolved]: true,
534
+ },
535
+ {
536
+ type: "toolCall",
537
+ id: "tc-bash",
538
+ name: "bash",
539
+ arguments: { command: "ls" },
540
+ [kCursorExecResolved]: true,
541
+ },
542
+ {
543
+ type: "toolCall",
544
+ id: "tc-advise",
545
+ name: "advise",
546
+ arguments: { note: "The retry backoff looks unbounded." },
547
+ },
548
+ ],
549
+ stopReason: "toolUse",
550
+ } as unknown as AssistantMessage;
551
+ const originalContent = message.content;
552
+
553
+ // Grant is `advise` only (WATCHDOG.yml `tools: []`). The native grep/bash
554
+ // frames already ran server-side through the advisor-scoped bridge, which
555
+ // rejected them in-band; they must not discard the legitimate advise.
556
+ expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBeUndefined();
557
+ expect(message.stopReason).toBe("toolUse");
558
+ expect(message.content).toBe(originalContent);
559
+ expect(JSON.stringify(message)).toContain("unbounded");
560
+ });
561
+
562
+ it("still quarantines an ungranted native tool that was not exec-resolved", () => {
563
+ const message = {
564
+ role: "assistant",
565
+ content: [{ type: "toolCall", id: "tc-bash", name: "bash", arguments: { command: "ls" } }],
566
+ stopReason: "toolUse",
567
+ } as unknown as AssistantMessage;
568
+
569
+ expect(quarantineAdvisorUnsafeOutput(message, new Set(["advise"]))).toBe(
570
+ "Advisor response quarantined: requested unavailable tool bash",
571
+ );
572
+ expect(message.stopReason).toBe("error");
573
+ });
574
+
522
575
  it("sanitizes destructive advise notes even when advise is an allowed tool", () => {
523
576
  const message = {
524
577
  role: "assistant",
@@ -865,6 +918,65 @@ describe("advisor", () => {
865
918
  expect(promptInputs[1]).toContain("second");
866
919
  });
867
920
 
921
+ it("waits for an in-flight review within the catch-up deadline", async () => {
922
+ const promptStarted = Promise.withResolvers<void>();
923
+ const releasePrompt = Promise.withResolvers<void>();
924
+ const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
925
+ const agent: AdvisorAgent = {
926
+ prompt: async () => {
927
+ promptStarted.resolve();
928
+ await releasePrompt.promise;
929
+ },
930
+ abort: () => {},
931
+ reset: () => {},
932
+ state: { messages: [] },
933
+ };
934
+ const runtime = new AdvisorRuntime(agent, {
935
+ snapshotMessages: () => messages,
936
+ enqueueAdvice: () => {},
937
+ });
938
+
939
+ runtime.onTurnEnd();
940
+ await promptStarted.promise;
941
+ let settled = false;
942
+ const catchup = runtime.waitForCatchup(1000, 1).then(caughtUp => {
943
+ settled = true;
944
+ return caughtUp;
945
+ });
946
+ await Promise.resolve();
947
+ expect(settled).toBe(false);
948
+
949
+ releasePrompt.resolve();
950
+ expect(await catchup).toBe(true);
951
+ });
952
+
953
+ it("reports an in-flight review that exceeds the catch-up deadline", async () => {
954
+ const promptStarted = Promise.withResolvers<void>();
955
+ const releasePrompt = Promise.withResolvers<void>();
956
+ const messages: AgentMessage[] = [{ role: "user", content: "first", timestamp: 1 } as AgentMessage];
957
+ const agent: AdvisorAgent = {
958
+ prompt: async () => {
959
+ promptStarted.resolve();
960
+ await releasePrompt.promise;
961
+ },
962
+ abort: () => {},
963
+ reset: () => {},
964
+ state: { messages: [] },
965
+ };
966
+ const runtime = new AdvisorRuntime(agent, {
967
+ snapshotMessages: () => messages,
968
+ enqueueAdvice: () => {},
969
+ });
970
+
971
+ runtime.onTurnEnd();
972
+ await promptStarted.promise;
973
+ expect(await runtime.waitForCatchup(20, 1)).toBe(false);
974
+ expect(runtime.backlog).toBe(1);
975
+
976
+ releasePrompt.resolve();
977
+ await settleUntil(() => runtime.backlog === 0);
978
+ });
979
+
868
980
  it("preserves the next user turn when an accepted empty stop is pruned", async () => {
869
981
  const promptInputs: string[] = [];
870
982
  const agent = makeAgent(promptInputs);
@@ -3779,6 +3891,44 @@ describe("advisor", () => {
3779
3891
  // or it strands and #drainStrandedQueuedMessages auto-resumes it. Do not swap
3780
3892
  // the call site back to session `isStreaming`.
3781
3893
  describe("resolveAdvisorDeliveryChannel", () => {
3894
+ it("preserves every severity when a headless drain forbids primary turns", () => {
3895
+ for (const severity of [undefined, "nit", "concern", "blocker"] as const) {
3896
+ expect(
3897
+ resolveAdvisorDeliveryChannel({
3898
+ severity,
3899
+ autoResumeSuppressed: false,
3900
+ streaming: false,
3901
+ aborting: false,
3902
+ terminalAnswerNoQueuedWork: true,
3903
+ preserveOnly: true,
3904
+ }),
3905
+ ).toBe("preserve");
3906
+ }
3907
+ });
3908
+
3909
+ it("keeps live headless advice on normal delivery channels until the primary finishes", () => {
3910
+ expect(
3911
+ resolveAdvisorDeliveryChannel({
3912
+ severity: "nit",
3913
+ autoResumeSuppressed: false,
3914
+ streaming: true,
3915
+ aborting: false,
3916
+ preserveOnly: true,
3917
+ }),
3918
+ ).toBe("aside");
3919
+ for (const severity of ["concern", "blocker"] as const) {
3920
+ expect(
3921
+ resolveAdvisorDeliveryChannel({
3922
+ severity,
3923
+ autoResumeSuppressed: false,
3924
+ streaming: true,
3925
+ aborting: false,
3926
+ preserveOnly: true,
3927
+ }),
3928
+ ).toBe("steer");
3929
+ }
3930
+ });
3931
+
3782
3932
  it("routes a non-interrupting nit to the aside queue regardless of state", () => {
3783
3933
  expect(
3784
3934
  resolveAdvisorDeliveryChannel({
@@ -101,6 +101,8 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
101
101
  /**
102
102
  * Decide how one advisor note reaches the primary agent.
103
103
  *
104
+ * - A `preserveOnly` caller records every note that arrives while the primary
105
+ * is idle as a visible card and never starts a new primary turn.
104
106
  * - A non-interrupting `nit` always rides the non-interrupting aside queue.
105
107
  * - An interrupting `concern`/`blocker` is normally steered into the agent: into
106
108
  * the live turn while one is streaming, or (when idle) a triggered turn so the
@@ -129,7 +131,9 @@ export function resolveAdvisorDeliveryChannel(opts: {
129
131
  aborting: boolean;
130
132
  terminalAnswerNoQueuedWork?: boolean;
131
133
  interruptImmuneTurnActive?: boolean;
134
+ preserveOnly?: boolean;
132
135
  }): AdvisorDeliveryChannel {
136
+ if (opts.preserveOnly && !opts.streaming) return "preserve";
133
137
  if (!isInterruptingSeverity(opts.severity)) return "aside";
134
138
  if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
135
139
  if (opts.terminalAnswerNoQueuedWork && opts.severity !== "blocker" && !opts.streaming && !opts.aborting)
@@ -2,6 +2,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
4
4
  import * as AIError from "@oh-my-pi/pi-ai/error";
5
+ import { type CursorExecResolvedCarrier, kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
5
6
  import { logger } from "@oh-my-pi/pi-utils";
6
7
  import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
7
8
  import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
@@ -129,7 +130,20 @@ export function quarantineAdvisorUnsafeOutput(
129
130
  const unavailableToolNames = new Set<string>();
130
131
  const generatedParts: string[] = [];
131
132
  for (const block of message.content) {
132
- if (block.type === "toolCall" && !availableToolNames.has(block.name)) unavailableToolNames.add(block.name);
133
+ // Cursor exec-channel native blocks (bash/read/grep/...) are stamped
134
+ // kCursorExecResolved: they already ran server-side through the
135
+ // advisor-scoped CursorExecHandlers bridge, which rejects ungranted
136
+ // tools in-band ("Tool not available") and lets the model self-correct.
137
+ // Quarantining them would discard the legitimate advise emitted in the
138
+ // same turn (issue #5900). The scoped bridge is the grant gate here, not
139
+ // this pre-dispatch check.
140
+ if (
141
+ block.type === "toolCall" &&
142
+ !availableToolNames.has(block.name) &&
143
+ (block as CursorExecResolvedCarrier)[kCursorExecResolved] !== true
144
+ ) {
145
+ unavailableToolNames.add(block.name);
146
+ }
133
147
  if (block.type === "toolCall" && block.name === "advise" && typeof block.arguments.note === "string") {
134
148
  generatedParts.push(block.arguments.note);
135
149
  }
@@ -219,8 +233,7 @@ interface PendingDelta {
219
233
 
220
234
  interface CatchupWaiter {
221
235
  threshold: number;
222
- resolve: () => void;
223
- finish: () => void;
236
+ finish: (caughtUp: boolean) => void;
224
237
  timer?: NodeJS.Timeout;
225
238
  }
226
239
 
@@ -366,7 +379,13 @@ export class AdvisorRuntime {
366
379
  }
367
380
  }
368
381
 
369
- waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<void> {
382
+ /**
383
+ * Wait until the advisor backlog falls below `threshold`.
384
+ *
385
+ * Returns `false` when the deadline, abort signal, or a runtime failure releases
386
+ * the waiter before the requested backlog was drained.
387
+ */
388
+ waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<boolean> {
370
389
  if (
371
390
  this.disposed ||
372
391
  signal?.aborted ||
@@ -378,21 +397,26 @@ export class AdvisorRuntime {
378
397
  // primary would otherwise park for the full catch-up budget.
379
398
  this.#failing
380
399
  )
381
- return Promise.resolve();
382
- const { promise, resolve } = Promise.withResolvers<void>();
400
+ return Promise.resolve(this.#backlog < threshold);
401
+ const { promise, resolve } = Promise.withResolvers<boolean>();
383
402
  let waiter!: CatchupWaiter;
384
- const finish = (): void => {
403
+ const finish = (caughtUp: boolean): void => {
385
404
  const idx = this.#waiters.indexOf(waiter);
386
405
  if (idx >= 0) this.#waiters.splice(idx, 1);
387
406
  clearTimeout(waiter.timer);
388
- signal?.removeEventListener("abort", finish);
389
- resolve();
407
+ signal?.removeEventListener("abort", abort);
408
+ resolve(caughtUp);
409
+ };
410
+ const abort = (): void => finish(false);
411
+ waiter = {
412
+ threshold,
413
+ finish,
414
+ timer: setTimeout(abort, maxMs),
390
415
  };
391
- waiter = { threshold, resolve, finish, timer: setTimeout(finish, maxMs) };
392
416
  this.#waiters.push(waiter);
393
- signal?.addEventListener("abort", finish, { once: true });
417
+ signal?.addEventListener("abort", abort, { once: true });
394
418
  if (signal?.aborted) {
395
- finish();
419
+ abort();
396
420
  }
397
421
  return promise;
398
422
  }
@@ -571,14 +595,14 @@ export class AdvisorRuntime {
571
595
  for (let i = this.#waiters.length - 1; i >= 0; i--) {
572
596
  const w = this.#waiters[i];
573
597
  if (this.#backlog < w.threshold) {
574
- w.finish();
598
+ w.finish(true);
575
599
  }
576
600
  }
577
601
  }
578
602
 
579
603
  #wakeAllWaiters(): void {
580
604
  for (const w of [...this.#waiters]) {
581
- w.finish();
605
+ w.finish(false);
582
606
  }
583
607
  }
584
608
 
@@ -37,6 +37,8 @@ export interface AsyncJob {
37
37
  promise: Promise<void>;
38
38
  resultText?: string;
39
39
  errorText?: string;
40
+ /** Latest tool-render details reported by the running job. */
41
+ latestDetails?: Record<string, unknown>;
40
42
  /**
41
43
  * Registry id of the agent that registered the job (e.g. "Main",
42
44
  * "AuthLoader"). Used by scoped cancel/list APIs so a subagent's teardown
@@ -205,6 +207,7 @@ export class AsyncJobManager {
205
207
  };
206
208
 
207
209
  const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
210
+ if (details) job.latestDetails = details;
208
211
  if (!options?.onProgress) return;
209
212
  try {
210
213
  await options.onProgress(text, details);
@@ -443,7 +443,7 @@ function resolveBenchModels(
443
443
  const resolved: BenchTarget[] = [];
444
444
  const errors: string[] = [];
445
445
  for (const selector of selectors) {
446
- const result = resolveCliModel({ cliModel: selector, modelRegistry, preferences });
446
+ const result = resolveCliModel({ cliModel: selector, modelRegistry, settings, preferences });
447
447
  if (result.error) {
448
448
  errors.push(`${selector}: ${result.error}`);
449
449
  continue;
@@ -454,7 +454,13 @@ function resolveBenchModels(
454
454
  }
455
455
  if (result.warning) writeStderr(`${chalk.yellow(`Warning: ${result.warning}`)}\n`);
456
456
  let model = result.model;
457
- const authenticated = resolveAuthenticatedAlternative(selector, model, modelRegistry, preferences.providerOrder);
457
+ const authSelector = result.configuredPatterns?.[result.configuredPatternIndex ?? 0] ?? selector;
458
+ const authenticated = resolveAuthenticatedAlternative(
459
+ authSelector,
460
+ model,
461
+ modelRegistry,
462
+ preferences.providerOrder,
463
+ );
458
464
  if (authenticated) {
459
465
  writeStderr(
460
466
  `${chalk.yellow(
@@ -555,6 +555,7 @@ async function resolveDryBalanceModel(
555
555
  const resolved = resolveCliModel({
556
556
  cliModel: modelSelector,
557
557
  modelRegistry,
558
+ settings,
558
559
  preferences,
559
560
  });
560
561
  if (resolved.error) throw new Error(resolved.error);