@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.13

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 (113) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli.js +3176 -3087
  3. package/dist/types/advisor/runtime.d.ts +11 -0
  4. package/dist/types/config/keybindings.d.ts +9 -4
  5. package/dist/types/config/model-registry.d.ts +4 -0
  6. package/dist/types/config/settings-schema.d.ts +6 -0
  7. package/dist/types/config/settings.d.ts +3 -1
  8. package/dist/types/discovery/helpers.d.ts +9 -0
  9. package/dist/types/exec/bash-executor.d.ts +1 -0
  10. package/dist/types/extensibility/extensions/types.d.ts +11 -2
  11. package/dist/types/extensibility/shared-events.d.ts +2 -2
  12. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
  13. package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
  14. package/dist/types/mnemopi/state.d.ts +7 -3
  15. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
  16. package/dist/types/modes/components/model-selector.d.ts +2 -1
  17. package/dist/types/modes/components/read-tool-group.d.ts +1 -0
  18. package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
  19. package/dist/types/modes/interactive-mode.d.ts +3 -1
  20. package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
  21. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
  22. package/dist/types/modes/types.d.ts +3 -1
  23. package/dist/types/modes/utils/context-usage.d.ts +0 -12
  24. package/dist/types/modes/workflow.d.ts +5 -1
  25. package/dist/types/session/agent-session.d.ts +8 -4
  26. package/dist/types/system-prompt.d.ts +1 -1
  27. package/dist/types/tools/bash-interactive.d.ts +1 -1
  28. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  29. package/dist/types/tools/bash.d.ts +2 -1
  30. package/dist/types/tools/browser/launch.d.ts +1 -0
  31. package/dist/types/tools/grep.d.ts +2 -0
  32. package/dist/types/tools/index.d.ts +4 -0
  33. package/dist/types/tools/path-utils.d.ts +24 -0
  34. package/dist/types/tools/read.d.ts +3 -0
  35. package/dist/types/tools/renderers.d.ts +12 -5
  36. package/dist/types/tools/ssh.d.ts +4 -1
  37. package/dist/types/tools/write.d.ts +1 -0
  38. package/dist/types/utils/local-date.d.ts +2 -0
  39. package/package.json +12 -12
  40. package/src/advisor/__tests__/advisor.test.ts +145 -0
  41. package/src/advisor/runtime.ts +19 -0
  42. package/src/config/api-key-resolver.ts +7 -2
  43. package/src/config/keybindings.ts +62 -10
  44. package/src/config/model-registry.ts +94 -20
  45. package/src/config/settings-schema.ts +11 -1
  46. package/src/config/settings.ts +59 -21
  47. package/src/discovery/builtin.ts +2 -1
  48. package/src/discovery/claude-plugins.ts +167 -46
  49. package/src/discovery/helpers.ts +16 -1
  50. package/src/edit/renderer.ts +20 -6
  51. package/src/eval/js/worker-core.ts +163 -6
  52. package/src/exec/bash-executor.ts +14 -9
  53. package/src/extensibility/extensions/runner.ts +1 -0
  54. package/src/extensibility/extensions/types.ts +13 -2
  55. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
  56. package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
  57. package/src/extensibility/shared-events.ts +2 -2
  58. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
  59. package/src/internal-urls/docs-index.generated.txt +1 -1
  60. package/src/internal-urls/registry-helpers.ts +9 -6
  61. package/src/mnemopi/state.ts +19 -5
  62. package/src/modes/acp/acp-agent.ts +69 -8
  63. package/src/modes/acp/acp-event-mapper.ts +1 -1
  64. package/src/modes/components/model-selector.ts +30 -6
  65. package/src/modes/components/read-tool-group.ts +5 -1
  66. package/src/modes/components/settings-defs.ts +1 -1
  67. package/src/modes/components/status-line/component.ts +14 -2
  68. package/src/modes/components/tool-execution.ts +28 -24
  69. package/src/modes/controllers/command-controller.ts +13 -23
  70. package/src/modes/controllers/event-controller.ts +12 -12
  71. package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
  72. package/src/modes/controllers/extension-ui-controller.ts +7 -35
  73. package/src/modes/controllers/input-controller.ts +23 -57
  74. package/src/modes/controllers/mcp-command-controller.ts +10 -9
  75. package/src/modes/controllers/selector-controller.ts +16 -5
  76. package/src/modes/github-ref-autocomplete.ts +75 -0
  77. package/src/modes/interactive-mode.ts +97 -12
  78. package/src/modes/prompt-action-autocomplete.ts +35 -0
  79. package/src/modes/rpc/rpc-client.ts +42 -13
  80. package/src/modes/rpc/rpc-mode.ts +21 -19
  81. package/src/modes/types.ts +3 -0
  82. package/src/modes/utils/context-usage.ts +58 -5
  83. package/src/modes/utils/hotkeys-markdown.ts +2 -1
  84. package/src/modes/utils/ui-helpers.ts +2 -2
  85. package/src/modes/workflow.ts +14 -8
  86. package/src/prompts/agents/plan.md +0 -1
  87. package/src/prompts/agents/reviewer.md +0 -1
  88. package/src/prompts/system/plan-mode-active.md +5 -2
  89. package/src/prompts/system/system-prompt.md +1 -2
  90. package/src/prompts/system/workflow-notice.md +69 -50
  91. package/src/prompts/tools/bash.md +18 -7
  92. package/src/prompts/tools/grep.md +2 -1
  93. package/src/prompts/tools/memory-edit.md +2 -0
  94. package/src/prompts/tools/read.md +4 -3
  95. package/src/sdk.ts +11 -0
  96. package/src/session/agent-session.ts +136 -19
  97. package/src/system-prompt.ts +3 -2
  98. package/src/tools/bash-interactive.ts +1 -1
  99. package/src/tools/bash-skill-urls.ts +39 -7
  100. package/src/tools/bash.ts +69 -39
  101. package/src/tools/browser/launch.ts +31 -4
  102. package/src/tools/grep.ts +105 -21
  103. package/src/tools/image-gen.ts +1 -1
  104. package/src/tools/index.ts +11 -0
  105. package/src/tools/memory-edit.ts +3 -1
  106. package/src/tools/path-utils.ts +46 -1
  107. package/src/tools/read.ts +135 -57
  108. package/src/tools/renderers.ts +13 -5
  109. package/src/tools/ssh.ts +10 -3
  110. package/src/tools/tts.ts +1 -1
  111. package/src/tools/write.ts +26 -0
  112. package/src/utils/local-date.ts +7 -0
  113. package/src/utils/open.ts +36 -10
