@oh-my-pi/pi-coding-agent 13.16.3 → 13.16.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.
- package/CHANGELOG.md +54 -5
- package/package.json +7 -7
- package/src/config/model-registry.ts +37 -0
- package/src/config/model-resolver.ts +18 -3
- package/src/config/prompt-templates.ts +4 -4
- package/src/config/settings-schema.ts +12 -0
- package/src/extensibility/skills.ts +3 -3
- package/src/modes/components/settings-defs.ts +9 -0
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/interactive-mode.ts +5 -0
- package/src/patch/index.ts +9 -17
- package/src/patch/shared.ts +2 -3
- package/src/prompts/tools/bash.md +3 -3
- package/src/prompts/tools/hashline.md +38 -39
- package/src/session/agent-session.ts +292 -6
- package/src/tools/auto-generated-guard.ts +1 -1
- package/src/tools/bash.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.16.5] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `--model provider/id` resolving to wrong provider when model ID exists in multiple catalogs ([#560](https://github.com/can1357/oh-my-pi/issues/560))
|
|
10
|
+
|
|
11
|
+
## [13.16.4] - 2026-03-28
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Renamed hashline helper functions from `hlineref`/`hlinefull` to `href`/`hline` for brevity
|
|
15
|
+
- Simplified hashline edit location API: replaced separate `line` and `block` properties with unified `range` property accepting `{ pos, end }` for all range-based edits
|
|
16
|
+
- Updated hashline prompt documentation to reflect new `range` syntax and clarified editing guidelines
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Added detection for `kysely-codegen` generated files in auto-generated file guard
|
|
21
|
+
|
|
5
22
|
## [13.16.1] - 2026-03-27
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -17,6 +34,7 @@
|
|
|
17
34
|
- Updated grep tool description to remove ripgrep-specific implementation detail
|
|
18
35
|
|
|
19
36
|
## [13.16.0] - 2026-03-27
|
|
37
|
+
|
|
20
38
|
### Added
|
|
21
39
|
|
|
22
40
|
- Implemented root path alias: bare `/` in tool inputs now resolves to the session working directory instead of the filesystem root
|
|
@@ -36,6 +54,7 @@
|
|
|
36
54
|
- Added configurable `app.model.selectTemporary` keybinding for temporary model selection.
|
|
37
55
|
|
|
38
56
|
## [13.15.0] - 2026-03-23
|
|
57
|
+
|
|
39
58
|
### Breaking Changes
|
|
40
59
|
|
|
41
60
|
- Changed hashline edit schema from flat `op`/`pos`/`end`/`lines` fields to structured `loc`/`content` format with location-specific objects
|
|
@@ -162,10 +181,10 @@
|
|
|
162
181
|
- Fixed autoresearch logging to require durable ASI metadata (hypothesis, rollback_reason, next_action_hint) for every run including rollback context for discarded, crashed, and checks-failed experiments
|
|
163
182
|
- Fixed autoresearch logging to require durable ASI metadata for every run, including rollback context for discarded, crashed, and checks-failed experiments
|
|
164
183
|
|
|
165
|
-
|
|
166
184
|
### Fixed
|
|
167
185
|
|
|
168
186
|
- Fixed resumed and session-switched GitHub Copilot/OpenAI Responses conversations replaying stale assistant native history from older saved sessions by sanitizing persisted assistant replay metadata on rehydration and resetting provider session state across live session boundaries ([#505](https://github.com/can1357/oh-my-pi/issues/505))
|
|
187
|
+
|
|
169
188
|
## [13.14.0] - 2026-03-20
|
|
170
189
|
|
|
171
190
|
### Added
|
|
@@ -200,6 +219,7 @@
|
|
|
200
219
|
- Added `mcpServerName` and `mcpToolName` optional properties to custom tools for MCP server discovery and search metadata
|
|
201
220
|
|
|
202
221
|
## [13.13.1] - 2026-03-18
|
|
222
|
+
|
|
203
223
|
### Added
|
|
204
224
|
|
|
205
225
|
- Automatic deduplication of identical context files by content, keeping the closest (lowest depth) copy when duplicates are discovered
|
|
@@ -218,6 +238,7 @@
|
|
|
218
238
|
- Enhanced auto-generated marker detection to only scan leading header comments rather than entire file prefix, improving accuracy for files with generated markers in code
|
|
219
239
|
|
|
220
240
|
## [13.12.10] - 2026-03-17
|
|
241
|
+
|
|
221
242
|
### Added
|
|
222
243
|
|
|
223
244
|
- Added `args` field to ShellResult to capture the executed command
|
|
@@ -230,6 +251,7 @@
|
|
|
230
251
|
- Updated `run()` command documentation to clarify available ShellResult fields
|
|
231
252
|
|
|
232
253
|
## [13.12.9] - 2026-03-17
|
|
254
|
+
|
|
233
255
|
### Added
|
|
234
256
|
|
|
235
257
|
- Added `/session delete` command to delete current session with confirmation and return to session selector
|
|
@@ -274,6 +296,7 @@
|
|
|
274
296
|
- Fixed session directory resolution to correctly handle symlink-equivalent paths, ensuring aliased home and temp directories resolve to the same session storage location as their real targets
|
|
275
297
|
|
|
276
298
|
## [13.12.7] - 2026-03-16
|
|
299
|
+
|
|
277
300
|
### Changed
|
|
278
301
|
|
|
279
302
|
- Modified `getSelectedMCPToolNames()` to return only active MCP tools in non-discovery sessions, filtering by tool registry availability
|
|
@@ -285,6 +308,7 @@
|
|
|
285
308
|
- Fixed MCP tool selection tracking to properly distinguish between discovery-enabled and non-discovery sessions, preventing orphaned tool selections after manual deactivation
|
|
286
309
|
|
|
287
310
|
## [13.12.6] - 2026-03-15
|
|
311
|
+
|
|
288
312
|
### Changed
|
|
289
313
|
|
|
290
314
|
- Updated llama.cpp model discovery to read context window from the `/props` endpoint's `default_generation_settings.n_ctx` field instead of using hardcoded 128000 default
|
|
@@ -320,6 +344,7 @@
|
|
|
320
344
|
- Fixed automatic migration of legacy session directories to new `-tmp-` prefixed naming scheme for temp-root sessions
|
|
321
345
|
|
|
322
346
|
## [13.12.4] - 2026-03-15
|
|
347
|
+
|
|
323
348
|
### Added
|
|
324
349
|
|
|
325
350
|
- Exposed `settings` instance in `CustomToolContext` for session-specific configuration access
|
|
@@ -357,6 +382,7 @@
|
|
|
357
382
|
- Fixed skill loading to properly respect disabled skill names when loading from custom directories
|
|
358
383
|
|
|
359
384
|
## [13.12.1] - 2026-03-15
|
|
385
|
+
|
|
360
386
|
### Added
|
|
361
387
|
|
|
362
388
|
- Support for move-only operations that preserve exact bytes including binary files
|
|
@@ -396,7 +422,7 @@
|
|
|
396
422
|
- Moved LSP settings (lsp.enabled, lsp.formatOnWrite, lsp.diagnosticsOnWrite, lsp.diagnosticsOnEdit) to the Editing tab
|
|
397
423
|
- Moved bash interceptor settings to the Editing tab
|
|
398
424
|
- Moved Python settings (python.toolMode, python.kernelMode, python.sharedGateway) to the Editing tab
|
|
399
|
-
- Moved task delegation settings (task.isolation
|
|
425
|
+
- Moved task delegation settings (task.isolation.\*, task.eager, task.maxConcurrency, task.maxRecursionDepth) to the Tasks tab
|
|
400
426
|
- Moved skill and command settings to the Tasks tab
|
|
401
427
|
- Moved provider selection settings (providers.webSearch, providers.codeSearch, providers.image, etc.) to the Providers tab
|
|
402
428
|
- Moved Exa settings to the Providers tab
|
|
@@ -452,6 +478,7 @@
|
|
|
452
478
|
- Fixed line number parsing in compact diff preview to handle variable-width line number fields with leading whitespace
|
|
453
479
|
|
|
454
480
|
## [13.11.0] - 2026-03-12
|
|
481
|
+
|
|
455
482
|
### Added
|
|
456
483
|
|
|
457
484
|
- Added Parallel as a web search provider with support for fast and research modes
|
|
@@ -494,6 +521,7 @@
|
|
|
494
521
|
- Per-role `modelRoles` thinking selectors now propagate through commit/title helper model selection, legacy commit analysis, and agentic commit sessions while preserving default thinking inheritance when no role override is configured
|
|
495
522
|
|
|
496
523
|
## [13.10.1] - 2026-03-10
|
|
524
|
+
|
|
497
525
|
### Added
|
|
498
526
|
|
|
499
527
|
- Exported `submitInteractiveInput()` function for programmatic submission of user input in interactive mode
|
|
@@ -501,15 +529,19 @@
|
|
|
501
529
|
- Added reactive 401/403 retry with automatic token refresh on HTTP MCP transports
|
|
502
530
|
- Added `refreshMCPOAuthToken()` for standard OAuth 2.0 refresh_token grants
|
|
503
531
|
- Persisted `tokenUrl`, `clientId`, and `clientSecret` in MCP auth config for cross-session token refresh
|
|
532
|
+
|
|
504
533
|
### Fixed
|
|
534
|
+
|
|
505
535
|
- Respected `PI_CONFIG_DIR` when discovering native user config paths for slash commands and related config directories ([#349](https://github.com/can1357/oh-my-pi/issues/349))
|
|
506
536
|
|
|
507
537
|
## [13.10.0] - 2026-03-10
|
|
538
|
+
|
|
508
539
|
### Fixed
|
|
509
540
|
|
|
510
541
|
- Preserved text signature metadata (id and phase) when building OpenAI native history during session compaction
|
|
511
542
|
|
|
512
543
|
## [13.9.16] - 2026-03-10
|
|
544
|
+
|
|
513
545
|
### Breaking Changes
|
|
514
546
|
|
|
515
547
|
- Web search tool no longer accepts `provider` parameter in tool calls; use internal provider resolution instead
|
|
@@ -548,6 +580,7 @@
|
|
|
548
580
|
- Fixed model selector to show discovery status messages when provider has no models
|
|
549
581
|
|
|
550
582
|
## [13.9.15] - 2026-03-10
|
|
583
|
+
|
|
551
584
|
### Added
|
|
552
585
|
|
|
553
586
|
- Added `ensureLoadingAnimation()` method to manage loading animation lifecycle and prevent duplicate spinners
|
|
@@ -558,6 +591,7 @@
|
|
|
558
591
|
- Updated `showError()` to properly clean up loading animation state when errors occur
|
|
559
592
|
|
|
560
593
|
## [13.9.12] - 2026-03-09
|
|
594
|
+
|
|
561
595
|
### Added
|
|
562
596
|
|
|
563
597
|
- Added Tavily as a supported web search provider with `TAVILY_API_KEY` credential discovery and provider fallback support
|
|
@@ -575,6 +609,7 @@
|
|
|
575
609
|
- Canonicalized bash executor working directories before handing them to brush so `pwd` stays aligned with canonical Git worktree paths in symlinked workspaces
|
|
576
610
|
|
|
577
611
|
## [13.9.10] - 2026-03-08
|
|
612
|
+
|
|
578
613
|
### Added
|
|
579
614
|
|
|
580
615
|
- Added `env` parameter to bash tool to pass environment variables safely without shell re-parsing, preventing quote and special character bugs with multiline or untrusted values
|
|
@@ -586,6 +621,7 @@
|
|
|
586
621
|
- Changed bash tool to display environment variable assignments in command preview when `env` parameter is used
|
|
587
622
|
|
|
588
623
|
## [13.9.8] - 2026-03-08
|
|
624
|
+
|
|
589
625
|
### Added
|
|
590
626
|
|
|
591
627
|
- Added docs.rs scraper for extracting Rust crate documentation from rustdoc JSON, including support for modules, functions, structs, traits, enums, and other Rust items with caching
|
|
@@ -620,6 +656,7 @@
|
|
|
620
656
|
- Updated `grep` tool to combine glob patterns from `path` and `glob` parameters instead of throwing an error when both are provided
|
|
621
657
|
|
|
622
658
|
## [13.9.4] - 2026-03-07
|
|
659
|
+
|
|
623
660
|
### Added
|
|
624
661
|
|
|
625
662
|
- Automatic detection of Ollama model capabilities including reasoning/thinking support and vision input via the `/api/show` endpoint
|
|
@@ -708,6 +745,7 @@
|
|
|
708
745
|
- Auto-corrected off-by-one range start errors in hashline edits that would duplicate preceding lines
|
|
709
746
|
|
|
710
747
|
## [13.9.0] - 2026-03-05
|
|
748
|
+
|
|
711
749
|
### Added
|
|
712
750
|
|
|
713
751
|
- Added `read.defaultLimit` setting to configure default number of lines returned by read tool when no limit is specified (default: 300 lines)
|
|
@@ -736,6 +774,7 @@
|
|
|
736
774
|
- Fixed provider session state not being cleared when branching or navigating tree history, preventing resource leaks with codex provider sessions
|
|
737
775
|
|
|
738
776
|
## [13.8.0] - 2026-03-04
|
|
777
|
+
|
|
739
778
|
### Added
|
|
740
779
|
|
|
741
780
|
- Added `buildCompactHashlineDiffPreview()` function to generate compact diff previews for model-visible tool responses, collapsing long unchanged runs and consecutive additions/removals to show edit shape without full file content
|
|
@@ -751,6 +790,7 @@
|
|
|
751
790
|
- Fixed `:thinking` suffix in `modelRoles` config values silently breaking model resolution (e.g., `slow: anthropic/claude-opus-4-6:high`) and being stripped on Ctrl+P role cycling
|
|
752
791
|
|
|
753
792
|
## [13.7.6] - 2026-03-04
|
|
793
|
+
|
|
754
794
|
### Added
|
|
755
795
|
|
|
756
796
|
- Exported `dedupeParseErrors` utility function to deduplicate parse error messages while preserving order
|
|
@@ -761,7 +801,9 @@
|
|
|
761
801
|
- Normalized parse error output in ast-grep to remove pattern-specific prefixes and show only file-level errors
|
|
762
802
|
|
|
763
803
|
## [13.7.4] - 2026-03-04
|
|
804
|
+
|
|
764
805
|
### Added
|
|
806
|
+
|
|
765
807
|
- Added `fetch.useKagiSummarizer` setting to toggle Kagi Universal Summarizer usage in the fetch tool.
|
|
766
808
|
|
|
767
809
|
### Fixed
|
|
@@ -781,7 +823,7 @@
|
|
|
781
823
|
### Changed
|
|
782
824
|
|
|
783
825
|
- Updated hashline prompt documentation with clearer operation syntax and improved examples showing full edit structure with path and edits array
|
|
784
|
-
- Refactored `
|
|
826
|
+
- Refactored `href` Handlebars helper to return JSON-quoted strings for safer embedding in JSON blocks within prompts
|
|
785
827
|
- Improved `hashlineParseText` to correctly preserve blank lines and trailing empty strings in array input while stripping trailing newlines from string input
|
|
786
828
|
- Optimized duplicate line detection in range replacements to use trimmed comparison, reducing false positives from whitespace differences
|
|
787
829
|
- Refactored Kagi search provider to use shared Kagi API utilities from `web/kagi` module
|
|
@@ -792,6 +834,7 @@
|
|
|
792
834
|
- Fixed `isEscapedTabAutocorrectEnabled` environment variable parsing to use switch statement for clearer logic and consistent default behavior
|
|
793
835
|
|
|
794
836
|
## [13.7.2] - 2026-03-04
|
|
837
|
+
|
|
795
838
|
### Added
|
|
796
839
|
|
|
797
840
|
- Added support for direct OAuth provider login via `/login <provider>` command (e.g., `/login kagi`)
|
|
@@ -808,6 +851,7 @@
|
|
|
808
851
|
- Fixed `ask` timeout handling to auto-select the recommended option instead of aborting the turn, while preserving explicit user-cancel abort behavior ([#266](https://github.com/can1357/oh-my-pi/issues/266))
|
|
809
852
|
|
|
810
853
|
## [13.6.2] - 2026-03-03
|
|
854
|
+
|
|
811
855
|
### Fixed
|
|
812
856
|
|
|
813
857
|
- Fixed LM Studio API key retrieval to use configured provider name instead of hardcoded 'lm-studio'
|
|
@@ -822,7 +866,9 @@
|
|
|
822
866
|
- Fixed `omp update` silently succeeding without actually updating the binary when the update channel (bun global vs compiled binary) doesn't match the installation method ([#247](https://github.com/can1357/oh-my-pi/issues/247))
|
|
823
867
|
- Added post-update verification that checks the resolved `omp` binary reports the expected version, with actionable warnings on mismatch
|
|
824
868
|
- `omp update` now detects when the `omp` in PATH is not managed by bun and falls back to binary replacement instead of updating the wrong location
|
|
869
|
+
|
|
825
870
|
## [13.6.0] - 2026-03-03
|
|
871
|
+
|
|
826
872
|
### Added
|
|
827
873
|
|
|
828
874
|
- Added `mcp://` internal URL protocol for reading MCP server resources directly via the read tool (e.g., `read(path="mcp://resource-uri")`)
|
|
@@ -853,9 +899,11 @@
|
|
|
853
899
|
- Fixed URI template matching to correctly handle expressions that expand to empty strings
|
|
854
900
|
|
|
855
901
|
## [13.5.6] - 2026-03-01
|
|
902
|
+
|
|
856
903
|
### Changed
|
|
857
904
|
|
|
858
905
|
- Updated OAuth client name from 'oh-my-pi MCP' to 'Codex' for dynamic client registration
|
|
906
|
+
|
|
859
907
|
### Fixed
|
|
860
908
|
|
|
861
909
|
- Fixed exit_plan_mode handler to abort active agent turn before opening plan approval selector, ensuring proper session cleanup
|
|
@@ -867,6 +915,7 @@
|
|
|
867
915
|
- Added Kagi web search provider (Search API v0) with related searches support and automatic `KAGI_API_KEY` detection
|
|
868
916
|
|
|
869
917
|
## [13.5.4] - 2026-03-01
|
|
918
|
+
|
|
870
919
|
### Added
|
|
871
920
|
|
|
872
921
|
- Added `authServerUrl` field to `AuthDetectionResult` to capture OAuth server metadata from `Mcp-Auth-Server` headers
|
|
@@ -875,6 +924,7 @@
|
|
|
875
924
|
- Added recursive auth server discovery to follow `authorization_servers` references when discovering OAuth endpoints
|
|
876
925
|
|
|
877
926
|
- Added `omp agents unpack` CLI subcommand to export bundled subagent definitions to `~/.omp/agent/agents` by default, with `--project` support for `./.omp/agents`
|
|
927
|
+
|
|
878
928
|
### Changed
|
|
879
929
|
|
|
880
930
|
- Enhanced `discoverOAuthEndpoints()` to accept optional `authServerUrl` parameter and query both auth server and resource server for OAuth metadata
|
|
@@ -1569,7 +1619,7 @@
|
|
|
1569
1619
|
- Updated hashline reference format from `LINE:HASH` to `LINE#ID` throughout the codebase for improved clarity
|
|
1570
1620
|
- Renamed hashline edit operations: `set_line` → `set`, `replace_lines` → `set_range`, `insert_after` → `insert` with support for `before` and `between` anchors
|
|
1571
1621
|
- Changed hashline edit `body` field from string to array of strings for clearer multiline handling
|
|
1572
|
-
- Updated handlebars helpers: renamed `hashline` to `
|
|
1622
|
+
- Updated handlebars helpers: renamed `hashline` to `href` and added `hline` for formatted line output
|
|
1573
1623
|
- Improved insert operation to support `before`, `after`, and `between` (both anchors) positioning modes
|
|
1574
1624
|
- Made autocorrect heuristics (boundary echo stripping, indent restoration) conditional on `PI_HL_AUTOCORRECT` environment variable
|
|
1575
1625
|
- Updated SSH host discovery to load from managed omp config paths (.omp/ssh.json and ~/.omp/agent/ssh.json) in addition to legacy root-level ssh.json and .ssh.json files
|
|
@@ -1901,7 +1951,6 @@
|
|
|
1901
1951
|
- Improved error reporting in fetch tool to include HTTP status codes when URL fetching fails
|
|
1902
1952
|
- Fixed fetch tool to preserve actual response metadata (finalUrl, contentType) instead of defaults when requests fail
|
|
1903
1953
|
|
|
1904
|
-
|
|
1905
1954
|
## [12.1.0] - 2026-02-13
|
|
1906
1955
|
|
|
1907
1956
|
### Added
|
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": "13.16.
|
|
4
|
+
"version": "13.16.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
44
44
|
"@mozilla/readability": "^0.6",
|
|
45
|
-
"@oh-my-pi/omp-stats": "13.16.
|
|
46
|
-
"@oh-my-pi/pi-agent-core": "13.16.
|
|
47
|
-
"@oh-my-pi/pi-ai": "13.16.
|
|
48
|
-
"@oh-my-pi/pi-natives": "13.16.
|
|
49
|
-
"@oh-my-pi/pi-tui": "13.16.
|
|
50
|
-
"@oh-my-pi/pi-utils": "13.16.
|
|
45
|
+
"@oh-my-pi/omp-stats": "13.16.5",
|
|
46
|
+
"@oh-my-pi/pi-agent-core": "13.16.5",
|
|
47
|
+
"@oh-my-pi/pi-ai": "13.16.5",
|
|
48
|
+
"@oh-my-pi/pi-natives": "13.16.5",
|
|
49
|
+
"@oh-my-pi/pi-tui": "13.16.5",
|
|
50
|
+
"@oh-my-pi/pi-utils": "13.16.5",
|
|
51
51
|
"@sinclair/typebox": "^0.34",
|
|
52
52
|
"@xterm/headless": "^6.0",
|
|
53
53
|
"ajv": "^8.18",
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
29
29
|
import { type Static, Type } from "@sinclair/typebox";
|
|
30
30
|
import { type ConfigError, ConfigFile } from "../config";
|
|
31
|
+
import { parseModelString } from "../config/model-resolver";
|
|
31
32
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
32
33
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
33
34
|
import type { Settings } from "./settings";
|
|
@@ -721,6 +722,14 @@ function buildCustomModel(
|
|
|
721
722
|
return finalizeCustomModel(model, options);
|
|
722
723
|
}
|
|
723
724
|
|
|
725
|
+
function normalizeSuppressedSelector(selector: string): string {
|
|
726
|
+
const trimmed = selector.trim();
|
|
727
|
+
if (!trimmed) return trimmed;
|
|
728
|
+
const parsed = parseModelString(trimmed);
|
|
729
|
+
if (!parsed) return trimmed;
|
|
730
|
+
return `${parsed.provider}/${parsed.id}`;
|
|
731
|
+
}
|
|
732
|
+
|
|
724
733
|
/**
|
|
725
734
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
726
735
|
*/
|
|
@@ -737,6 +746,7 @@ export class ModelRegistry {
|
|
|
737
746
|
#registeredProviderSources: Set<string> = new Set();
|
|
738
747
|
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
|
739
748
|
#cacheDbPath?: string;
|
|
749
|
+
#suppressedSelectors: Map<string, number> = new Map();
|
|
740
750
|
#backgroundRefresh?: Promise<void>;
|
|
741
751
|
#lastDiscoveryWarnings: Map<string, string> = new Map();
|
|
742
752
|
|
|
@@ -766,6 +776,7 @@ export class ModelRegistry {
|
|
|
766
776
|
*/
|
|
767
777
|
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
768
778
|
this.#reloadStaticModels();
|
|
779
|
+
this.#suppressedSelectors.clear();
|
|
769
780
|
await this.#refreshRuntimeDiscoveries(strategy);
|
|
770
781
|
}
|
|
771
782
|
|
|
@@ -789,6 +800,11 @@ export class ModelRegistry {
|
|
|
789
800
|
|
|
790
801
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
791
802
|
this.#reloadStaticModels();
|
|
803
|
+
for (const selector of this.#suppressedSelectors.keys()) {
|
|
804
|
+
if (selector.startsWith(`${providerId}/`)) {
|
|
805
|
+
this.#suppressedSelectors.delete(selector);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
792
808
|
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
|
793
809
|
}
|
|
794
810
|
|
|
@@ -1825,6 +1841,27 @@ export class ModelRegistry {
|
|
|
1825
1841
|
});
|
|
1826
1842
|
}
|
|
1827
1843
|
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Suppress a specific model selector (e.g., "provider/id") until a specific timestamp.
|
|
1847
|
+
*/
|
|
1848
|
+
suppressSelector(selector: string, untilMs: number): void {
|
|
1849
|
+
this.#suppressedSelectors.set(normalizeSuppressedSelector(selector), untilMs);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* Check if a model selector is currently suppressed due to rate limits.
|
|
1854
|
+
*/
|
|
1855
|
+
isSelectorSuppressed(selector: string): boolean {
|
|
1856
|
+
const normalizedSelector = normalizeSuppressedSelector(selector);
|
|
1857
|
+
const suppressedUntil = this.#suppressedSelectors.get(normalizedSelector);
|
|
1858
|
+
if (!suppressedUntil) return false;
|
|
1859
|
+
if (suppressedUntil <= Date.now()) {
|
|
1860
|
+
this.#suppressedSelectors.delete(normalizedSelector);
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
return true;
|
|
1864
|
+
}
|
|
1828
1865
|
}
|
|
1829
1866
|
|
|
1830
1867
|
/**
|
|
@@ -730,9 +730,24 @@ export function resolveCliModel(options: {
|
|
|
730
730
|
|
|
731
731
|
if (!provider) {
|
|
732
732
|
const lower = cliModel.toLowerCase();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
733
|
+
// When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
|
|
734
|
+
// provider+id match over flat id match. Without this, a model with id
|
|
735
|
+
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
736
|
+
// with id "glm-5", because Array.find returns the first catalog hit.
|
|
737
|
+
const slashIdx = lower.indexOf("/");
|
|
738
|
+
let exact: (typeof availableModels)[number] | undefined;
|
|
739
|
+
if (slashIdx !== -1) {
|
|
740
|
+
const prefix = lower.substring(0, slashIdx);
|
|
741
|
+
const suffix = lower.substring(slashIdx + 1);
|
|
742
|
+
exact = availableModels.find(
|
|
743
|
+
model => model.provider.toLowerCase() === prefix && model.id.toLowerCase() === suffix,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (!exact) {
|
|
747
|
+
exact = availableModels.find(
|
|
748
|
+
model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
736
751
|
if (exact) {
|
|
737
752
|
return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined };
|
|
738
753
|
}
|
|
@@ -258,19 +258,19 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
/**
|
|
261
|
-
* {{
|
|
261
|
+
* {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
|
|
262
262
|
* Returns `"lineNum#hash"` using the actual hash algorithm.
|
|
263
263
|
*/
|
|
264
|
-
handlebars.registerHelper("
|
|
264
|
+
handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string => {
|
|
265
265
|
const { ref } = formatHashlineRef(lineNum, content);
|
|
266
266
|
return JSON.stringify(ref);
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
|
-
* {{
|
|
270
|
+
* {{hline lineNum "content"}} — format a full read-style line with prefix.
|
|
271
271
|
* Returns `"lineNum#hash:content"`.
|
|
272
272
|
*/
|
|
273
|
-
handlebars.registerHelper("
|
|
273
|
+
handlebars.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
|
|
274
274
|
const { ref, text } = formatHashlineRef(lineNum, content);
|
|
275
275
|
return `${ref}:${text}`;
|
|
276
276
|
});
|
|
@@ -525,6 +525,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
525
525
|
},
|
|
526
526
|
|
|
527
527
|
"retry.baseDelayMs": { type: "number", default: 2000 },
|
|
528
|
+
"retry.fallbackChains": { type: "record", default: {} as Record<string, string[]> },
|
|
529
|
+
"retry.fallbackRevertPolicy": {
|
|
530
|
+
type: "enum",
|
|
531
|
+
values: ["cooldown-expiry", "never"] as const,
|
|
532
|
+
default: "cooldown-expiry",
|
|
533
|
+
ui: {
|
|
534
|
+
tab: "model",
|
|
535
|
+
label: "Fallback Revert Policy",
|
|
536
|
+
description: "When to return to the primary model after a fallback",
|
|
537
|
+
submenu: true,
|
|
538
|
+
},
|
|
539
|
+
},
|
|
528
540
|
|
|
529
541
|
// ────────────────────────────────────────────────────────────────────────
|
|
530
542
|
// Interaction
|
|
@@ -54,7 +54,7 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
54
54
|
name: capSkill.name,
|
|
55
55
|
description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
|
|
56
56
|
filePath: capSkill.path,
|
|
57
|
-
baseDir: capSkill.path.replace(
|
|
57
|
+
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
58
58
|
source: options.source,
|
|
59
59
|
_source: capSkill._source,
|
|
60
60
|
})),
|
|
@@ -168,7 +168,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
168
168
|
name: capSkill.name,
|
|
169
169
|
description: typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
|
|
170
170
|
filePath: capSkill.path,
|
|
171
|
-
baseDir: capSkill.path.replace(
|
|
171
|
+
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
172
172
|
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
173
173
|
_source: capSkill._source,
|
|
174
174
|
});
|
|
@@ -204,7 +204,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
204
204
|
description:
|
|
205
205
|
typeof capSkill.frontmatter?.description === "string" ? capSkill.frontmatter.description : "",
|
|
206
206
|
filePath: capSkill.path,
|
|
207
|
-
baseDir: capSkill.path.replace(
|
|
207
|
+
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
208
208
|
source: "custom:user",
|
|
209
209
|
_source: { ...capSkill._source, providerName: "Custom" },
|
|
210
210
|
},
|
|
@@ -117,6 +117,15 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
117
117
|
{ value: "5", label: "5 retries" },
|
|
118
118
|
{ value: "10", label: "10 retries" },
|
|
119
119
|
],
|
|
120
|
+
// Retry fallback revert policy
|
|
121
|
+
"retry.fallbackRevertPolicy": [
|
|
122
|
+
{
|
|
123
|
+
value: "cooldown-expiry",
|
|
124
|
+
label: "Cooldown expiry",
|
|
125
|
+
description: "Return to the primary model after its suppression window ends",
|
|
126
|
+
},
|
|
127
|
+
{ value: "never", label: "Never", description: "Stay on the fallback model until manually changed" },
|
|
128
|
+
],
|
|
120
129
|
// Task max concurrency
|
|
121
130
|
"task.maxConcurrency": [
|
|
122
131
|
{ value: "0", label: "Unlimited" },
|
|
@@ -534,6 +534,16 @@ export class EventController {
|
|
|
534
534
|
break;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
case "retry_fallback_applied": {
|
|
538
|
+
this.ctx.showWarning(`Fallback: ${event.from} -> ${event.to}`);
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
case "retry_fallback_succeeded": {
|
|
543
|
+
this.ctx.showStatus(`Fallback succeeded on ${event.model}`);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
537
547
|
case "ttsr_triggered": {
|
|
538
548
|
const component = new TtsrNotificationComponent(event.rules);
|
|
539
549
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
@@ -306,6 +306,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
306
306
|
|
|
307
307
|
const startupQuiet = settings.get("startup.quiet");
|
|
308
308
|
|
|
309
|
+
for (const warning of this.session.configWarnings) {
|
|
310
|
+
this.ui.addChild(new Text(theme.fg("warning", `Warning: ${warning}`), 1, 0));
|
|
311
|
+
this.ui.addChild(new Spacer(1));
|
|
312
|
+
}
|
|
313
|
+
|
|
309
314
|
if (!startupQuiet) {
|
|
310
315
|
// Add welcome header
|
|
311
316
|
const welcome = new WelcomeComponent(this.#version, modelName, providerName, recentSessions, lspServerInfo);
|
package/src/patch/index.ts
CHANGED
|
@@ -187,12 +187,9 @@ const locSchema = Type.Union(
|
|
|
187
187
|
Type.Object({ append: Type.String({ description: "anchor" }) }),
|
|
188
188
|
Type.Object({ prepend: Type.String({ description: "anchor" }) }),
|
|
189
189
|
Type.Object({
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
block: Type.Object({
|
|
194
|
-
pos: Type.String({ description: "anchor" }),
|
|
195
|
-
end: Type.String({ description: "limit position" }),
|
|
190
|
+
range: Type.Object({
|
|
191
|
+
pos: Type.String({ description: "first line to edit (inclusive)" }),
|
|
192
|
+
end: Type.String({ description: "last line to edit (inclusive)" }),
|
|
196
193
|
}),
|
|
197
194
|
}),
|
|
198
195
|
],
|
|
@@ -231,8 +228,7 @@ export type HashlineParams = Static<typeof hashlineEditParamsSchema>;
|
|
|
231
228
|
* loc can be:
|
|
232
229
|
* - "append" / "prepend" — file-level insert
|
|
233
230
|
* - { append: anchor } / { prepend: anchor } — insert relative to anchor
|
|
234
|
-
* - {
|
|
235
|
-
* - { replace_block: { pos, end } } — replace inclusive range
|
|
231
|
+
* - { range: { pos, end } } — replace inclusive range
|
|
236
232
|
*/
|
|
237
233
|
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
238
234
|
const result: HashlineEdit[] = [];
|
|
@@ -253,17 +249,13 @@ function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
|
253
249
|
const anchor = tryParseTag(loc.prepend);
|
|
254
250
|
if (!anchor) throw new Error("prepend requires a valid anchor.");
|
|
255
251
|
result.push({ op: "prepend_at", pos: anchor, lines });
|
|
256
|
-
} else if ("
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} else if ("block" in loc) {
|
|
261
|
-
const posAnchor = tryParseTag(loc.block.pos);
|
|
262
|
-
const endAnchor = tryParseTag(loc.block.end);
|
|
263
|
-
if (!posAnchor || !endAnchor) throw new Error("block requires valid pos and end anchors.");
|
|
252
|
+
} else if ("range" in loc) {
|
|
253
|
+
const posAnchor = tryParseTag(loc.range.pos);
|
|
254
|
+
const endAnchor = tryParseTag(loc.range.end);
|
|
255
|
+
if (!posAnchor || !endAnchor) throw new Error("range requires valid pos and end anchors.");
|
|
264
256
|
result.push({ op: "replace_range", pos: posAnchor, end: endAnchor, lines });
|
|
265
257
|
} else {
|
|
266
|
-
throw new Error("Unknown loc shape. Expected append, prepend,
|
|
258
|
+
throw new Error("Unknown loc shape. Expected append, prepend, or range.");
|
|
267
259
|
}
|
|
268
260
|
} else {
|
|
269
261
|
throw new Error(`Invalid loc value: ${JSON.stringify(loc)}`);
|
package/src/patch/shared.ts
CHANGED
|
@@ -164,9 +164,8 @@ function formatStreamingHashlineEdits(edits: Partial<HashlineToolEdit>[], uiThem
|
|
|
164
164
|
return { srcLabel: `• ${loc} (file-level)`, dst: contentLines };
|
|
165
165
|
}
|
|
166
166
|
if (typeof loc === "object" && loc) {
|
|
167
|
-
if ("
|
|
168
|
-
|
|
169
|
-
return { srcLabel: `• block ${rb.pos ?? "?"}…${rb.end ?? "?"}`, dst: contentLines };
|
|
167
|
+
if ("range" in loc && typeof loc.range === "object" && loc.range) {
|
|
168
|
+
return { srcLabel: `• range ${loc.range.pos ?? "?"}…${loc.range.end ?? "?"}`, dst: contentLines };
|
|
170
169
|
}
|
|
171
170
|
if ("line" in loc) {
|
|
172
171
|
return { srcLabel: `• line ${(loc as { line: string }).line}`, dst: contentLines };
|
|
@@ -30,10 +30,10 @@ You **MUST** use specialized tools instead of bash for ALL file operations:
|
|
|
30
30
|
|---|---|
|
|
31
31
|
|`cat file`, `head -n N file`|`read(path="file", limit=N)`|
|
|
32
32
|
|`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
|
|
33
|
-
|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`|
|
|
33
|
+
{{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`|
|
|
34
34
|
|`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`|
|
|
35
|
-
|`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|
|
|
36
|
-
|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|
|
|
35
|
+
|`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}}
|
|
36
|
+
{{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
|
|
37
37
|
|`ls dir/`|`read(path="dir/")`|
|
|
38
38
|
|`cat <<'EOF' > file`|`write(path="file", content="…")`|
|
|
39
39
|
|`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
|
|
@@ -16,40 +16,39 @@ Read the file first. Copy anchors exactly from the latest `read` output. In one
|
|
|
16
16
|
**`loc` values**
|
|
17
17
|
- `"append"` / `"prepend"` — insert at end/start of file
|
|
18
18
|
- `{ append: "N#ID" }` / `{ prepend: "N#ID" }` — insert after/before anchored line
|
|
19
|
-
- `{
|
|
20
|
-
|
|
21
|
-
</operations>
|
|
19
|
+
- `{ range: { pos: "N#ID", end: "N#ID" } }` — replace inclusive range of lines `pos..end` with new content
|
|
20
|
+
</operations>
|
|
22
21
|
|
|
23
22
|
<examples>
|
|
24
|
-
All examples below reference the same file
|
|
25
|
-
```ts
|
|
26
|
-
{{
|
|
27
|
-
{{
|
|
28
|
-
{{
|
|
29
|
-
{{
|
|
30
|
-
{{
|
|
31
|
-
{{
|
|
32
|
-
{{
|
|
33
|
-
{{
|
|
34
|
-
{{
|
|
35
|
-
{{
|
|
36
|
-
{{
|
|
37
|
-
{{
|
|
38
|
-
{{
|
|
39
|
-
{{
|
|
40
|
-
{{
|
|
41
|
-
{{
|
|
42
|
-
{{
|
|
43
|
-
{{
|
|
23
|
+
All examples below reference the same file:
|
|
24
|
+
```ts title="a.ts"
|
|
25
|
+
{{hline 1 "// @ts-ignore"}}
|
|
26
|
+
{{hline 2 "const timeout = 5000;"}}
|
|
27
|
+
{{hline 3 "const tag = \"DO NOT SHIP\";"}}
|
|
28
|
+
{{hline 4 ""}}
|
|
29
|
+
{{hline 5 "function alpha() {"}}
|
|
30
|
+
{{hline 6 "\tlog();"}}
|
|
31
|
+
{{hline 7 "}"}}
|
|
32
|
+
{{hline 8 ""}}
|
|
33
|
+
{{hline 9 "function beta() {"}}
|
|
34
|
+
{{hline 10 "\t// TODO: remove after migration"}}
|
|
35
|
+
{{hline 11 "\tlegacy();"}}
|
|
36
|
+
{{hline 12 "\ttry {"}}
|
|
37
|
+
{{hline 13 "\t\treturn parse(data);"}}
|
|
38
|
+
{{hline 14 "\t} catch (err) {"}}
|
|
39
|
+
{{hline 15 "\t\tconsole.error(err);"}}
|
|
40
|
+
{{hline 16 "\t\treturn null;"}}
|
|
41
|
+
{{hline 17 "\t}"}}
|
|
42
|
+
{{hline 18 "}"}}
|
|
44
43
|
```
|
|
45
44
|
|
|
46
45
|
<example name="replace a block body">
|
|
47
46
|
Replace only the catch body. Do not target the shared boundary line `} catch (err) {`.
|
|
48
47
|
```
|
|
49
48
|
{
|
|
50
|
-
path: "
|
|
49
|
+
path: "a.ts",
|
|
51
50
|
edits: [{
|
|
52
|
-
loc: {
|
|
51
|
+
loc: { range: { pos: {{href 15 "\t\tconsole.error(err);"}}, end: {{href 16 "\t\treturn null;"}} } },
|
|
53
52
|
content: [
|
|
54
53
|
"\t\tif (isEnoent(err)) return null;",
|
|
55
54
|
"\t\tthrow err;"
|
|
@@ -60,12 +59,12 @@ Replace only the catch body. Do not target the shared boundary line `} catch (er
|
|
|
60
59
|
</example>
|
|
61
60
|
|
|
62
61
|
<example name="replace whole block including closing brace">
|
|
63
|
-
Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be {{
|
|
62
|
+
Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be {{href 7 "}"}} because `content` includes `}`.
|
|
64
63
|
```
|
|
65
64
|
{
|
|
66
|
-
path: "
|
|
65
|
+
path: "a.ts",
|
|
67
66
|
edits: [{
|
|
68
|
-
loc: {
|
|
67
|
+
loc: { range: { pos: {{href 6 "\tlog();"}}, end: {{href 7 "}"}} } },
|
|
69
68
|
content: [
|
|
70
69
|
"\tvalidate();",
|
|
71
70
|
"\tlog();",
|
|
@@ -74,15 +73,15 @@ Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be
|
|
|
74
73
|
}]
|
|
75
74
|
}
|
|
76
75
|
```
|
|
77
|
-
**Wrong**: using `end: {{
|
|
76
|
+
**Wrong**: using `end: {{href 6 "\tlog();"}}` with the same content — line 7 (`}`) survives the replacement AND content emits `}`, producing two closing braces.
|
|
78
77
|
</example>
|
|
79
78
|
|
|
80
79
|
<example name="replace one line">
|
|
81
80
|
```
|
|
82
81
|
{
|
|
83
|
-
path: "
|
|
82
|
+
path: "a.ts",
|
|
84
83
|
edits: [{
|
|
85
|
-
loc: {
|
|
84
|
+
loc: { range: { pos: {{href 2 "const timeout = 5000;"}}, end: {{href 2 "const timeout = 5000;"}} } },
|
|
86
85
|
content: ["const timeout = 30_000;"]
|
|
87
86
|
}]
|
|
88
87
|
}
|
|
@@ -92,9 +91,9 @@ Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be
|
|
|
92
91
|
<example name="delete a range">
|
|
93
92
|
```
|
|
94
93
|
{
|
|
95
|
-
path: "
|
|
94
|
+
path: "a.ts",
|
|
96
95
|
edits: [{
|
|
97
|
-
loc: {
|
|
96
|
+
loc: { range: { pos: {{href 10 "\t// TODO: remove after migration"}}, end: {{href 11 "\tlegacy();"}} } },
|
|
98
97
|
content: null
|
|
99
98
|
}]
|
|
100
99
|
}
|
|
@@ -105,9 +104,9 @@ Replace the entire body of `alpha`, including its closing `}`. `end` **MUST** be
|
|
|
105
104
|
When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
106
105
|
```
|
|
107
106
|
{
|
|
108
|
-
path: "
|
|
107
|
+
path: "a.ts",
|
|
109
108
|
edits: [{
|
|
110
|
-
loc: { prepend: {{
|
|
109
|
+
loc: { prepend: {{href 9 "function beta() {"}} },
|
|
111
110
|
content: [
|
|
112
111
|
"function gamma() {",
|
|
113
112
|
"\tvalidate();",
|
|
@@ -123,10 +122,10 @@ When adding a sibling declaration, prefer `prepend` on the next declaration.
|
|
|
123
122
|
<critical>
|
|
124
123
|
- Make the minimum exact edit. Do not rewrite nearby code unless the consumed range requires it.
|
|
125
124
|
- Use anchors exactly as `N#ID` from the latest `read` output.
|
|
126
|
-
- `
|
|
125
|
+
- `range` requires both `pos` and `end`.
|
|
127
126
|
- When your replacement `content` ends with a closing delimiter (`}`, `*/`, `)`, `]`), verify `end` includes the original line carrying that delimiter. If `end` stops one line too early, the original delimiter survives and your content adds a second copy.
|
|
128
127
|
- **Self-check**: compare the last line of `content` with the line immediately after `end` in the file. If they match (e.g., both are `}`), extend `end` to include that line.
|
|
129
|
-
- For a
|
|
128
|
+
- For a range, either replace only the body or replace the whole range. Do not split range boundaries.
|
|
130
129
|
- `content` must be literal file content with matching indentation. If the file uses tabs, use real tabs.
|
|
131
|
-
-
|
|
132
|
-
</critical>
|
|
130
|
+
- You **MUST NOT** use this tool to reformat or clean up unrelated code. **ALWAYS** use project-specific tooling like linters or code formatters which are much more efficient and reliable.
|
|
131
|
+
</critical>
|
|
@@ -55,7 +55,12 @@ import { abortableSleep, getAgentDbPath, isEnoent, logger } from "@oh-my-pi/pi-u
|
|
|
55
55
|
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
56
56
|
import type { Rule } from "../capability/rule";
|
|
57
57
|
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
58
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
extractExplicitThinkingSelector,
|
|
60
|
+
formatModelString,
|
|
61
|
+
parseModelString,
|
|
62
|
+
resolveModelRoleValue,
|
|
63
|
+
} from "../config/model-resolver";
|
|
59
64
|
import { expandPromptTemplate, type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
60
65
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
61
66
|
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
@@ -170,6 +175,8 @@ export type AgentSessionEvent =
|
|
|
170
175
|
}
|
|
171
176
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
172
177
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
178
|
+
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
179
|
+
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
173
180
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
174
181
|
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
175
182
|
| { type: "todo_auto_clear" };
|
|
@@ -315,6 +322,46 @@ interface HandoffOptions {
|
|
|
315
322
|
|
|
316
323
|
const AUTO_HANDOFF_THRESHOLD_FOCUS = renderPromptTemplate(autoHandoffThresholdFocusPrompt);
|
|
317
324
|
|
|
325
|
+
type RetryFallbackChains = Record<string, string[]>;
|
|
326
|
+
|
|
327
|
+
type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
|
|
328
|
+
|
|
329
|
+
interface RetryFallbackSelector {
|
|
330
|
+
raw: string;
|
|
331
|
+
provider: string;
|
|
332
|
+
id: string;
|
|
333
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface ActiveRetryFallbackState {
|
|
337
|
+
role: string;
|
|
338
|
+
originalSelector: string;
|
|
339
|
+
originalThinkingLevel: ThinkingLevel | undefined;
|
|
340
|
+
lastAppliedFallbackThinkingLevel: ThinkingLevel | undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
344
|
+
const trimmed = selector.trim();
|
|
345
|
+
if (!trimmed) return undefined;
|
|
346
|
+
const parsed = parseModelString(trimmed);
|
|
347
|
+
if (!parsed) return undefined;
|
|
348
|
+
return {
|
|
349
|
+
raw: trimmed,
|
|
350
|
+
provider: parsed.provider,
|
|
351
|
+
id: parsed.id,
|
|
352
|
+
thinkingLevel: parsed.thinkingLevel,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatRetryFallbackSelector(model: Model, thinkingLevel: ThinkingLevel | undefined): string {
|
|
357
|
+
const selector = formatModelString(model);
|
|
358
|
+
return thinkingLevel ? `${selector}:${thinkingLevel}` : selector;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function formatRetryFallbackBaseSelector(selector: RetryFallbackSelector): string {
|
|
362
|
+
return `${selector.provider}/${selector.id}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
318
365
|
const noOpUIContext: ExtensionUIContext = {
|
|
319
366
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
320
367
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -352,6 +399,7 @@ export class AgentSession {
|
|
|
352
399
|
readonly sessionManager: SessionManager;
|
|
353
400
|
readonly settings: Settings;
|
|
354
401
|
readonly searchDb: SearchDb | undefined;
|
|
402
|
+
readonly configWarnings: string[] = [];
|
|
355
403
|
|
|
356
404
|
#asyncJobManager: AsyncJobManager | undefined = undefined;
|
|
357
405
|
#scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
|
|
@@ -391,7 +439,7 @@ export class AgentSession {
|
|
|
391
439
|
#retryAttempt = 0;
|
|
392
440
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
393
441
|
#retryResolve: (() => void) | undefined = undefined;
|
|
394
|
-
|
|
442
|
+
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
395
443
|
// Todo completion reminder state
|
|
396
444
|
#todoReminderCount = 0;
|
|
397
445
|
#todoPhases: TodoPhase[] = [];
|
|
@@ -478,6 +526,7 @@ export class AgentSession {
|
|
|
478
526
|
this.#customCommands = config.customCommands ?? [];
|
|
479
527
|
this.#skillsSettings = config.skillsSettings;
|
|
480
528
|
this.#modelRegistry = config.modelRegistry;
|
|
529
|
+
this.#validateRetryFallbackChains();
|
|
481
530
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
482
531
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
483
532
|
this.#onPayload = config.onPayload;
|
|
@@ -787,6 +836,13 @@ export class AgentSession {
|
|
|
787
836
|
assistantMsg.stopReason !== "aborted" &&
|
|
788
837
|
this.#retryAttempt > 0
|
|
789
838
|
) {
|
|
839
|
+
if (this.#activeRetryFallback && this.model) {
|
|
840
|
+
await this.#emitSessionEvent({
|
|
841
|
+
type: "retry_fallback_succeeded",
|
|
842
|
+
model: formatRetryFallbackSelector(this.model, this.thinkingLevel),
|
|
843
|
+
role: this.#activeRetryFallback.role,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
790
846
|
await this.#emitSessionEvent({
|
|
791
847
|
type: "auto_retry_end",
|
|
792
848
|
success: true,
|
|
@@ -985,6 +1041,7 @@ export class AgentSession {
|
|
|
985
1041
|
return;
|
|
986
1042
|
}
|
|
987
1043
|
try {
|
|
1044
|
+
await this.#maybeRestoreRetryFallbackPrimary();
|
|
988
1045
|
await this.agent.continue();
|
|
989
1046
|
} catch {
|
|
990
1047
|
options?.onError?.();
|
|
@@ -2288,6 +2345,8 @@ export class AgentSession {
|
|
|
2288
2345
|
// Reset todo reminder count on new user prompt
|
|
2289
2346
|
this.#todoReminderCount = 0;
|
|
2290
2347
|
|
|
2348
|
+
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2349
|
+
|
|
2291
2350
|
// Validate model
|
|
2292
2351
|
if (!this.model) {
|
|
2293
2352
|
throw new Error(
|
|
@@ -3108,6 +3167,7 @@ export class AgentSession {
|
|
|
3108
3167
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3109
3168
|
}
|
|
3110
3169
|
|
|
3170
|
+
this.#clearActiveRetryFallback();
|
|
3111
3171
|
this.#setModelWithProviderSessionReset(model);
|
|
3112
3172
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
3113
3173
|
this.settings.setModelRole(role, this.#formatRoleModelValue(role, model));
|
|
@@ -3128,6 +3188,7 @@ export class AgentSession {
|
|
|
3128
3188
|
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
3129
3189
|
}
|
|
3130
3190
|
|
|
3191
|
+
this.#clearActiveRetryFallback();
|
|
3131
3192
|
this.#setModelWithProviderSessionReset(model);
|
|
3132
3193
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
3133
3194
|
this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
@@ -3253,6 +3314,7 @@ export class AgentSession {
|
|
|
3253
3314
|
const next = scopedModels[nextIndex];
|
|
3254
3315
|
|
|
3255
3316
|
// Apply model
|
|
3317
|
+
this.#clearActiveRetryFallback();
|
|
3256
3318
|
this.#setModelWithProviderSessionReset(next.model);
|
|
3257
3319
|
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
3258
3320
|
this.settings.setModelRole("default", this.#formatRoleModelValue("default", next.model));
|
|
@@ -3281,11 +3343,11 @@ export class AgentSession {
|
|
|
3281
3343
|
throw new Error(`No API key for ${nextModel.provider}/${nextModel.id}`);
|
|
3282
3344
|
}
|
|
3283
3345
|
|
|
3346
|
+
this.#clearActiveRetryFallback();
|
|
3284
3347
|
this.#setModelWithProviderSessionReset(nextModel);
|
|
3285
3348
|
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
3286
3349
|
this.settings.setModelRole("default", this.#formatRoleModelValue("default", nextModel));
|
|
3287
3350
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
3288
|
-
|
|
3289
3351
|
// Re-apply the current thinking level for the newly selected model
|
|
3290
3352
|
this.setThinkingLevel(this.thinkingLevel);
|
|
3291
3353
|
|
|
@@ -4768,6 +4830,217 @@ export class AgentSession {
|
|
|
4768
4830
|
);
|
|
4769
4831
|
}
|
|
4770
4832
|
|
|
4833
|
+
#getRetryFallbackChains(): RetryFallbackChains {
|
|
4834
|
+
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
4835
|
+
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
4836
|
+
return configuredChains as RetryFallbackChains;
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
#validateRetryFallbackChains(): void {
|
|
4840
|
+
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
4841
|
+
if (configuredChains === undefined) return;
|
|
4842
|
+
if (!configuredChains || typeof configuredChains !== "object" || Array.isArray(configuredChains)) {
|
|
4843
|
+
const msg = "retry.fallbackChains must be a mapping of role names to selector arrays.";
|
|
4844
|
+
logger.warn(msg);
|
|
4845
|
+
this.configWarnings.push(msg);
|
|
4846
|
+
return;
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
for (const [role, chain] of Object.entries(configuredChains)) {
|
|
4850
|
+
if (!Array.isArray(chain)) {
|
|
4851
|
+
const msg = `Fallback chain for role '${role}' must be an array of selector strings.`;
|
|
4852
|
+
logger.warn(msg);
|
|
4853
|
+
this.configWarnings.push(msg);
|
|
4854
|
+
continue;
|
|
4855
|
+
}
|
|
4856
|
+
for (const selectorStr of chain) {
|
|
4857
|
+
if (typeof selectorStr !== "string") {
|
|
4858
|
+
const msg = `Fallback chain for role '${role}' contains a non-string selector.`;
|
|
4859
|
+
logger.warn(msg);
|
|
4860
|
+
this.configWarnings.push(msg);
|
|
4861
|
+
continue;
|
|
4862
|
+
}
|
|
4863
|
+
const parsed = parseRetryFallbackSelector(selectorStr);
|
|
4864
|
+
if (!parsed) {
|
|
4865
|
+
const msg = `Invalid fallback selector format in role '${role}': ${selectorStr}`;
|
|
4866
|
+
logger.warn(msg);
|
|
4867
|
+
this.configWarnings.push(msg);
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4870
|
+
const exists = this.#modelRegistry.find(parsed.provider, parsed.id);
|
|
4871
|
+
if (!exists) {
|
|
4872
|
+
const msg = `Fallback chain for role '${role}' references unknown model: ${selectorStr}`;
|
|
4873
|
+
logger.warn(msg);
|
|
4874
|
+
this.configWarnings.push(msg);
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
|
|
4880
|
+
#getRetryFallbackRevertPolicy(): RetryFallbackRevertPolicy {
|
|
4881
|
+
return this.settings.get("retry.fallbackRevertPolicy") === "never" ? "never" : "cooldown-expiry";
|
|
4882
|
+
}
|
|
4883
|
+
|
|
4884
|
+
#getRetryFallbackPrimarySelector(role: string): RetryFallbackSelector | undefined {
|
|
4885
|
+
const configuredSelector = this.settings.getModelRole(role);
|
|
4886
|
+
return configuredSelector ? parseRetryFallbackSelector(configuredSelector) : undefined;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
#clearActiveRetryFallback(): void {
|
|
4890
|
+
this.#activeRetryFallback = undefined;
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4893
|
+
#isRetryFallbackSelectorSuppressed(selector: RetryFallbackSelector): boolean {
|
|
4894
|
+
return this.#modelRegistry.isSelectorSuppressed(selector.raw);
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
#noteRetryFallbackCooldown(currentSelector: string, retryAfterMs: number | undefined, errorMessage: string): void {
|
|
4898
|
+
let cooldownMs = retryAfterMs;
|
|
4899
|
+
if (!cooldownMs || cooldownMs <= 0) {
|
|
4900
|
+
const reason = parseRateLimitReason(errorMessage);
|
|
4901
|
+
cooldownMs = reason === "UNKNOWN" ? 5 * 60 * 1000 : calculateRateLimitBackoffMs(reason);
|
|
4902
|
+
}
|
|
4903
|
+
this.#modelRegistry.suppressSelector(currentSelector, Date.now() + cooldownMs);
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
#resolveRetryFallbackRole(currentSelector: string): string | undefined {
|
|
4907
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
4908
|
+
if (!parsedCurrent) return undefined;
|
|
4909
|
+
const currentBaseSelector = formatRetryFallbackBaseSelector(parsedCurrent);
|
|
4910
|
+
for (const role of Object.keys(this.#getRetryFallbackChains())) {
|
|
4911
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
4912
|
+
if (!primarySelector) continue;
|
|
4913
|
+
if (primarySelector.raw === currentSelector) return role;
|
|
4914
|
+
if (formatRetryFallbackBaseSelector(primarySelector) === currentBaseSelector) return role;
|
|
4915
|
+
}
|
|
4916
|
+
return undefined;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
#getRetryFallbackEffectiveChain(role: string): RetryFallbackSelector[] {
|
|
4920
|
+
const primarySelector = this.#getRetryFallbackPrimarySelector(role);
|
|
4921
|
+
if (!primarySelector) return [];
|
|
4922
|
+
const chain = [primarySelector];
|
|
4923
|
+
const seen = new Set<string>([primarySelector.raw]);
|
|
4924
|
+
for (const selector of this.#getRetryFallbackChains()[role] ?? []) {
|
|
4925
|
+
const parsed = parseRetryFallbackSelector(selector);
|
|
4926
|
+
if (!parsed || seen.has(parsed.raw)) continue;
|
|
4927
|
+
seen.add(parsed.raw);
|
|
4928
|
+
chain.push(parsed);
|
|
4929
|
+
}
|
|
4930
|
+
return chain;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
#findRetryFallbackCandidates(role: string, currentSelector: string): RetryFallbackSelector[] {
|
|
4934
|
+
const chain = this.#getRetryFallbackEffectiveChain(role);
|
|
4935
|
+
if (chain.length <= 1) return [];
|
|
4936
|
+
const parsedCurrent = parseRetryFallbackSelector(currentSelector);
|
|
4937
|
+
const currentBaseSelector = parsedCurrent ? formatRetryFallbackBaseSelector(parsedCurrent) : undefined;
|
|
4938
|
+
const exactIndex = chain.findIndex(selector => selector.raw === currentSelector);
|
|
4939
|
+
if (exactIndex >= 0) return chain.slice(exactIndex + 1);
|
|
4940
|
+
const baseIndex = currentBaseSelector
|
|
4941
|
+
? chain.findIndex(selector => formatRetryFallbackBaseSelector(selector) === currentBaseSelector)
|
|
4942
|
+
: -1;
|
|
4943
|
+
if (baseIndex >= 0) return chain.slice(baseIndex + 1);
|
|
4944
|
+
return chain.slice(1);
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
async #applyRetryFallbackCandidate(
|
|
4948
|
+
role: string,
|
|
4949
|
+
selector: RetryFallbackSelector,
|
|
4950
|
+
currentSelector: string,
|
|
4951
|
+
): Promise<void> {
|
|
4952
|
+
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
4953
|
+
if (!candidate) {
|
|
4954
|
+
throw new Error(`Retry fallback model not found: ${selector.raw}`);
|
|
4955
|
+
}
|
|
4956
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
4957
|
+
if (!apiKey) {
|
|
4958
|
+
throw new Error(`No API key for retry fallback ${selector.raw}`);
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4961
|
+
const currentThinkingLevel = this.thinkingLevel;
|
|
4962
|
+
const nextThinkingLevel = selector.thinkingLevel ?? currentThinkingLevel;
|
|
4963
|
+
|
|
4964
|
+
this.#setModelWithProviderSessionReset(candidate);
|
|
4965
|
+
this.sessionManager.appendModelChange(`${candidate.provider}/${candidate.id}`, "temporary");
|
|
4966
|
+
this.settings.getStorage()?.recordModelUsage(`${candidate.provider}/${candidate.id}`);
|
|
4967
|
+
this.setThinkingLevel(nextThinkingLevel);
|
|
4968
|
+
if (!this.#activeRetryFallback) {
|
|
4969
|
+
this.#activeRetryFallback = {
|
|
4970
|
+
role,
|
|
4971
|
+
originalSelector: currentSelector,
|
|
4972
|
+
originalThinkingLevel: currentThinkingLevel,
|
|
4973
|
+
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
4974
|
+
};
|
|
4975
|
+
} else {
|
|
4976
|
+
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
4977
|
+
}
|
|
4978
|
+
await this.#emitSessionEvent({
|
|
4979
|
+
type: "retry_fallback_applied",
|
|
4980
|
+
from: currentSelector,
|
|
4981
|
+
to: selector.raw,
|
|
4982
|
+
role,
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
4987
|
+
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
4988
|
+
if (!role) return false;
|
|
4989
|
+
|
|
4990
|
+
for (const selector of this.#findRetryFallbackCandidates(role, currentSelector)) {
|
|
4991
|
+
if (this.#isRetryFallbackSelectorSuppressed(selector)) continue;
|
|
4992
|
+
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
4993
|
+
if (!candidate) continue;
|
|
4994
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
4995
|
+
if (!apiKey) continue;
|
|
4996
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
4997
|
+
return true;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
return false;
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
5004
|
+
if (!this.#activeRetryFallback) return;
|
|
5005
|
+
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
5006
|
+
|
|
5007
|
+
const {
|
|
5008
|
+
originalSelector: originalSelectorRaw,
|
|
5009
|
+
originalThinkingLevel,
|
|
5010
|
+
lastAppliedFallbackThinkingLevel,
|
|
5011
|
+
} = this.#activeRetryFallback;
|
|
5012
|
+
const originalSelector = parseRetryFallbackSelector(originalSelectorRaw);
|
|
5013
|
+
if (!originalSelector) {
|
|
5014
|
+
this.#clearActiveRetryFallback();
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
|
|
5018
|
+
const currentModel = this.model;
|
|
5019
|
+
if (!currentModel) return;
|
|
5020
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, this.thinkingLevel);
|
|
5021
|
+
if (currentSelector === originalSelector.raw) {
|
|
5022
|
+
if (!this.#isRetryFallbackSelectorSuppressed(originalSelector)) {
|
|
5023
|
+
this.#clearActiveRetryFallback();
|
|
5024
|
+
}
|
|
5025
|
+
return;
|
|
5026
|
+
}
|
|
5027
|
+
if (this.#isRetryFallbackSelectorSuppressed(originalSelector)) return;
|
|
5028
|
+
|
|
5029
|
+
const primaryModel = this.#modelRegistry.find(originalSelector.provider, originalSelector.id);
|
|
5030
|
+
if (!primaryModel) return;
|
|
5031
|
+
const apiKey = await this.#modelRegistry.getApiKey(primaryModel, this.sessionId);
|
|
5032
|
+
if (!apiKey) return;
|
|
5033
|
+
|
|
5034
|
+
const currentThinkingLevel = this.thinkingLevel;
|
|
5035
|
+
const thinkingToApply =
|
|
5036
|
+
currentThinkingLevel === lastAppliedFallbackThinkingLevel ? originalThinkingLevel : currentThinkingLevel;
|
|
5037
|
+
this.#setModelWithProviderSessionReset(primaryModel);
|
|
5038
|
+
this.sessionManager.appendModelChange(`${primaryModel.provider}/${primaryModel.id}`, "temporary");
|
|
5039
|
+
this.settings.getStorage()?.recordModelUsage(`${primaryModel.provider}/${primaryModel.id}`);
|
|
5040
|
+
this.setThinkingLevel(thinkingToApply);
|
|
5041
|
+
this.#clearActiveRetryFallback();
|
|
5042
|
+
}
|
|
5043
|
+
|
|
4771
5044
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
4772
5045
|
const now = Date.now();
|
|
4773
5046
|
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
@@ -4847,12 +5120,13 @@ export class AgentSession {
|
|
|
4847
5120
|
}
|
|
4848
5121
|
|
|
4849
5122
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
5123
|
+
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
4850
5124
|
let delayMs = retrySettings.baseDelayMs * 2 ** (this.#retryAttempt - 1);
|
|
5125
|
+
let switchedCredential = false;
|
|
5126
|
+
let switchedModel = false;
|
|
4851
5127
|
|
|
4852
5128
|
if (this.model && isUsageLimitError(errorMessage)) {
|
|
4853
|
-
const retryAfterMs =
|
|
4854
|
-
this.#parseRetryAfterMsFromError(errorMessage) ??
|
|
4855
|
-
calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
5129
|
+
const retryAfterMs = parsedRetryAfterMs ?? calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
4856
5130
|
const switched = await this.#modelRegistry.authStorage.markUsageLimitReached(
|
|
4857
5131
|
this.model.provider,
|
|
4858
5132
|
this.sessionId,
|
|
@@ -4862,6 +5136,7 @@ export class AgentSession {
|
|
|
4862
5136
|
},
|
|
4863
5137
|
);
|
|
4864
5138
|
if (switched) {
|
|
5139
|
+
switchedCredential = true;
|
|
4865
5140
|
delayMs = 0;
|
|
4866
5141
|
} else if (retryAfterMs > delayMs) {
|
|
4867
5142
|
// No more accounts to switch to — wait out the backoff
|
|
@@ -4869,6 +5144,17 @@ export class AgentSession {
|
|
|
4869
5144
|
}
|
|
4870
5145
|
}
|
|
4871
5146
|
|
|
5147
|
+
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
5148
|
+
if (!switchedCredential && currentSelector) {
|
|
5149
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
5150
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
|
5151
|
+
if (switchedModel) {
|
|
5152
|
+
delayMs = 0;
|
|
5153
|
+
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
5154
|
+
delayMs = parsedRetryAfterMs;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
|
|
4872
5158
|
await this.#emitSessionEvent({
|
|
4873
5159
|
type: "auto_retry_start",
|
|
4874
5160
|
attempt: this.#retryAttempt,
|
|
@@ -16,7 +16,7 @@ const CHECK_BYTE_COUNT = 1024;
|
|
|
16
16
|
const HEADER_LINE_LIMIT = 40;
|
|
17
17
|
|
|
18
18
|
const KNOWN_GENERATOR_PATTERN =
|
|
19
|
-
"(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen)";
|
|
19
|
+
"(?:protoc(?:-gen-[\\w-]+)?|sqlc|buf|swagger(?:-codegen)?|openapi(?:-generator)?|grpc-gateway|mockery|stringer|easyjson|deepcopy-gen|defaulter-gen|conversion-gen|client-gen|lister-gen|informer-gen|kysely-codegen)";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Strong marker patterns for generated-file headers.
|
package/src/tools/bash.ts
CHANGED
|
@@ -222,6 +222,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
222
222
|
asyncEnabled: this.#asyncEnabled,
|
|
223
223
|
hasAstGrep: this.session.settings.get("astGrep.enabled"),
|
|
224
224
|
hasAstEdit: this.session.settings.get("astEdit.enabled"),
|
|
225
|
+
hasGrep: this.session.settings.get("grep.enabled"),
|
|
226
|
+
hasFind: this.session.settings.get("find.enabled"),
|
|
225
227
|
});
|
|
226
228
|
}
|
|
227
229
|
|