@@ -6,14 +6,22 @@ The shell invokes **real binaries** with simple args. It is NOT full GNU Bash.
6
6
 
7
7
  Use bash ONLY for: a single binary call, or one short pipeline that COMPUTES a fact and does not depend on shell-specific regex/quoting (`wc -l`, `sort | uniq -c`, `comm`, `diff`, a checksum, `git status`).
8
8
 
9
- Anything below → `eval` cell, not bash:
9
+ {{#if hasEval}}Anything below → `eval` cell, not bash:
10
10
  - Inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists for that language
11
11
  - Heredocs (`<<EOF`), `while`/`for`/`if`/`case` shell control flow
12
12
  - `$(…)` command substitution nested inside another command
13
13
  - Pipelines with more than two stages, or stages that need control flow or quote/JSON escaping
14
14
  - Multiline commands, `&&`-chains mixing control flow
15
15
  - Quote/JSON escaping that fights the shell
16
- - GNU grep BRE extensions are not guaranteed in the embedded shell: use `grep -E 'json|tool'` for alternation instead of `grep 'json\|tool'`; use the built-in `grep` tool with `pattern: "json|tool"` (Rust regex, so `\bword\b` works there), or `eval` for exact text processing.
16
+ {{else}}Anything below means you are writing a shell program, not invoking one. Prefer a purpose-built tool, a checked-in script, or a single repo command instead:
17
+ - Inline interpreter scripts (`-e`/`-c`/`--eval`)
18
+ - Heredocs (`<<EOF`), `while`/`for`/`if`/`case` shell control flow
19
+ - `$(…)` command substitution nested inside another command
20
+ - Pipelines with more than two stages, or stages that need control flow or quote/JSON escaping
21
+ - Multiline commands, `&&`-chains mixing control flow
22
+ - Quote/JSON escaping that fights the shell
23
+ {{/if}}
24
+ {{#if hasGrep}}- GNU grep BRE extensions are not guaranteed in the embedded shell: use `grep -E 'json|tool'` for alternation instead of `grep 'json\|tool'`; use the built-in `grep` tool with `pattern: "json|tool"` (Rust regex, so `\bword\b` works there){{#if hasEval}}, or `eval` for exact text processing{{/if}}.{{else}}- GNU grep BRE extensions are not guaranteed in the embedded shell: use `grep -E 'json|tool'` for alternation instead of `grep 'json\|tool'`{{#if hasEval}}, or use `eval` for exact text processing{{/if}}.{{/if}}
17
25
 
18
26
  <instruction>
19
27
  - `cwd` sets the working dir, not `cd dir && …`
@@ -23,14 +31,17 @@ Anything below → `eval` cell, not bash:
23
31
  - `;` only when later commands should run despite earlier failures
24
32
  - Multiple bash calls per message run concurrently. NEVER split order-dependent commands across parallel calls — chain with `&&` in one call.
25
33
  - Internal URIs (`skill://`, `agent://`, …) auto-resolve to FS paths
26
- - Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Prefer `eval` and process the stream directly.
34
+ {{#if hasEval}}- Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Prefer `eval` and process the stream directly.{{else}}- Need exact pipeline semantics (`cmd | head`, multi-stage filtering) or output truncation? Use a checked-in script, purpose-built tool, or single command that owns the output shape.{{/if}}
27
35
  {{#if asyncEnabled}}
28
36
  - `async: true` for long-running commands when you don't need immediate output: returns a background job ID; result delivered as a follow-up.
29
37
  {{/if}}
30
38
  </instruction>
31
39
 
32
40
  <critical>
33
- - The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash. Loops, conditionals, heredocs, inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a program → use `eval` cells: restartable, stateful, and free of shell-quoting traps.
41
+ {{#if hasEval}}- The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash and NOT a scripting surface. Loops, conditionals, heredocs, inline interpreter scripts (`-e`/`-c`/`--eval`) when an eval runtime exists, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a program → use `eval` cells: restartable, stateful, and free of shell-quoting traps.{{else}}- The embedded shell invokes real binaries with simple args; it is NOT full GNU Bash and NOT a scripting surface. Loops, conditionals, heredocs, inline interpreter scripts, several piped stages, exact pipeline semantics, or quote/JSON escaping mean you're writing a shell program; use a purpose-built tool or checked-in script instead.{{/if}}
42
+ {{#if hasGrep}}- NEVER shell out to search content or files: `grep/rg` → `grep`.{{else}}- Avoid shelling out for broad content search; use an active search/read tool when one is available.{{/if}}
43
+ {{#if hasRead}}{{#if hasGlob}}- NEVER use `ls` or `find` to list or locate files — `ls` → `read` (a directory path lists entries), `find` → the `glob` tool (globbing). This is non-negotiable, even for a single quick listing.{{else}}- Prefer `read` for known file and directory reads. Only use shell listing when no file-listing tool is active.{{/if}}{{else}}{{#if hasGlob}}- Prefer `glob` for file discovery; avoid `find` when `glob` is active.{{else}}- If no file read/listing tool is active, keep shell inspection narrow and state that limitation.{{/if}}{{/if}}
44
+ - Avoid head/tail/redirections: stderr already merged; long output auto-truncated, FULL capture kept at `artifact://<id>`.
34
45
  </critical>
35
46
 
36
47
  <output>
@@ -41,9 +52,9 @@ Anything below → `eval` cell, not bash:
41
52
  {{#if asyncEnabled}}
42
53
  # Timeout and async
43
54
 
44
- - `timeout` is seconds, clamped to `1..3600`; the process is killed on elapse.
45
- - `async: true` defers only reporting — it does NOT extend the timeout; a daemon with `async: true` is still killed at the clamped timeout.
46
- - Need >3600s? Detach/manage lifecycle yourself (`cmd &`, supervisor, self-restarting script). The shell session persists across calls.
55
+ - `timeout` is seconds; nonzero values are clamped to `1..3600` and the process is killed on elapse. Set `timeout: 0` only for commands that must run until completion or explicit cancellation.
56
+ - `async: true` defers only reporting — it does NOT extend a nonzero timeout; use `timeout: 0` when a daemon or watcher must be cancellation-owned.
57
+ - Need a daemon or >3600s run? Use `async: true` with `timeout: 0` when the harness should keep it alive until cancellation, or detach/manage lifecycle yourself (`cmd &`, supervisor, self-restarting script). The shell session persists across calls.
47
58
  {{/if}}
48
59
  {{#if autoBackgroundEnabled}}
49
60
 
@@ -2,7 +2,8 @@ Greps files using regex.
2
2
 
3
3
  <instruction>
4
4
  - Rust regex (RE2-style): alternation is `foo|bar`, not GNU BRE-style `foo\|bar`; Rust word boundaries like `\bword\b` are supported. Use line anchors or post-filters instead of lookaround/backreferences.
5
- - `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`).
5
+ - `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`). Use `selector` only for line-number filtering, never path/root selection (`"/"` belongs in `path`).
6
+ - Literal colon filename + line range? Use `selector` (e.g. `{"path":"test:1-2","selector":"1-2"}`), not recursive `path:"test:1-2:1-2"`.
6
7
  - Cross-line patterns detected from literal `\n` or `\\n` in `pattern`.
7
8
  </instruction>
8
9
 
@@ -5,6 +5,8 @@ Use only with ids returned by the `recall` tool. Operations:
5
5
  - `forget`: permanently delete a working memory.
6
6
  - `invalidate`: softly supersede a working or episodic memory, optionally pointing at `replacement_id`.
7
7
 
8
+ Fact ids (recall results marked `[facts]`) are read-only: inspect them with `read memory://<id>`; every edit op on a fact id returns `not_editable`.
9
+
8
10
  Prefer `invalidate` when a memory became stale but its history may still be useful. Use `forget` only for content that should be hard-deleted.
9
11
 
10
12
  **Always read the full memory before `update`.** Recall results are clipped previews (the trailing `…` marks a truncation and `full_length` reports the original size); `update` replaces content wholesale, so overwriting the preview would delete the unseen tail. Fetch the row first with `read memory://<id>`, then pass the merged content in `content`.
@@ -1,4 +1,4 @@
1
- Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via one `path`.
1
+ Read files, directories, archives, SQLite, images, documents, internal resources, and web URLs via `path` plus optional `selector`.
2
2
 
3
3
  <instruction>
4
4
  - SHOULD parallelize independent reads.
@@ -7,7 +7,8 @@ Read files, directories, archives, SQLite, images, documents, internal resources
7
7
 
8
8
  ## Parameters
9
9
 
10
- - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL. Append `:<sel>` for ranges/modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
10
+ - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`, `ssh://`), or URL. Inline `:<sel>` still works for ranges/modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
11
+ - `selector` — optional selector without leading `:` (e.g. `"50-200"`, `"raw"`, `"raw:50-100"`, `"conflicts"`). Use when `path` contains literal colons: `{"path":"test:1-2","selector":"1-2"}`.
11
12
 
12
13
  ## Selectors
13
14
 
@@ -72,6 +73,6 @@ All URI schemes take the same line selectors. `artifact://<id>` recovers spilled
72
73
  `ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
73
74
 
74
75
  <critical>
75
- - Line ranges go in the selector: `path="src/foo.ts:50-200"`.
76
+ - Literal colon filename + selector? Use `selector`, not recursive `path:"file:sel:sel"`.
76
77
  - Summary footer names elided ranges? Re-issue ONLY those ranges. NEVER guess `..`/`…` content.
77
78
  </critical>
package/src/sdk.ts CHANGED
@@ -1525,10 +1525,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1525
1525
  // entries capture it at fetch time and are dropped at injection if a newer
1526
1526
  // mutation (any tool) bumped it in the meantime.
1527
1527
  const fileMutationVersions = new Map<string, number>();
1528
+ const activeToolNames = new Set<string>();
1529
+ const setActiveToolNames = (names: Iterable<string>): void => {
1530
+ activeToolNames.clear();
1531
+ for (const name of names) {
1532
+ activeToolNames.add(name);
1533
+ }
1534
+ };
1528
1535
  const toolSession: ToolSession = {
1529
1536
  get cwd() {
1530
1537
  return sessionManager.getCwd();
1531
1538
  },
1539
+ isToolActive: name => activeToolNames.has(name),
1540
+ setActiveToolNames,
1532
1541
  hasUI: options.hasUI ?? false,
1533
1542
  enableLsp,
1534
1543
  get hasEditTool() {
@@ -2558,6 +2567,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2558
2567
  });
2559
2568
  hasRegistered = true;
2560
2569
 
2570
+ setActiveToolNames(initialToolNames);
2561
2571
  const { systemPrompt } = await logger.time(
2562
2572
  "buildSystemPrompt",
2563
2573
  rebuildSystemPrompt,
@@ -2852,6 +2862,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2852
2862
  rebuildSystemPrompt,
2853
2863
  reloadSshTool,
2854
2864
  requestedToolNames: requestedToolNameSet,
2865
+ setActiveToolNames,
2855
2866
  getMcpServerInstructions: mcpManager
2856
2867
  ? () => {
2857
2868
  const raw = mcpManager.getServerInstructions();
@@ -35,6 +35,7 @@ import {
35
35
  type AsideMessage,
36
36
  type CompactionSummaryMessage,
37
37
  countTokens,
38
+ createToolScopedAbortReason,
38
39
  resolveTelemetry,
39
40
  type StreamFn,
40
41
  ThinkingLevel,
@@ -108,6 +109,7 @@ import {
108
109
  clearAnthropicFastModeFallback,
109
110
  deriveClaudeDeviceId,
110
111
  Effort,
112
+ isUsageLimitOutcome,
111
113
  parseRateLimitReason,
112
114
  realizesPriorityServiceTier,
113
115
  resolveModelServiceTier,
@@ -124,6 +126,7 @@ import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
124
126
  import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
125
127
  import {
126
128
  escapeXmlText,
129
+ extractHttpStatusFromError,
127
130
  extractRetryHint,
128
131
  formatDuration,
129
132
  getAgentDbPath,
@@ -179,7 +182,12 @@ import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
179
182
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
180
183
  import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier";
181
184
  import type { Settings, SkillsSettings } from "../config/settings";
182
- import { getDefault, onAppendOnlyModeChanged, validateProviderMaxInFlightRequests } from "../config/settings";
185
+ import {
186
+ getDefault,
187
+ onAppendOnlyModeChanged,
188
+ onModelRolesChanged,
189
+ validateProviderMaxInFlightRequests,
190
+ } from "../config/settings";
183
191
  import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
184
192
  import { loadCapability } from "../discovery";
185
193
  import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
@@ -237,7 +245,7 @@ import { theme } from "../modes/theme/theme";
237
245
  import { parseTurnBudget } from "../modes/turn-budget";
238
246
  import { containsUltrathink, ULTRATHINK_NOTICE } from "../modes/ultrathink";
239
247
  import { computeNonMessageBreakdown, computeNonMessageTokens } from "../modes/utils/context-usage";
240
- import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
248
+ import { containsWorkflow, renderWorkflowNotice } from "../modes/workflow";
241
249
  import { createPlanReadMatcher } from "../plan-mode/plan-protection";
242
250
  import type { PlanModeState } from "../plan-mode/state";
243
251
  import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
@@ -314,6 +322,7 @@ import { resolveFileDisplayMode } from "../utils/file-display-mode";
314
322
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
315
323
  import { normalizeModelContextImages } from "../utils/image-loading";
316
324
  import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
325
+ import { formatLocalCalendarDate } from "../utils/local-date";
317
326
  import { generateSessionTitle } from "../utils/title-generator";
318
327
  import { buildNamedToolChoice, isToolChoiceActive } from "../utils/tool-choice";
319
328
  import type { AuthStorage } from "./auth-storage";
@@ -686,6 +695,8 @@ export interface AgentSessionConfig {
686
695
  toolRegistry?: Map<string, AgentTool>;
687
696
  /** Tool names whose current registry entry is still the built-in implementation. */
688
697
  builtInToolNames?: Iterable<string>;
698
+ /** Update tool-session predicates that render guidance from the live active tool set. */
699
+ setActiveToolNames?: (names: Iterable<string>) => void;
689
700
  /** Current session pre-LLM message transform pipeline */
690
701
  transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
691
702
  /**
@@ -724,6 +735,8 @@ export interface AgentSessionConfig {
724
735
  convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
725
736
  /** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
726
737
  rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>;
738
+ /** Local calendar date provider used by prompt-cache invalidation. Defaults to the host local date. */
739
+ getLocalCalendarDate?: () => string;
727
740
  /** Rebuild the SSH tool from current capability discovery results. */
728
741
  reloadSshTool?: () => Promise<AgentTool | null>;
729
742
  requestedToolNames?: ReadonlySet<string>;
@@ -870,6 +883,7 @@ export interface HandoffResult {
870
883
  export interface SessionHandoffOptions {
871
884
  autoTriggered?: boolean;
872
885
  signal?: AbortSignal;
886
+ onSwitchCancelled?: () => void;
873
887
  }
874
888
 
875
889
  /** Result from cycleModel() */
@@ -1166,6 +1180,7 @@ const noOpUIContext: ExtensionUIContext = {
1166
1180
  pasteToEditor: () => {},
1167
1181
  getEditorText: () => "",
1168
1182
  editor: async () => undefined,
1183
+ addAutocompleteProvider: () => {},
1169
1184
  get theme() {
1170
1185
  return theme;
1171
1186
  },
@@ -1555,6 +1570,7 @@ export class AgentSession {
1555
1570
  #cancelExitRecorder?: () => void;
1556
1571
  #exitRecorded = false;
1557
1572
  #unsubscribeAppendOnly?: () => void;
1573
+ #unsubscribeModelRoles?: () => void;
1558
1574
  /** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
1559
1575
  #lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
1560
1576
  #eventListeners: AgentSessionEventListener[] = [];
@@ -1716,8 +1732,10 @@ export class AgentSession {
1716
1732
  #rebuildSystemPrompt:
1717
1733
  | ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
1718
1734
  | undefined;
1735
+ #getLocalCalendarDate: () => string;
1719
1736
  #getMcpServerInstructions: (() => Map<string, string> | undefined) | undefined;
1720
1737
  #reloadSshTool: (() => Promise<AgentTool | null>) | undefined;
1738
+ #setActiveToolNames: ((names: Iterable<string>) => void) | undefined;
1721
1739
  #disconnectOwnedMcpManager: (() => Promise<void>) | undefined;
1722
1740
  #requestedToolNames: ReadonlySet<string> | undefined;
1723
1741
  #baseSystemPrompt: string[];
@@ -2161,8 +2179,10 @@ export class AgentSession {
2161
2179
  });
2162
2180
  this.#convertToLlm = config.convertToLlm ?? convertToLlm;
2163
2181
  this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
2182
+ this.#getLocalCalendarDate = config.getLocalCalendarDate ?? formatLocalCalendarDate;
2164
2183
  this.#getMcpServerInstructions = config.getMcpServerInstructions;
2165
2184
  this.#reloadSshTool = config.reloadSshTool;
2185
+ this.#setActiveToolNames = config.setActiveToolNames;
2166
2186
  this.#disconnectOwnedMcpManager = config.disconnectOwnedMcpManager;
2167
2187
  this.#baseSystemPrompt = this.agent.state.systemPrompt;
2168
2188
  this.#promptModelKey = this.#currentPromptModelKey();
@@ -2259,6 +2279,11 @@ export class AgentSession {
2259
2279
  this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
2260
2280
  // Re-evaluate append-only context mode when the setting changes at runtime.
2261
2281
  this.#unsubscribeAppendOnly = onAppendOnlyModeChanged(_value => this.#syncAppendOnlyContext(this.model));
2282
+ this.#unsubscribeModelRoles = onModelRolesChanged(() => {
2283
+ if (!this.#advisorEnabled || this.#isDisposed) return;
2284
+ if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime();
2285
+ this.#buildAdvisorRuntime(true);
2286
+ });
2262
2287
  }
2263
2288
  // -------------------------------------------------------------------------
2264
2289
  // Advisor runtime lifecycle
@@ -2391,7 +2416,9 @@ export class AgentSession {
2391
2416
  #advisorRuntimeSignature(config: AdvisorConfig, slug: string, model: Model, thinkingLevel: ThinkingLevel): string {
2392
2417
  const tools = config.tools?.length ? config.tools.join("\u001e") : "";
2393
2418
  const instructions = config.instructions?.trim() ?? "";
2394
- return [config.name, slug, model.provider, model.id, thinkingLevel, tools, instructions].join("\u001f");
2419
+ return [config.name, slug, formatModelStringWithRouting(model), thinkingLevel, tools, instructions].join(
2420
+ "\u001f",
2421
+ );
2395
2422
  }
2396
2423
 
2397
2424
  #advisorRuntimeMatchesCurrentConfig(): boolean {
@@ -2539,6 +2566,21 @@ export class AgentSession {
2539
2566
  maintainContext: incomingTokens => this.#maintainAdvisorContext(advisorRef, incomingTokens),
2540
2567
  obfuscator: this.#obfuscator,
2541
2568
  beginAdvisorUpdate: () => advisorRef.emissionGuard.beginUpdate(),
2569
+ onTurnError: async error => {
2570
+ // Mirror the auth-gateway's usage-limit remedy: the in-stream a/b/c
2571
+ // auth retry rotates through siblings within one request but never
2572
+ // blocks the LAST failing credential, so without this the advisor
2573
+ // re-picks the same exhausted account every retry. Usage limits
2574
+ // only — other failures keep the plain retry/notify path (never
2575
+ // suspect-mark a credential on a transient advisor error).
2576
+ const message = error instanceof Error ? error.message : String(error);
2577
+ if (!isUsageLimitOutcome(extractHttpStatusFromError(error), message)) return;
2578
+ await this.#modelRegistry.authStorage.markUsageLimitReached(advisorModel.provider, advisorSessionId, {
2579
+ retryAfterMs: extractRetryHint(undefined, message),
2580
+ baseUrl: advisorModel.baseUrl,
2581
+ modelId: advisorModel.id,
2582
+ });
2583
+ },
2542
2584
  notifyFailure: error => {
2543
2585
  const message = error instanceof Error ? error.message : String(error);
2544
2586
  this.emitNotice(
@@ -4666,7 +4708,8 @@ export class AgentSession {
4666
4708
  // Decide first: a non-interrupting tool-source match attaches to the
4667
4709
  // specific tool call's result instead of driving a loop-wide follow-up.
4668
4710
  const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
4669
- const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
4711
+ const matchedToolId = this.#extractTtsrToolCallId(matchContext);
4712
+ const perToolId = shouldInterrupt ? undefined : matchedToolId;
4670
4713
  if (perToolId) {
4671
4714
  this.#addPerToolTtsrInjections(perToolId, matches);
4672
4715
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
@@ -4682,7 +4725,16 @@ export class AgentSession {
4682
4725
  // Abort the stream immediately — do not gate on extension callbacks
4683
4726
  this.#ttsrAbortPending = true;
4684
4727
  this.#ensureTtsrResumePromise();
4685
- this.agent.abort(this.#formatTtsrAbortReason(matches));
4728
+ const abortReason = this.#formatTtsrAbortReason(matches);
4729
+ this.agent.abort(
4730
+ matchedToolId
4731
+ ? createToolScopedAbortReason(
4732
+ abortReason,
4733
+ { [matchedToolId]: abortReason },
4734
+ "TTSR interrupt on another tool call",
4735
+ )
4736
+ : abortReason,
4737
+ );
4686
4738
  // Notify extensions (fire-and-forget, does not block abort)
4687
4739
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
4688
4740
  // Schedule retry after a short delay
@@ -5755,6 +5807,10 @@ export class AgentSession {
5755
5807
  this.#unsubscribeAppendOnly();
5756
5808
  this.#unsubscribeAppendOnly = undefined;
5757
5809
  }
5810
+ if (this.#unsubscribeModelRoles) {
5811
+ this.#unsubscribeModelRoles();
5812
+ this.#unsubscribeModelRoles = undefined;
5813
+ }
5758
5814
  this.#eventListeners = [];
5759
5815
  }
5760
5816
 
@@ -6298,6 +6354,7 @@ export class AgentSession {
6298
6354
  ),
6299
6355
  );
6300
6356
  }
6357
+ this.#setActiveToolNames?.(validToolNames);
6301
6358
  const activeNameSet = new Set(validToolNames);
6302
6359
  for (const name of Array.from(this.#selectedDiscoveredToolNames)) {
6303
6360
  if (!activeNameSet.has(name) || isMCPToolName(name) || !this.#toolRegistry.has(name)) {
@@ -6403,6 +6460,7 @@ export class AgentSession {
6403
6460
  async refreshBaseSystemPrompt(): Promise<void> {
6404
6461
  if (!this.#rebuildSystemPrompt) return;
6405
6462
  const activeToolNames = this.getActiveToolNames();
6463
+ this.#setActiveToolNames?.(activeToolNames);
6406
6464
  const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
6407
6465
  this.#baseSystemPrompt = built.systemPrompt;
6408
6466
  this.#baseSystemPromptBeforeMemoryPromotion = undefined;
@@ -6525,7 +6583,7 @@ export class AgentSession {
6525
6583
  entries.sort();
6526
6584
  instructionsSegment = entries.join("\u0006");
6527
6585
  }
6528
- const date = new Date().toISOString().slice(0, 10);
6586
+ const date = this.#getLocalCalendarDate();
6529
6587
  return `${nameSegment}\u0003${descriptionSegment}\u0005${registrySegment}\u0007${instructionsSegment}|${date}`;
6530
6588
  }
6531
6589
 
@@ -7342,11 +7400,15 @@ export class AgentSession {
7342
7400
  timestamp,
7343
7401
  });
7344
7402
  }
7345
- if (this.#magicKeywordEnabled("workflow") && containsWorkflow(text)) {
7403
+ if (
7404
+ this.#magicKeywordEnabled("workflow") &&
7405
+ containsWorkflow(text) &&
7406
+ this.getActiveToolNames().includes("task")
7407
+ ) {
7346
7408
  keywordNotices.push({
7347
7409
  role: "custom",
7348
7410
  customType: "workflow-notice",
7349
- content: WORKFLOW_NOTICE,
7411
+ content: renderWorkflowNotice({ taskBatch: this.settings.get("task.batch") }),
7350
7412
  display: false,
7351
7413
  attribution: "user",
7352
7414
  timestamp,
@@ -8248,11 +8310,10 @@ export class AgentSession {
8248
8310
  }
8249
8311
 
8250
8312
  /**
8251
- * Send a user message to the agent.
8252
- * When deliverAs is set, queue the message instead of starting a new turn.
8313
+ * Send a user message through the prompt flow.
8253
8314
  *
8254
- * @param content User message content (string or content array)
8255
- * @param options.deliverAs Delivery mode: "steer" or "followUp"
8315
+ * Omitted `deliverAs` starts a turn when idle and queues as a steer while streaming.
8316
+ * Explicit `deliverAs` queues without starting a turn in either state.
8256
8317
  */
8257
8318
  async sendUserMessage(
8258
8319
  content: string | (TextContent | ImageContent)[],
@@ -8287,10 +8348,13 @@ export class AgentSession {
8287
8348
  return;
8288
8349
  }
8289
8350
 
8290
- // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
8351
+ // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion.
8352
+ // `streamingBehavior: "steer"` preserves prompt-flow side effects during streaming while
8353
+ // covering the narrow race where a stream starts before prompt() acquires the turn.
8291
8354
  await this.prompt(text, {
8292
8355
  expandPromptTemplates: false,
8293
8356
  images,
8357
+ streamingBehavior: "steer",
8294
8358
  });
8295
8359
  }
8296
8360
 
@@ -8787,6 +8851,7 @@ export class AgentSession {
8787
8851
 
8788
8852
  const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
8789
8853
 
8854
+ this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
8790
8855
  this.#clearActiveRetryFallback();
8791
8856
  this.#setModelWithProviderSessionReset(targetModel);
8792
8857
  this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
@@ -8824,6 +8889,7 @@ export class AgentSession {
8824
8889
 
8825
8890
  const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
8826
8891
 
8892
+ this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(targetModel));
8827
8893
  this.#clearActiveRetryFallback();
8828
8894
  this.#setModelWithProviderSessionReset(targetModel);
8829
8895
  this.sessionManager.appendModelChange(
@@ -8983,6 +9049,7 @@ export class AgentSession {
8983
9049
  const next = scopedModels[nextIndex];
8984
9050
 
8985
9051
  // Apply model
9052
+ this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(next.model));
8986
9053
  this.#clearActiveRetryFallback();
8987
9054
  this.#setModelWithProviderSessionReset(next.model);
8988
9055
  this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
@@ -9013,6 +9080,7 @@ export class AgentSession {
9013
9080
  throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
9014
9081
  }
9015
9082
 
9083
+ this.#modelRegistry.clearSuppressedSelector(formatModelStringWithRouting(nextModel));
9016
9084
  this.#clearActiveRetryFallback();
9017
9085
  this.#setModelWithProviderSessionReset(nextModel);
9018
9086
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
@@ -10041,6 +10109,17 @@ export class AgentSession {
10041
10109
 
10042
10110
  // Start a new session
10043
10111
  const previousSessionFile = this.sessionFile;
10112
+ if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
10113
+ const result = (await this.#extensionRunner.emit({
10114
+ type: "session_before_switch",
10115
+ reason: "handoff",
10116
+ })) as SessionBeforeSwitchResult | undefined;
10117
+
10118
+ if (result?.cancel) {
10119
+ options?.onSwitchCancelled?.();
10120
+ return undefined;
10121
+ }
10122
+ }
10044
10123
  await this.sessionManager.flush();
10045
10124
  this.#cancelOwnAsyncJobs();
10046
10125
  await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
@@ -10097,6 +10176,13 @@ export class AgentSession {
10097
10176
  this.agent.replaceMessages(sessionContext.messages);
10098
10177
  this.#resetAllAdvisorRuntimes();
10099
10178
  this.#syncTodoPhasesFromBranch();
10179
+ if (this.#extensionRunner) {
10180
+ await this.#extensionRunner.emit({
10181
+ type: "session_switch",
10182
+ reason: "handoff",
10183
+ previousSessionFile,
10184
+ });
10185
+ }
10100
10186
 
10101
10187
  return { document: handoffText, savedPath };
10102
10188
  } catch (error) {
@@ -12313,13 +12399,17 @@ export class AgentSession {
12313
12399
  // queue, not the core steering queue (which handoff's agent.reset() would wipe).
12314
12400
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
12315
12401
  if (action === "handoff") {
12402
+ let handoffSwitchCancelled = false;
12316
12403
  const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
12317
12404
  const handoffResult = await this.handoff(handoffFocus, {
12318
12405
  autoTriggered: true,
12319
12406
  signal: autoCompactionSignal,
12407
+ onSwitchCancelled: () => {
12408
+ handoffSwitchCancelled = true;
12409
+ },
12320
12410
  });
12321
12411
  if (!handoffResult) {
12322
- const aborted = autoCompactionSignal.aborted;
12412
+ const aborted = autoCompactionSignal.aborted || handoffSwitchCancelled;
12323
12413
  if (aborted) {
12324
12414
  await this.#emitSessionEvent({
12325
12415
  type: "auto_compaction_end",
@@ -13220,6 +13310,7 @@ export class AgentSession {
13220
13310
  #resolveRetryFallbackRole(currentSelector: string): string | undefined {
13221
13311
  const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
13222
13312
  if (!parsedCurrent) return undefined;
13313
+ const chains = this.#getRetryFallbackChains();
13223
13314
  const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
13224
13315
  const currentPlainSelector = this.model
13225
13316
  ? formatModelSelectorValue(formatModelString(this.model), parsedCurrent.thinkingLevel)
@@ -13229,11 +13320,11 @@ export class AgentSession {
13229
13320
  ? formatRetryFallbackBaseSelector(parseRetryFallbackSelector(currentPlainSelector) ?? parsedCurrent)
13230
13321
  : undefined;
13231
13322
 
13232
- for (const role of Object.keys(this.#getRetryFallbackChains())) {
13323
+ for (const role of Object.keys(chains)) {
13233
13324
  const primarySelector = this.#getRetryFallbackPrimarySelector(role);
13234
13325
  if (primarySelector?.raw === currentSelector) return role;
13235
13326
  }
13236
- for (const role of Object.keys(this.#getRetryFallbackChains())) {
13327
+ for (const role of Object.keys(chains)) {
13237
13328
  const primarySelector = this.#getRetryFallbackPrimarySelector(role);
13238
13329
  if (!primarySelector) continue;
13239
13330
  if (currentPlainSelector && primarySelector.raw === currentPlainSelector) return role;
@@ -13241,6 +13332,14 @@ export class AgentSession {
13241
13332
  if (primaryBaseSelector === currentBaseSelector) return role;
13242
13333
  if (currentPlainBaseSelector && primaryBaseSelector === currentPlainBaseSelector) return role;
13243
13334
  }
13335
+ const defaultChain = chains.default;
13336
+ if (
13337
+ Array.isArray(defaultChain) &&
13338
+ defaultChain.length > 0 &&
13339
+ this.#getRetryFallbackPrimarySelector("default") === undefined
13340
+ ) {
13341
+ return "default";
13342
+ }
13244
13343
  return undefined;
13245
13344
  }
13246
13345
 
@@ -13259,9 +13358,27 @@ export class AgentSession {
13259
13358
  }
13260
13359
 
13261
13360
  #findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
13262
- const chain = this.#getRetryFallbackEffectiveChain(role);
13263
- if (chain.length <= 1) return [];
13361
+ let chain = this.#getRetryFallbackEffectiveChain(role);
13264
13362
  const parsedCurrent = parseRetryFallbackSelector(currentSelector, this.#modelRegistry);
13363
+ if (chain.length === 0 && role === "default" && parsedCurrent) {
13364
+ const chains = this.#getRetryFallbackChains();
13365
+ const defaultChain = chains.default;
13366
+ if (
13367
+ Array.isArray(defaultChain) &&
13368
+ defaultChain.length > 0 &&
13369
+ this.#getRetryFallbackPrimarySelector("default") === undefined
13370
+ ) {
13371
+ const seen = new Set<string>([parsedCurrent.raw]);
13372
+ chain = [parsedCurrent];
13373
+ for (const selector of defaultChain) {
13374
+ const parsed = parseRetryFallbackSelector(selector, this.#modelRegistry);
13375
+ if (!parsed || seen.has(parsed.raw)) continue;
13376
+ seen.add(parsed.raw);
13377
+ chain.push(parsed);
13378
+ }
13379
+ }
13380
+ }
13381
+ if (chain.length <= 1) return [];
13265
13382
  const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
13266
13383
  const currentPlainSelector =
13267
13384
  this.model && parsedCurrent
@@ -15535,7 +15652,7 @@ export class AgentSession {
15535
15652
  lastAttemptAtByAccount: coordinator.lastAttemptAtByAccount,
15536
15653
  });
15537
15654
  if (!decision.redeem) {
15538
- logger.debug("codex-auto-reset: skipped", { reason: decision.reason });
15655
+ logger.debug("codex-auto-reset: skipped", { reason: decision.reason, account: accountKey });
15539
15656
  return false;
15540
15657
  }
15541
15658
  if (shouldPromptCodexAutoRedeem(cfg.autoRedeem) && !(await this.#confirmCodexAutoRedeem(decision))) {
@@ -24,6 +24,7 @@ import projectPromptTemplate from "./prompts/system/project-prompt.md" with { ty
24
24
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
25
25
  import { shortenPath } from "./tools/render-utils";
26
26
  import { type ActiveRepoContext, resolveActiveRepoContext } from "./utils/active-repo-context";
27
+ import { formatLocalCalendarDate } from "./utils/local-date";
27
28
  import { normalizePromptPath } from "./utils/prompt-path";
28
29
  import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
29
30
 
@@ -400,7 +401,7 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
400
401
  return userLevel?.content ?? null;
401
402
  }
402
403
 
403
- export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "eval", "edit", "write"] as const;
404
+ export const DEFAULT_SYSTEM_PROMPT_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
404
405
 
405
406
  export interface SystemPromptToolMetadata {
406
407
  label: string;
@@ -693,7 +694,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
693
694
  }
694
695
  }
695
696
 
696
- const date = new Date().toISOString().slice(0, 10);
697
+ const date = formatLocalCalendarDate();
697
698
  const dateTime = date;
698
699
  const promptCwd = shortenPath(normalizePromptPath(resolvedCwd));
699
700
  const activeRepoContextPrompt = renderActiveRepoContextPrompt(activeRepoContext);
@@ -300,7 +300,7 @@ export async function runInteractiveBashPty(
300
300
  options: {
301
301
  command: string;
302
302
  cwd: string;
303
- timeoutMs: number;
303
+ timeoutMs?: number;
304
304
  signal?: AbortSignal;
305
305
  env?: Record<string, string>;
306
306
  artifactPath?: string;