@herbertgao/pi-extensions 2026.9.10 → 2026.9.12
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/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -2
- package/node_modules/@herbertgao/resume-from/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
- package/node_modules/@herbertgao/sol-pi/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
- package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
- package/node_modules/pi-mcp-adapter/README.md +75 -7
- package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
- package/node_modules/pi-mcp-adapter/cli.js +56 -5
- package/node_modules/pi-mcp-adapter/commands.ts +96 -5
- package/node_modules/pi-mcp-adapter/config.ts +73 -6
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
- package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
- package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
- package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
- package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
- package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
- package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
- package/node_modules/pi-mcp-adapter/index.ts +129 -6
- package/node_modules/pi-mcp-adapter/init.ts +6 -19
- package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
- package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
- package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
- package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +17 -9
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
- package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
- package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
- package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
- package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
- package/node_modules/pi-mcp-adapter/types.ts +53 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
- package/node_modules/pi-mcp-adapter/utils.ts +16 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
- package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
- package/node_modules/pi-multi-account/LICENSE +21 -0
- package/node_modules/pi-multi-account/README.md +197 -0
- package/node_modules/pi-multi-account/SECURITY.md +27 -0
- package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
- package/node_modules/pi-multi-account/child-usability.ts +233 -0
- package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
- package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
- package/node_modules/pi-multi-account/context-guard.ts +420 -0
- package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
- package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
- package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
- package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
- package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
- package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
- package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
- package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
- package/node_modules/pi-multi-account/cursor/index.ts +572 -0
- package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
- package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
- package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
- package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
- package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
- package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
- package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
- package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
- package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
- package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
- package/node_modules/pi-multi-account/index.ts +11825 -0
- package/node_modules/pi-multi-account/model-catalog.ts +354 -0
- package/node_modules/pi-multi-account/package.json +101 -0
- package/node_modules/pi-multi-account/pi-contract.ts +281 -0
- package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
- package/node_modules/pi-multi-account/provider-priority.ts +189 -0
- package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
- package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
- package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
- package/node_modules/pi-multi-account/usage.ts +1099 -0
- package/node_modules/pi-typesafe/README.md +6 -2
- package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
- package/node_modules/pi-typesafe/dist/client.js +45 -10
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/package.json +2 -2
- package/node_modules/pi-web-access/CHANGELOG.md +36 -0
- package/node_modules/pi-web-access/README.md +75 -18
- package/node_modules/pi-web-access/anysearch.ts +4 -15
- package/node_modules/pi-web-access/bocha.ts +3 -22
- package/node_modules/pi-web-access/brave.ts +3 -21
- package/node_modules/pi-web-access/brightdata.ts +5 -32
- package/node_modules/pi-web-access/content-find.ts +168 -53
- package/node_modules/pi-web-access/curator-page.ts +4 -1
- package/node_modules/pi-web-access/curator-run.ts +2 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +24620 -0
- package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
- package/node_modules/pi-web-access/duckduckgo.ts +3 -21
- package/node_modules/pi-web-access/extract.ts +3 -1
- package/node_modules/pi-web-access/firecrawl.ts +5 -29
- package/node_modules/pi-web-access/gemini-search.ts +81 -32
- package/node_modules/pi-web-access/index.ts +149 -148
- package/node_modules/pi-web-access/jina-search.ts +4 -15
- package/node_modules/pi-web-access/kagi.ts +4 -13
- package/node_modules/pi-web-access/kimi-search.ts +5 -30
- package/node_modules/pi-web-access/mistral-search.ts +1 -15
- package/node_modules/pi-web-access/ollama.ts +2 -7
- package/node_modules/pi-web-access/openai-search.ts +174 -36
- package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
- package/node_modules/pi-web-access/package.json +10 -4
- package/node_modules/pi-web-access/page-query.ts +10 -2
- package/node_modules/pi-web-access/parallel.ts +1 -15
- package/node_modules/pi-web-access/pdf-extract.ts +3 -0
- package/node_modules/pi-web-access/querit.ts +5 -29
- package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
- package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
- package/node_modules/pi-web-access/search1api.ts +5 -29
- package/node_modules/pi-web-access/searchinfinity.ts +5 -29
- package/node_modules/pi-web-access/searxng.ts +3 -21
- package/node_modules/pi-web-access/serpapi.ts +5 -28
- package/node_modules/pi-web-access/serpbase.ts +3 -22
- package/node_modules/pi-web-access/serpdive.ts +3 -21
- package/node_modules/pi-web-access/serper.ts +5 -28
- package/node_modules/pi-web-access/serply.ts +197 -0
- package/node_modules/pi-web-access/source-check.ts +11 -47
- package/node_modules/pi-web-access/summary-review.ts +7 -3
- package/node_modules/pi-web-access/tavily.ts +3 -21
- package/node_modules/pi-web-access/tinyfish.ts +5 -29
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/node_modules/pi-web-access/valyu.ts +5 -28
- package/node_modules/pi-web-access/xai-search.ts +1 -15
- package/node_modules/pi-web-access/xcrawl.ts +5 -32
- package/package.json +23 -16
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.22.0] — 2026-09-17
|
|
9
|
+
|
|
10
|
+
### Compatibility
|
|
11
|
+
|
|
12
|
+
- Requires Agent Pi ≥0.85.1; release tests now install that supported host version. No host runtime upgrade is performed by this extension.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- GLM Coding Plan CN quota for `zai-coding-cn`: explicit 5-hour/weekly credit windows, plan and reset metadata, fixed HTTPS endpoint and raw API-key authentication. Unknown, expired or malformed telemetry never invents available quota (#58).
|
|
17
|
+
- SuperGrok / xAI OAuth subscription billing usage in the footer and limits command (PR #53). API-key usage remains unknown; private endpoint schema is best-effort.
|
|
18
|
+
|
|
19
|
+
- **Background completions now route through Agent Pi, not through another extension.** On hosts exposing the optional `registerCompletionRouter` API, Multi Account registers one credentialless implementation. Stock Pi 0.85.1 lacks this capability; the integration safely stays inactive there. Hermes and other callers use `ctx.requestCompletion`; this extension sees only operation IDs, model identities, and provider-owned attempt facts. Quota/auth/model/transport failures share cooldown state with foreground routing, but the operation-local lease never calls `pi.setModel` or queues a continuation. Route selection now records `completion_route_select` / `completion_route_unusable` in the debug log so a skipped session route is inspectable without coupling to the caller.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Session model ownership follows Pi's native session branch rather than shared legacy preferences. Another pane's telemetry writes cannot undo a live `/model` choice, and concurrent in-process subagents keep their launch model. Root activation ownership is released on shutdown, preserving `/reload` and session replacement (#51, #60).
|
|
24
|
+
- Manual model switches in auto reasoning mode adopt Pi's per-model thinking default. Automatic failover still preserves effort intent; explicit CLI thinking and forced configuration remain authoritative (#59).
|
|
25
|
+
- Context guard adds elisions based on the already-trimmed outgoing request with soft/target hysteresis. Growing raw history still requests compaction, but small new tool results no longer repeatedly invalidate the cached conversation prefix. Documentation now accounts for Pi's native between-tool compaction (#54).
|
|
26
|
+
- Claude Code billing-header version updated to 2.1.274. Weekly checks maintain one drift issue without forbidden direct pushes or duplicate alerts (#52, #57).
|
|
27
|
+
- xAI quota parsing treats missing or malformed percentages as unknown instead of manufacturing 100% headroom.
|
|
28
|
+
- Codex forced refresh now holds Pi's cross-process credential lock through token exchange and persistence. Concurrent Pi processes adopt the winner's rotated credential instead of reusing the one-use refresh token and forcing another login. Numbered slots update their parent-only OAuth sidecar without exposing credentials in child-facing `auth.json`.
|
|
29
|
+
- Codex now recognizes `refresh_token_reused` as a refresh-rotation error and checks for a newer credential already written by another process.
|
|
30
|
+
|
|
31
|
+
- **Cursor turns now complete the way every other provider does.** Cursor often sends `turnEnded` and then keeps the gRPC Run open; the proxy treated that as "still Working" until the TCP stream happened to close. The OpenAI stream now finishes with `stop` on `turnEnded` (or pauses on a Pi-bound tool call), matching Codex/Claude/Kimi.
|
|
32
|
+
- **Cursor transport liveness and model progress now use separate clocks.** A minute with no complete upstream frame restarts a dead Run quickly. Heartbeats and other decoded housekeeping prove the HTTP/2 transport is alive, while only visible tokens and Pi-bound tools reset the longer five-minute useful-output bound; live long-reasoning Runs are no longer killed after one quiet minute.
|
|
33
|
+
- **Cursor native tools (`read` / `write` / `grep` / `ls` / `shell`) are forwarded onto Pi's MCP tools** instead of being rejected in a loop that never becomes a `tool_calls` pause. Results are written back in Cursor's native result shape so the Run can continue.
|
|
34
|
+
- **A burst of Cursor tools (two `read`s, etc.) is one Pi pause, not a deadlock.** Closing the OpenAI stream on the first exec left the sibling exec unanswered; Pi showed the files and then sat on Working forever. Sibling tools now share a short coalesce window, a late extra exec is answered immediately on the Cursor bridge, and a failed native result encoding throws instead of staying silent.
|
|
35
|
+
- **Every mid-turn Cursor question is answered.** Web search, ask-user, Exa, plan, switch-mode and VM-setup queries used to be ignored, so Cursor waited forever. They are now rejected (or acknowledged, for VM setup) immediately. Unhandled execs throw instead of logging "Bridge may stall".
|
|
36
|
+
- **Cursor's reply after tools is no longer dropped on the paused stream.** The OpenAI stream is closed for the `tool_calls` pause, but the gRPC Run stays open. Writing exec results before attaching the next SSE listener made Cursor's next tokens (and `turnEnded`) land on the dead writer. Pi then showed Working until the stall watchdog fired. The new listener is attached first.
|
|
37
|
+
- **Native Cursor shell results now close their exec response stream.** Sending stdout and an exit event without `ExecClientStreamClose` left Cursor waiting forever after an already completed Pi bash command, while Run heartbeats continued. Terminal shell results (including rejection) now send the close signal with the original RPC id. A fresh live run reproduces the missing-close stall and completes the same native-tool scenario after the fix, without shorter watchdogs or a provider switch.
|
|
38
|
+
- **Restored Cursor sessions no longer turn their entire history into one oversized inline action.** The previous correctness repair made compacted/restored context visible by rendering it into `userMessage`, but Cursor deterministically stopped consuming tool results once that action reached roughly 50–60 KiB. Restored turns now travel in the fetched root-system blob while the action remains a short question or continuation. A copy of the reported ~100k-token session changed from a 512,650-byte action and 60-second tool-resume stall to a 481-byte action, a 361,148-byte context blob, and a completed tool/result/final turn in 8.8 seconds.
|
|
39
|
+
- **Cursor's process-scoped loopback survives in-process session changes.** Pi emits `session_shutdown` for `/new` and session replacement without exiting the process; closing the listener there left the next session registered against a dead port and produced four immediate `Connection error` retries. The listener is now unreferenced so one-shot Pi processes still exit naturally, while only per-session conversation state is cleared on shutdown.
|
|
40
|
+
- **Temporary errors never authorize an account or provider switch.** Cursor stalls and transient 5xx/overload errors retry the exact selected provider/account/model with session-local backoff, then visibly stop after four failed attempts (or an earlier recovery breaker). They no longer poison shared quota health or enter the fallback ladder. This supersedes the previously attempted immediate-stall failover and the v1.21.3 second-error escalation policy. Genuine quota/auth/model-unavailable handling remains separate.
|
|
41
|
+
- **Cursor stall evidence survives useful tool progress until the task really finishes.** A successful tool-call pause cannot erase accumulated stalls or restart the bounded retry budget. Pi's early HTTP 200 cancels a duplicate wake, not the failure history.
|
|
42
|
+
- **Automatic continuation after compaction stays inside the existing recovery chain.** The follow-up is marked as extension-injected before Pi emits its `input` event, so it no longer looks like a new owner prompt, clears the accumulated Cursor failures, or resets the shared eight-hop budget. A queueing failure restores the marker safely and is recorded as `compaction_continue_failed`.
|
|
43
|
+
- **Cancelled and stale Cursor Runs no longer leak HTTP/2 bridge processes.** Closing the bridge's stdin did not guarantee that Cursor closed the response side, so dead `h2-bridge.mjs` children could survive stalls, compaction, and session switches indefinitely. Destructive cleanup now terminates the subprocess after closing the pipe.
|
|
44
|
+
- **A healthy Cursor account no longer runs Pi's native compaction.** Native compact uses the session model; on Cursor that is another silent Grok Run behind "Compacting context…". The summary is generated on a non-Cursor live account, or cancelled if none exists.
|
|
45
|
+
- **`/multi-account status` now hashes vendored `cursor/*.ts`.** Changing the Cursor proxy used to keep the same source fingerprint, so a running Pi could look up to date after a restart was required.
|
|
46
|
+
|
|
47
|
+
## [1.21.3] — 2026-09-05
|
|
48
|
+
|
|
49
|
+
- Provider-level payload shaping now covers independent Pi callers: Anthropic base/alias OAuth, Qwen base/alias role compatibility and Cursor session isolation. Native-wire fixtures and clean installed-Pi activation verify the boundary.
|
|
50
|
+
- Keep the extensions independent through public Agent Pi APIs. No private sibling catalog/health channel or injected sibling transport is required. README links describe the optional companion and invite focused community contributions.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
- Awaited OAuth/failover recovery now respects manual selection, cancellation, shutdown and session replacement. Simultaneous recovery requests share one forced OAuth refresh within the extension instance.
|
|
54
|
+
- Shared cooldown and quota state uses cross-process locking, local-change merging and unique atomic publication, preserving another window's updates and deliberate local clearing.
|
|
55
|
+
- Late quota responses cannot mark a replacement login exhausted or update a closed session.
|
|
56
|
+
|
|
57
|
+
- Credential removal uses Pi's locked deletion API. A modify-only host fails explicitly because `modify(() => undefined)` means no change. Clearing slots avoids intermediate rediscovery and account switching.
|
|
58
|
+
- OAuth shadow/restore and last-resort refresh persistence now use Pi-compatible cross-process file locking. Shadow publication saves the real OAuth recovery copy before publishing a placeholder; restoration writes the real credential before removing its recovery copy. Interrupted-write and actual Pi AuthStorage tests cover preservation.
|
|
59
|
+
- `/multi-account status` and host-capability diagnostics include the loaded source fingerprint so changed source files cannot be mistaken for code already loaded by the running process.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Cursor stall detection rechecks a monotonic deadline when Node wakes its timer early, preventing premature stream termination.
|
|
64
|
+
|
|
65
|
+
- **Cursor streams send headers immediately and keep the client connection open during pauses.** SSE comments arrive every 15 seconds. A five-minute watchdog bounds the wait for decoded upstream progress; heartbeat messages and partial frames do not extend it. Thinking, blob requests, and checkpoints do. Stalled streams end with an explicit timeout error and cancel the bridge. `PI_CURSOR_UPSTREAM_STALL_MS` changes the interval; `0` disables the watchdog.
|
|
66
|
+
|
|
67
|
+
- Integrate the fixes from community PR #48 (Cursor SSE keepalives and bounded upstream stalls) and PR #49 (completed-turn cancellation and resume-watch ownership), preserving the current lifecycle guards. The protobuf proxy regression uses a portable test loader on Node 24/26.
|
|
68
|
+
- The legacy only-active setting no longer deletes inactive account models from Pi's shared registry. A picker preference must not make independently loaded tools unable to resolve a model.
|
|
69
|
+
|
|
70
|
+
- **Pi 0.84.3+ session-scoped model selection is no longer misreported as contract drift.** Pi <=0.84.2 rewrote `settings.json` on every `setModel`; Pi 0.84.3 deliberately changed ordinary selection to session-only and persists a global default only when explicitly requested. The compatibility check is now version-gated, absent saved defaults are valid, and status describes the exact remaining risk: only a bare child launched without `--model` inherits the saved global default. Broker and subagent children are explicitly model-pinned and remain unaffected; multi-account never rewrites the global default to imitate the removed behaviour. Host-version detection reads nearby package metadata or the real CLI entrypoint without importing Pi at runtime, so isolated/degraded installs still load and an unknown host simply omits the optional legacy diagnostic.
|
|
71
|
+
- **Cancelled compaction recovery now listens to Pi's public lifecycle event instead of an internal event extensions never receive.** `compaction_end` belongs to AgentSession's internal stream, so the previous handler could pass a synthetic unit test while never running in a real extension. Pi 0.84.3+ exposes `session_compact_failed`; the guard now subscribes to that event, while its own callback/watchdog remains the compatibility path on older hosts.
|
|
72
|
+
- **Repeated transient failures now escape the failing route instead of parking on it.** The first 5xx/overload may retry the same provider/model once, but a second consecutive failure immediately enters the normal same-model-first, same-quality failover ladder. This coordinates with Pi's separate agent-level retry loop instead of multiplying its attempts into several extension-owned same-Kimi continuation waves. If Pi's own retry succeeds, the obsolete extension wake is cancelled so completed work cannot be replayed after the session idles.
|
|
73
|
+
- **A failed or cancelled context-guard compaction no longer parks the session in Working.** The guard still backs off before asking for another summary, but it resumes the interrupted task (or flushes held input even if the host is not yet idle) instead of dropping continuation when the summarizer aborts with `This operation was aborted`.
|
|
74
|
+
|
|
75
|
+
## [1.21.2] - 2026-09-04
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **A second concurrent Pi session no longer hangs before it can submit prompts.** When another process owns the canonical child-proxy port, the fallback listener now retries on an ephemeral port on the next event-loop turn and resolves from a standalone `listening` handler. This avoids leaving `session_start` pending forever under Pi's compiled Bun runtime after `EADDRINUSE`.
|
|
80
|
+
- **Slot provisioning no longer deletes user-authored model metadata.** Existing `modelOverrides` entries — including per-model `contextWindow` and `maxTokens` — now survive startup publication, `/multi-account rediscover`, catalog replacement, and loopback shutdown cleanup. Generated proxy routes are still removed when their owner exits, so preserving the override layer cannot leave a dead port or credential behind.
|
|
81
|
+
|
|
82
|
+
## [1.21.1] - 2026-09-03
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **Rotation now preserves the user's model class as well as reasoning effort.** Exact-model siblings remain first, while cross-provider fallback uses explicit quality bands: frontier (Sol, Opus and provider flagships), balanced (Terra, Sonnet), and fast (Luna, Haiku). Manual `next`, automatic failover, pending resume, startup preflight, and `best` share the rule, so a Sol task can no longer silently land on Terra because that happened to be the next account's flagship.
|
|
87
|
+
- **Manual control now invalidates the complete previous failover chain.** A user prompt, Pi model selection, `next`, `best`, or `switch` advances the chain epoch and clears pending wakes, watchdogs, injected-continuation state, and recovery counters. A timer armed by an earlier failure can no longer restart its old rotation underneath the account the user selected.
|
|
88
|
+
- **Fresh user messages are no longer swallowed into an extension-private cooldown queue.** When no account is ready the message remains in Pi's visible transcript and Pi owns delivery/retry; the extension only uses Pi's native follow-up queue when an automatic agent turn is genuinely active.
|
|
89
|
+
- **Automatic routing no longer wastes turns on accounts whose fresh usage verdict already says 100% / blocked.** Background usage refresh makes an account eligible as soon as the provider reports recovery; manual `next` remains the explicit one-attempt override for stale telemetry.
|
|
90
|
+
- **Repeated same-model 500 retries are bounded by the recovery breaker.** The extension stops after three failed automatic recoveries instead of multiplying its eight-hop budget by the provider's own HTTP retries, and the continuation text says `retrying` when no account/model switch occurred.
|
|
91
|
+
- **Pending work is session-local.** Parallel Pi windows may still publish a diagnostic marker to the shared state file, but no window reads another window's marker as executable work. Switch, error, resume, continuation, and session-start log events now include a session id so overlapping workdays can be reconstructed reliably.
|
|
92
|
+
- **Explicit CLI model and thinking selections remain authoritative.** A launch with `--model`, `--provider`, `--thinking`, or a model thinking suffix no longer gets overwritten by remembered interactive state and no longer writes the one-shot launch choice back as the global preference. Pi's own CLI resolver supplies the parsing semantics, native model clamps are distinguished from genuine user thinking changes, and later manual `next`, `best`, `switch`, model, or thinking choices correctly take ownership. Integrated from PR #39 by Gabriel Vinhaes.
|
|
93
|
+
|
|
94
|
+
### Tests
|
|
95
|
+
|
|
96
|
+
- Added regression replays for the observed 2026-09-03 workday failures: Sol-to-Terra downgrade, manual `next` followed by stale automatic rotation, invisible cooldown input, fresh-100%-account selection, repeated Kimi 500 continuation loops, and cross-window pending-state contamination.
|
|
97
|
+
|
|
98
|
+
## [1.21.0] - 2026-09-03
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- **A `pi-subagents` child now keeps the exact model its parent runner launched.** Native children carry `PI_SUBAGENT_CHILD=1`; inside that boundary this extension remains available for provider/account registration, OAuth request shaping, and catalogs, but becomes passive as a router. It no longer restores the interactive process's remembered model during `session_start`, persists a child model as the user's preference, intercepts provider errors, switches models, queues inputs, routes compaction, or auto-continues. The provider error reaches `pi-subagents` unchanged, so its verified `fallbackModels` chain remains the single routing authority. This fixes children launched as `qoder/qfmodel:high` being changed milliseconds later to the parent process's `qoder/qmodel_38max:medium` or even an unrelated `openai-codex` model, then rejected by `model_verification_failed` after completing the work.
|
|
103
|
+
|
|
104
|
+
- **A queued owner prompt no longer collides with an automatic resume.** The `input` hook rechecks readiness after awaited preflight work and routes an ordinary prompt arriving during an active automatic turn through Pi's explicit `followUp` queue; held-message flushes also always provide `deliverAs: "followUp"`, which is harmless while idle and closes the check→send race.
|
|
105
|
+
|
|
106
|
+
- **The parent now exposes a credentialless native-provider boundary for the delegation controller.** After the multi-account catalog and Cursor bridge are ready, `ctx.controllerProvider = { providerTransport, routePreflight, routeResolver }` binds each controller lease to the exact provider/model and current parent-resolved credential, preflights exact route readiness before lease admission, then streams through Pi's native provider implementation with retries pinned to zero. The route pool is therefore the complete current subscription-native catalog rather than a Cursor-only canary; OAuth tokens and API keys stay in the parent, including hidden `only-active` slots. Direct child auth remains only as the compatibility fallback when an older host cannot expose the native provider boundary.
|
|
107
|
+
|
|
108
|
+
- **An extension-free child now runs on the account the rotation chose, instead of silently rerouting to another vendor.** Pi keeps `settings.json` pointed at the active model, so anything spawned without this extension loaded reads those two keys and tries to run on the active rotation slot. Publishing that slot's *name* into `models.json` was never enough: measured 2026-08-24, a published slot carrying an OAuth credential fails with `No API key found`, because Pi honours an OAuth credential only for a provider definition that declares the flow and a `models.json` entry declares none. The child does not stop there — Pi falls through to its own first-available provider, and the first configured entry after two unconfigured ones is `anthropic`. That is the whole chain behind a memory extension's consolidation subprocess getting `Third-party apps now draw from your extra usage, not your plan limits` from Anthropic while the rotation was sitting on Codex, with nothing in this extension's black box recording it, because the failure happened inside a `--no-extensions` child it cannot see. The Cursor slots already solved this shape — publish the slot against a route the parent serves on `127.0.0.1`, with a deliberately non-secret placeholder as its `apiKey` — and that pattern now covers the Anthropic and Codex families too. Pi admits the provider because it sees a credential; the child authenticates to this machine with a string worth nothing; the parent swaps in the real token, adds `anthropic-beta: oauth-2025-04-20` or the Codex account header, and forwards. The credential never enters a file a child reads. Verified against a real `pi -p --no-extensions` child with an **empty** `auth.json`: both families resolve the slot, get past authentication, and reach the loopback route; the shaped upstream requests were then confirmed against the real backends (Codex answered HTTP 200 with a live stream, Anthropic authenticated and returned its own rate-limit verdict rather than an auth error). Two measurements shaped the implementation. Pi's Codex API refuses to send anything until it can base64-decode an account id out of the key — `Failed to extract accountId from token` — so that family's placeholder is JWT-shaped: algorithm `none`, no signature, and the placeholder string where the account id would be, with the proxy substituting the real account on the way out so the user's own id stays out of the published file too. And Pi's Codex API tries a WebSocket before falling back to SSE, so the proxy refuses the upgrade immediately rather than spending an upstream round trip on a request that could never have worked. Two gates guard the port, because a loopback port is reachable by every process on the machine and what sits behind it is the user's subscription: only slots we published are served, and only a caller presenting the published placeholder is served — a refusal never echoes what was presented, since that may be someone else's real credential. Paths containing `..` are refused rather than normalised, and the published route is asserted to point at this machine. `childProxy: false` turns the whole thing off, which returns the previous behaviour: those slots become unusable to children again, and `/multi-account status` says so.
|
|
109
|
+
|
|
110
|
+
- **`status` now says which rotation slots an extension-free child can actually authenticate to, and warns when the active one is not among them.** Anything spawned without this extension loaded — a memory extension consolidating its own notes, an external CLI, a bare `pi -p --no-extensions` call — reads `settings.json` to find the active account, and Pi keeps those keys pointed at whatever the rotation last chose. Publishing a slot's *name* into `models.json` is not the same as publishing a usable route, and the difference is invisible from here: measured 2026-08-24, a numbered slot carrying an OAuth credential resolves by name and then dies with `No API key found`, because Pi honours an OAuth credential only for a provider definition that declares the flow and a `models.json` entry declares none. A child in that position does not fail loudly — Pi falls through to its own first-available provider, which is a different account and usually a different vendor. That is exactly how a consolidation child ended up on Anthropic, and billing-refused, while the rotation was sitting on Codex. `status` now reports the count of rotation slots a bare child can use, names the ones it cannot, and prints a warning when the account it would pick up is one of those. The judgement is made from the files a child actually reads rather than from this extension's own registry: a built-in provider is usable whatever we do, because Pi owns its auth flow; an API key resolves through Pi's credential store; a published placeholder pointing at a parent-owned loopback route is usable, which is what the Cursor slots already do; and an OAuth credential on a slot with neither is not. A placeholder key pointing anywhere other than this machine is reported as a misconfiguration rather than a route, since a placeholder means nothing to an upstream that did not issue it.
|
|
111
|
+
|
|
112
|
+
- **The assumptions this extension makes about Pi are now written down, and checked.** Two Pi changes have already broken it and neither announced itself: `AuthStorage` dropped `set()` on 0.84.x, which is how a refreshed Anthropic token was persisted — the file format was unchanged, the write path was not, and the symptom was a fresh `/login anthropic` roughly once a day; and slot catalogues written into `models.json` as bare id strings where Pi requires objects made Pi reject the **entire file**, so every custom provider the user had disappeared at once. Neither `auth.json` nor `models.json` carries a schema version (only `settings.json` records a version, and that is the app's, not the format's), so a format change cannot be detected by comparing anything — the only available detection is to look. Three rules are now enforced in one place. Read only what Pi publishes: `auth.json`, `models.json`, `settings.json`, which is exactly what a bare `pi -p --no-extensions` child reads and nothing deeper. Never write those files directly — writing goes through Pi, which owns locking and migrations. And check the shape on the way in, at session start, before discovery has rewritten anything, so what is judged is the file as Pi left it: an unfamiliar credential kind, a `providers` map that is not a map, a bare model-id string, a file that exists but will not parse. Alongside them a ledger names every load-bearing fact this extension depends on, where Pi promises it, what breaks when it stops being true, and — the part that matters after an upgrade — whether Pi promised it at all. Two rows are marked `observed` rather than `documented`, meaning nothing in Pi commits to them: that Pi rewrites `defaultProvider`/`defaultModel` on every model switch, and that Pi honours an OAuth credential only for a provider definition declaring the flow. The first of those is now checked directly rather than assumed. On the turn after any model switch — ours or the user's — `settings.json` is compared against the model actually running, because those two keys are the only way anything spawned without this extension finds the active account: when they disagree, a child does not fail, it quietly runs on another vendor's account, which is precisely the failure that produced a billing refusal from Anthropic while the rotation was sitting on Codex. Before the first switch of a session the file legitimately still names the previous session's choice, so it is not judged then. Drift is stated once per session, never per turn, and re-stated if it clears and comes back; `status` carries a line for it; logged as `pi_contract_checked`, which also names the unpromised assumptions to re-verify by hand.
|
|
113
|
+
|
|
114
|
+
- **A failover priority ladder, and `/multi-account priority` to set it.** Rotation's first step already worked: of 602 automatic failovers in this machine's black box, 588 stayed inside the same provider family — same subscription, same model, another account. The other 14 are what this fixes. They are the hops taken once every account of the family was spent, and they had no policy behind them: kimi-coding → openai-codex (3), openai-codex → anthropic (3), kimi-coding → anthropic (2), kimi-coding → cursor (2), ollama → kimi-coding (2), cursor → openai-codex (1), anthropic → cursor (1). The comparator ordered cross-family candidates by liveness telemetry and only fell through to the configured family order as its very last tiebreak, by which point it almost never spoke — so "the whole family is spent, where now?" was answered by whatever the telemetry happened to say that second. The second half of the same gap: `providerOrder` was typed to the six specially-managed families, so accounts outside them — openrouter, zai, minimax, opencode-go-api, openai — could not be placed in the order at all and sat last by construction. In 602 automatic failovers not one ever reached them; right as a default, since they bill per token while the managed families are flat-rate, but an accident of the type system rather than a policy anyone chose. `providerPriority` is now an ordered list of provider **groups** (numbered slots of one account collapse to their group, and nicknames like `claude`, `codex`, `kimi` are understood), consulted when the family runs out. Three bounds keep it narrow: it never overrides same-family failover, which is what preserves the model you picked; it never overrides availability, so a cooling account is still not chosen over a free one; and a group nobody ranked sorts after every group somebody did, because silence is not a preference. It deliberately sits *below* the per-account liveness signals — an earlier draft placed it above them and two existing tests caught it, since evidence about one specific account has to beat a preference about its whole category. What is left for the ladder is exactly the case the telemetry cannot separate. Default ships as the managed families in order with per-token providers left off the end. `/multi-account priority` reports the ladder and which rungs you are logged in to, `/multi-account priority cursor claude kimi` sets it, `none` clears it, `reset` restores the default; `status` states it in one line. Logged as `priority_set`.
|
|
115
|
+
|
|
116
|
+
- **The task now carries on after an automatic compaction, instead of stopping until the user types "continue".** Pi ends the run whenever a threshold compaction fires, and half of this machine's compactions were followed by the user typing a short "продовжуй" to get the work moving again (59 compactions in the logs: 49 % a short continue, and most of the remainder the user pasting the summary back in by hand or the failover's own continuation prompt — only a handful a genuinely new task). The route out is Pi's own and was simply unused: `_runAutoCompaction` ends with `return this.agent.hasQueuedMessages()`, and `_runAgentPrompt` turns a `true` there into `await this.agent.continue()`, which drains the follow-up queue and resumes the run. Pi relies on exactly that for overflow recovery, but queues nothing itself after a threshold compaction, so the queue is empty and the run ends. One follow-up is now queued while the compaction is still in flight — synchronously, so it is there by the time Pi reads `hasQueuedMessages()` a few lines later. Nothing else about the mechanism is touched. It stays silent for a manual `/compact` (a deliberate pause), when Pi is already retrying the turn itself (`willRetry`), when the session is idle (nothing would drain the queue, and the same call would append a stray message instead), when something is already queued, and after the user pressed Esc. It shares the `maxAutoContinuesPerPrompt` budget with failover, so the two together still cannot keep a task alive forever without the user saying anything. On the judgement call: `stopReason` does not separate unfinished work from finished — after a clean `stop` the corpus splits 17 continues to 15 new tasks — and no cheap textual signal does either (not one assistant message in the corpus even ends in a question mark). Rather than guess from the outside, the queued message says plainly that finishing is an allowed answer, so a task that really was complete is reported in one line instead of padded with invented work. Logged as `compaction_continue` and `compaction_continue_skipped`; `continueAfterCompaction: false` turns it off.
|
|
117
|
+
|
|
118
|
+
- **A context guard that keeps a request inside the model's window while the agent is still working.** Pi measures the context in exactly two places: after a whole agent run has ended, and just before a new user prompt. Inside a run — which is where an autonomous session spends almost all of its time — nothing measures anything, and a retryable provider error makes it worse by taking the retry branch of `_handlePostAgentRun()` and returning *before* the compaction check, so the one checkpoint that exists is routinely spent on a retry. Measured across 18 real coding sessions (~96 MB of transcripts): one agent run between two user messages — 78 minutes, 222 assistant turns, 360 tool results — grew from 85 663 to 542 529 reported tokens against a 272 000 window with no check at all, and a Codex 500 partway through was retried so the run simply carried on. On the main working model 48 % of all requests went out above 80 % of the window and 30 % above 100 %; auto-compaction, when it fired, fired at 94 %–199 % of the window (measured: 107, 121, 122, 125, 132, 134, 136, 137, 141, 145, 153, 199 %). Pi's own headroom is `contextWindow - 16384`, while the growth between two consecutive replies is p99 = 18 950 and p99.9 = 46 731 tokens — the margin is smaller than a normal step. The guard now measures every outgoing request from Pi's own message list plus the system prompt, and above 75 % of the usable window elides the oldest large tool results out of *that request only*; the session transcript is never modified and the most recent 40 000 tokens are never touched. 86.5 % of the weight in that runaway run was tool results, which is why they are what gets given up. Above 70 % a real summary is requested — but only once the agent has settled, because `ctx.compact()` begins with an `abort()` and mid-run would throw away the work in progress. Two details it has to get right and does: once a message is given up it stays given up, so the request prefix does not churn and the provider's prompt cache re-warms once instead of every turn; and the guard keeps its own accounting rather than the provider's, because trimming lowers what the provider reports and would otherwise hide the growth from Pi's threshold entirely. It never learns from an implausible reported size either — Cursor answers from its own checkpointed conversation and openai-codex continues one server-side through WebSocket deltas and `previous_response_id`, so both report their bookkeeping rather than the request we built. Advertised windows are capped at 400 000 by default: `claude-opus-5` and `kimi k3` declare 1 000 000+, which puts Pi's threshold at ~1 032 192 where it can never fire, and those sessions reached 717 813 and 547 203 tokens untouched. Logged as `context_guard_trimmed` and `context_guard_compaction_requested`; `contextGuard: false` turns it off.
|
|
119
|
+
|
|
120
|
+
- **The context guard notices when it has stopped working and says so.** Nothing in it lives inside Pi, so a host update cannot delete it — but a host update *can* quietly stop calling it, and because every handler here is crash-isolated a removed or renamed hook does not fail loudly: the guard simply never runs again and the session goes back to overflowing with nobody watching. That silent inertness is the failure worth reporting. After five real LLM responses with no pre-request hook ever firing, the guard says out loud that it is not running and that long runs are unprotected; if the hook is alive but Pi does not know the active model's context window — the guard's one honest reason to stand down, since it will not guess a limit — it names the model and says how to switch it back on. A host that no longer exposes compaction to extensions is reported separately, because the guard can still trim requests but can no longer ask for a summary. Each warning is said once per session, never per turn. Logged as `context_guard_inert`.
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- **One account table replaces the switch-and-check ritual.** `/multi-account accounts` lists every configured slot with its provider-reported email and alias, plan, primary/secondary quota, and actual routing state (`ready`, cooldown, unavailable, or duplicate). It uses cached snapshots by default; `/multi-account accounts refresh` explicitly refreshes every supported account even when the footer is disabled. Identity comes only from provider response metadata — token claims, API keys and raw credentials are never rendered or logged.
|
|
125
|
+
|
|
126
|
+
- **A governor: one place that can stop everything, instead of a dozen places that each stop themselves.** This changelog contains eight fixes for the same shape — "Runaway failover loop that could freeze the machine", "Escape did not stop the loop", "API-key providers no longer loop forever on a dead key", "a session/rate limit is no longer hot-retried every second", "Compaction no longer leaves 'Compacting context…' spinning forever", "a refusal that cannot be classified no longer strands the session forever", "the stuck-resume watchdog now ACTS instead of only warning", and the 275-switch rotation below. Every one of those was correct, and none of them prevented the next, because they were fixes to *paths*. The extension drives the session from about a dozen places — a failed turn, a resume timer, a queued-input timer, a stuck-turn watchdog, a compaction boundary, two preflights — and each decided for itself when to give up, using one of six unrelated counters (`autoContinuesThisPrompt`, the pending-resume hops, the stuck reminders, the recovery breaker, the guard's compaction cooldown, the queue length). Not one could see the others, and none could see the session. A new path, or a new interaction between two old ones, was therefore an unbounded loop by default, and only became safe if whoever wrote it remembered to bound it. The default is now inverted. Every automatic action asks one gate first, and the question it answers is not "have I done this too often" but "has this session gone anywhere", measured at the provider rather than in our own bookkeeping — the whole failure mode being that our bookkeeping believed it was making progress. Two invariants: no request reached a provider at all across twelve consecutive actions, which is a spin with certainty (the 275 switches made none); and no *successful* response across forty, which is what a hot retry against a limit our meters cannot see looks like, set loose enough that walking an entire seventeen-account fleet — a switch and a real request each — finishes normally, because every one of those requests clears the first invariant. Each invariant arms itself only after it has seen its own signal work once in this session, so a host that reports neither leaves the governor dormant rather than stopping sessions that were fine. Tripping is not a pause: every timer is cleared, every armed continuation dropped, the reason is stated in one sentence, and any text the user had typed and had held for them is handed back in that same sentence rather than discarded with the machinery. It stays stopped until the user sends a message or runs `/multi-account reset`. `status` carries a line saying whether it is running and how close it is; logged as `governor_stopped`. And because a rule that is remembered is a rule that lapses, `test/governed-actions.test.ts` reads the source and fails if any new place drives the session without asking — the enforcement the previous eight fixes each lacked.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **Ollama Cloud now shows its real session and weekly quota instead of claiming that no API exists.** The extension still reads plan and suspension metadata from `/api/me`, then best-effort fetches the live fractional windows from `GET /api/usage`. Because that endpoint and its fixed reset epochs are not yet a stable documented contract, any failure falls back to the plan-only status and inferred reset times remain forecasts subject to the normal recheck ceiling; they never turn a stale estimate into a permanent routing veto.
|
|
131
|
+
|
|
132
|
+
- **Different Codex users in one Team/Business workspace can occupy separate rotation slots.** Pi stores `chatgpt_account_id`, which identifies the selected workspace rather than the user membership inside it. Deduplicating on that value rejected the second login as the same “real account” and also shared cooldowns between distinct users. Codex identity now prefers the JWT's stable `chatgpt_account_user_id` claim; tokens without it use the documented `chatgpt_user_id` plus workspace id before falling back to the legacy stored `accountId`. Re-login detection uses the same identity, so a refreshed token keeps its cooldown while replacing a slot with another user in the same workspace clears the previous user's state.
|
|
133
|
+
|
|
134
|
+
- **The Anthropic OAuth billing header now identifies Claude Code `2.1.259`.** This replaces the stale `2.1.241` client version reported in #29.
|
|
135
|
+
|
|
136
|
+
- **Numbered OAuth accounts no longer degrade into an invalid API-key request after catalog refresh.** Child-facing aliases intentionally publish a non-secret placeholder through the local loopback proxy, but Codex and Anthropic catalog re-registration replaced that loopback URL with the public upstream. Pi then sent the placeholder directly to the provider and every account failed with `Could not parse your authentication token`. Catalog refresh now preserves the account-specific loopback route, and that provider error is classified as authentication failure rather than an unrelated request error.
|
|
137
|
+
|
|
138
|
+
- **A message held for quota recovery is sent immediately after a successful manual account switch.** `/multi-account switch`, `next`, `best`, and ordinary model selection now wake the held-input queue instead of leaving it asleep behind the previous account's cooldown timer. Items leave the queue only after Pi accepts them, and session-owned pending markers prevent another live Pi window's stale timer from clearing or resurrecting newer work.
|
|
139
|
+
|
|
140
|
+
- **Only the process that owns the loopback port may rewrite the files every child reads.** The port was treated as a convenience — if it was taken, the extension quietly listened on a random one instead and republished the whole rotation against itself, on the reasoning that both processes hold the same credentials so either can serve. That is true of serving and false of ownership. A second session, or a `pi-subagents` child (which is just another process on this machine), would repoint every slot in `models.json` at a socket that dies when it exits, and then on its own shutdown restore the credentials and unpublish the routes that belonged to the process still running. The listening half is unchanged — an ephemeral port still serves that process's own callers and costs nothing — but publishing, credential shadowing, restoring and unpublishing are now reserved for the process holding the canonical port. Everything else leaves the shared files exactly as it found them. The canonical port is overridable through `PI_MULTI_ACCOUNT_SLOT_PROXY_PORT`, because a fixed port can collide with unrelated software. Covered by a regression test that runs two real extension instances against one port and asserts the second one changes nothing.
|
|
141
|
+
|
|
142
|
+
- **A memory review or consolidation child no longer dies with "No API key found" or Anthropic's third-party extra-usage refusal while this session is on the active rotation account.** Pi keeps `settings.json` pointed at whichever account rotation chose, so a `pi -p --no-extensions` child — exactly how those background jobs start — tries to run there. Two measurements, not guesses, were the whole failure. A numbered slot (`anthropic-account-2`, `openai-codex-account-4`, …) published into `models.json` with a loopback placeholder still failed at auth, because Pi's resolver keys off the stored credential *type* first: an OAuth blob under the same key and a models.json-only provider (no OAuth method) returns undefined, and the placeholder is never consulted. That is `"No API key found for openai-codex-account-4"`. The empty-`auth.json` canary hid it. Base `anthropic` is different: Pi does resolve the subscription token, then Anthropic refuses the call as a third-party app because the child does not send the client identity the parent adds — measured on this session as `400 invalid_request_error` against `anthropic/claude-opus-5` while the parent on the same account answered. The loopback proxy already existed; it was aimed at the empty-auth case and never published the base account. Numbered OAuth blobs are now held in a parent-only sidecar while the proxy is listening, and `auth.json` shows the child a non-secret `api_key` placeholder so Pi actually sends to the loopback; on stop the blob is written back. Base `anthropic` stays OAuth in `auth.json`, is published against the live loopback, and the proxy admits that access token as well as the placeholder so the parent can add `anthropic-beta: oauth-2025-04-20`. The parent's own `registerProvider("anthropic")` now sets the real Anthropic base URL explicitly, so a published loopback cannot hijack the parent session. On stop, or if the listener never comes up, every own loopback is removed from `models.json` — numbered slots included — so a dead `127.0.0.1:41977` cannot remain as the child's route. A last-resort credential persist writes the child-facing `auth.json` from the file as stored, not from the parent merge, so a refresh cannot dump sidecar OAuth back onto sibling slots. Covered by child-usability, slot-proxy admission, sidecar shadow/restore, and loopback-unpublish tests. After a restart the live `anthropic/claude-opus-5 --no-extensions` child returned OK through the loopback (no third-party 400); the same child on the then-active `cursor/cursor-grok-4.6` died with `No API key found for cursor` because Cursor OAuth was still in `auth.json` while only `models.json` carried `cursor-proxy` — the empty-auth canary hid that too. Cursor and `cursor-account-N` are now shadowed the same way as numbered Anthropic/Codex slots, without routing them through the Anthropic/Codex loopback.
|
|
143
|
+
|
|
144
|
+
- **A delegated child on a Codex account no longer dies before its first tool call.** OpenAI's Responses API does not identify a tool call with one id: it composes `${call_id}|${item.id}`, so a real Codex tool call arrives as `call_Akvop7d1WDfLFTzvStUPaZe3|fc_06d3b3…`. The controller boundary validated that field with an identifier pattern that allowed letters, digits, `.`, `_`, `:` and `-` and nothing else, so the pipe made the frame unparseable and the stream was terminated as `malformed_provider_frame` before the child could read a single file. Nothing downstream was wrong and nothing retried its way out of it, because the shape was rejected identically on every attempt — the delegation simply reported that the provider had produced an unsupported frame. Tool-call identity now accepts exactly one optional `|`-joined pair, each half still constrained as before and the whole still bounded to 128 characters; every other identifier class (namespaces, tool names, block ids) keeps the stricter pattern, so this widens the one field the provider actually composes rather than loosening validation generally. Covered by an adapter test that streams a compound-id tool call through `openai-codex-responses`.
|
|
145
|
+
|
|
146
|
+
- **Compaction failure no longer re-enters immediately, and a guard summary no longer strands the task.** A cancelled or callback-less guard compaction now clears its pending demand and backs off before another settled boundary can ask again. Healthy active models stay on Pi's native compaction path, routed providers receive the full configured per-attempt watchdog, and post-compaction continuation is enabled by default with an explicit opt-out for another extension's continuation owner. A context-guard compaction starts the next turn only from Pi's completion callback; its idle-time `session_compact` hook cannot merely append an undelivered follow-up.
|
|
147
|
+
|
|
148
|
+
- **A held message no longer keeps a session polling every five seconds for six hours.** The queued-input timer had the same shape as the rotation loop and none of its bounds: every wake ran the full account preflight, which could switch account, and rescheduled itself unconditionally — for as long as the message waited, which for a real quota window is hours. Nothing counted those switches and nothing slowed the polling. The switches now go through the governor like every other action, and the poll starts fast — picking up a `/login` from another process within seconds is worth it for the first minute — then relaxes toward the ordinary poll interval instead of holding that rate all day.
|
|
149
|
+
|
|
150
|
+
- **`/multi-account stop` no longer discards what you typed.** A message held during a cooldown was dropped in silence when the wait was cancelled, which from the outside is indistinguishable from the tool eating what you wrote. Every path that abandons the queue now hands the text back in the same sentence that explains the stop, and records it as `held_messages_returned`.
|
|
151
|
+
|
|
152
|
+
- **Two spent accounts can no longer rotate onto each other for ever while a live account waits.** Recorded on this machine: 275 automatic switches in four minutes, alternating between `openai-codex-account-2` and `openai-codex-account-3` about every 1.24 seconds, with **not one request leaving the machine** — the debug log for that window contains 275 `switch` entries and zero `assistant_error` entries — while `anthropic-account-2` sat at 0 % used and `blocked: false`, and the priority ladder ranks it first. Esc did nothing, because there was no run to cancel; the rotation lived entirely in a timer. The cause was a reprieve with no way to spend it. Candidate selection deliberately admits a same-family sibling whose meter reads 100 %, on the grounds that a used-percentage is a forecast about a quota window that cannot see a session or plan limit, and skipping straight to another vendor on a forecast is what once sent a Codex turn to Claude with three Codex slots still untried. That reprieve was written as *has not refused this session* — and the pending-resume path rotates onto an account **without sending it a request**, so it never refuses, so the reprieve never expires. Two such siblings then re-admit each other indefinitely, and same-model ranks them above the free account on every pass. The reprieve is now spent by **selection**: one rotation onto a forecast-spent account is the single attempt its stale forecast has earned, handed back the moment the account answers a real request or the user sends a new prompt. Same-family failover is unchanged for accounts the meter has not written off, which is the 588-of-602 case that already worked.
|
|
153
|
+
|
|
154
|
+
- **The pending-resume wait is now bounded, and backs off when it is achieving nothing.** Two separate holes let that loop run without limit. `maxAutoContinuesPerPrompt` counts turns actually started, so a rotation that starts no turn cost nothing and the budget never moved — rotations that dispatch nothing are now counted against the same ceiling, and when it is reached the session says how many switches it made, that nothing was sent, and where to look. And the wake interval is the minimum recovery time across *every* rotation account, so one free account elsewhere pinned it at its one-second floor: the session re-checked at 1 Hz for as long as the state lasted, which is what wrote four megabytes of debug log in a few minutes. A wake that changes nothing now doubles the wait up to the ordinary poll interval, and anything that counts as progress puts it back on the floor.
|
|
155
|
+
|
|
156
|
+
- **One `/multi-account stop` is enough.** It reliably took two or three. Every automatic path is asynchronous, so a wake already past its guards kept running while the user typed and re-armed the wait a moment after `stop` had cleared it — the command was correct each time and the loop simply outlived it. `stop`, `reset`, `clear` and Esc now bump a chain epoch that every timer and every in-flight continuation captures and re-checks after each await, so a chain started under the old epoch abandons itself instead of re-arming.
|
|
157
|
+
|
|
158
|
+
- **A routed compaction no longer offers the summary to the entire fleet, one full hang-bound at a time.** Also recorded here: 98 consecutive `compaction_failed` entries — `insufficient balance`, `no resource package`, `this model requires a subscription`, `no endpoints found`, `does not exist or you do not have access`, `exceeds the context window` — because the candidate list is the whole rotation, which on a fully populated machine is nine accounts across seven vendors, including the per-token ones that are in the list precisely because nothing measures them. Each got the full 8-minute hang bound of its own, so the worst case was over an hour of "Compacting context…" for a summary that was never going to land. Two bounds now apply, and none of them is silent. At most three accounts are asked, because if three in a row cannot summarize the tenth will not either; the log records which candidates went unasked. `compactionWatchdogMs` is the bound for each attempted provider, not a third of that bound — the previous division aborted ordinary slow summaries at 160 seconds. A timed-out attempt is aborted and the next live account is tried. And a refusal that needs a human before it can change — an empty wallet, a plan that does not include the model, a model that is gone — benches that account for compaction for half an hour, so the next compaction does not rediscover the same wall of refusals.
|
|
159
|
+
|
|
160
|
+
- **A compaction that answers through neither callback no longer switches the context guard off for the rest of the session.** `ctx.compact()` reports back through `onComplete` or `onError`, and an in-flight flag is the only thing standing between the guard and a second request. A host that answers through neither — a cancellation reported only through `compaction_end`, a build that drops the handler — therefore did not merely lose one summary: the guard stopped asking for ever, while the context kept growing, which from the outside is a session that first cannot compact and then cannot send anything at all. The wait is now time-bounded and written off as `context_guard_compaction_unanswered`, and `compaction_end` releases the flag too, since `session_compact` only fires when a compaction actually landed — never on the cancelled and failed paths, which are the ones that matter.
|
|
161
|
+
|
|
162
|
+
- **A held user message no longer blocks compaction indefinitely.** When every account is cooling, a typed message is held in memory and compaction was cancelled to let it flush first. That is right when the message is about to go out and a deadlock when it is not: the context never shrinks, so every request overflows, so compaction fires again, and the held message is no closer to being sent. Compaction is now deferred for the queue only when an account is free within ten seconds; otherwise it proceeds, and the decision is logged as `compaction_allowed_over_queue`.
|
|
163
|
+
- **A Cursor conversation is no longer resumed across a gap it did not see.** Cursor answers from its own copy of the history, and rotation moves a session between providers mid-conversation — so a stretch of turns taken on Anthropic or Codex is simply invisible to it. Nothing invalidated its checkpoint on a provider switch: coming back to Cursor resumed a past with a hole in it exactly the length of the excursion, and the model carried on as if those turns had never happened. Each conversation now records how many completed turns of Pi's transcript it has actually seen; between two consecutive requests that count can grow by at most one, so a bigger jump means turns were completed somewhere else and the checkpoint is dropped in favour of a rebuild. The doubt is resolved toward rebuilding on purpose — a needless rebuild costs tokens, while resuming a stale checkpoint costs correctness and says nothing. Logged as `conversation.stale_checkpoint`. Nothing here touches other providers: they receive Pi's transcript directly and were never affected.
|
|
164
|
+
|
|
165
|
+
- **The restored context now reaches the model instead of sitting where it cannot be read.** There are two ways to put words in front of Cursor and they are not equally reliable: the request's own user message travels inline and always arrives — it is how the first turn of every conversation works — while history turns travel as sha256 blob IDs whose contents stay on our side until Cursor comes back over the KV channel and asks for them. If it never asks, or asks for a blob we do not hold, the answer is an empty result and no error at all. Replaying a restored session as history therefore put the compaction summary somewhere the model could not see it: present in Pi's chat, invisible to the agent, which then acted as if the session had just begun. The rebuild no longer uses history — the summary, the earlier exchanges, the tool calls and their outputs are rendered into the message the model is answering, through the one channel that cannot silently drop them, followed by an explicit instruction to continue rather than start over. A brand new conversation is unchanged: with nothing to restore, the question is sent exactly as typed. On the checkpoint path Cursor still holds its own history, so only the undelivered tool results travel in the request. A missing blob is now logged as `kv.blob_miss` rather than passing for a healthy conversation.
|
|
166
|
+
|
|
167
|
+
- **The Cursor bridge can no longer take Pi down with it.** Pi exited with an uncaught `ERR_STREAM_WRITE_AFTER_END` raised while answering one of Cursor's blob requests: the bridge pipe had just been closed (a compaction starting a new conversation, a cancelled turn, a session teardown) and Cursor, which talks on that pipe in both directions, asked for one more thing. The write site was already inside a try/catch and that is the trap — a write into an ended pipe does not fail there; Node raises it a tick later as an `error` event on the stream, and a stream with no error listener turns that into an uncaughtException. The bridge handle moves to `cursor/bridge-handle.ts` and now enforces both halves: once ended it reports itself dead and silently drops writes, and every stream it owns carries an error listener, so a broken pipe, a late frame, or a bridge that could not be spawned at all is a debug-log line instead of a dead editor. The same rule is applied to the proxy's own HTTP sockets, and an error raised after streaming has begun no longer tries to rewrite the response headers from inside the catch.
|
|
168
|
+
|
|
169
|
+
- **Compaction on Cursor stopped being a treadmill that ate its own summary.** Cursor is stateful: once the bridge holds a checkpoint it replays *that* on every request and ignores the message list Pi sent. So a compaction changed nothing on Cursor's side — the model kept answering from the full pre-compaction history, and `conversationCheckpointUpdate.tokenDetails.usedTokens` kept reporting that history's size. The bridge handed the number straight to Pi as `prompt_tokens`, Pi compared it against a 200k window and compacted again on the very next turn, forever. Measured in a real session: a compaction left a genuine ~22k-token context and an 11 130-char summary; the next reply reported 208 080 prompt tokens; auto-compaction fired again three minutes later. Three fixes, one per link in that chain — Pi's `session_compact` now closes the Cursor-side conversation and mints a new conversation id, so the next request is rebuilt from the compacted turns; the bridge reports the size of the request it actually sent instead of Cursor's private counter (that counter is now only a fallback for when nothing could be measured); and the in-flight bridge is torn down with the conversation, so a pending tool result cannot resume the very conversation being left behind.
|
|
170
|
+
- **A compaction with nothing left to summarize no longer destroys the accumulated summary.** Pi's `compact()` glues a history summary to a turn-prefix summary when the cut lands mid-turn — and when the history range is empty, which is exactly what a rapid second compaction produces, it substitutes the literal string `No prior history.` and never passes `previousSummary` to the model at all. The session's entire accumulated memory is replaced by a note about one truncated turn: in that same session, 11 130 chars became 2 032 describing work from a different day. Compaction is already intercepted here to route it to a healthy account, so the previous summary is now put back over that placeholder before the entry is saved, keeping the turn-prefix context Pi did produce.
|
|
171
|
+
|
|
172
|
+
- **Narrowing `/model` no longer deletes models from Pi's registry.** `only-active` re-registered *every* non-active provider with `models: []`, including providers this extension never created — Pi's own built-ins, `models.json` entries, another extension's registration. That registry is the single place anything asks "does this model exist?", so emptying a provider did not hide a model, it removed it: a pinned `zai/glm-5.1` stopped resolving for everyone, and any caller that falls back to "then just use the session model" silently began running on whichever account rotation happened to be on — a different one every few turns, invisible from here and undebuggable from there. The filter now narrows only the rotation slots this extension invented (`*-account-N`, names Pi cannot know without us); every model Pi knows on its own stays resolvable by reference while `/model` is narrowed. `/model` consequently lists each family's base provider again — that is the part that was never ours to take away.
|
|
173
|
+
- **A slot published into `models.json` is now usable there, not merely resolvable.** The Cursor slots were written into Pi's static registry so an extension-free `pi -p` child could resolve them, but with no key — and Pi refuses a provider it has no credential for, so every child call died at `credentials_not_configured` after the model resolved. Cursor's real credential is an OAuth token this extension holds and a child cannot read, so the published entry now carries the local proxy's own placeholder, which the proxy already treats as "no token on this request" and answers by supplying the real token itself. Verified end to end: a bare `pi -p --no-extensions` child with no `auth.json` at all completes a Cursor request through the proxy.
|
|
174
|
+
|
|
175
|
+
## [1.20.0] - 2026-08-23
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
|
|
179
|
+
- **Ollama Cloud's live catalog is discovered, so `kimi-k3` (and every new Cloud model) appears in `/model` without editing `models.json`.** The picker used to show a frozen snapshot of legacy `:cloud` tags — `glm-5.2:cloud`, `kimi-k2.6:cloud`, etc. — because nothing ever asked Ollama Cloud what it actually serves. `/multi-account` now fetches `https://ollama.com/v1/models` at startup and on each idle sweep, merges the canonical bare ids (`kimi-k3`, `glm-5.2`, `kimi-k2.7-code`…) with the built-in and configured lists, and re-registers the base `ollama` provider and every cloned slot with the full set. Ollama Cloud accepts both the bare and the `:cloud`-suffixed form, so existing configured ids keep working; the suffix is display-only now. A failed fetch leaves the previous list intact, and `autoDiscoverModels: false` turns it off.
|
|
180
|
+
- **Kimi For Coding accounts rotate like every other family.** `add kimi` was accepted by the argument parser and then dead-ended: it told the user to hand-write an `api_key` entry, and no code path ever registered a Kimi slot as a provider — so the slot never appeared in `/login`, and a second Kimi subscription was unreachable no matter what was put in `auth.json`. Kimi has had a device-code OAuth flow in pi-ai all along. Numbered Kimi slots are now registered providers with that flow, carry Kimi's own catalog, and `add kimi` points at `/login` like Anthropic and Codex do.
|
|
181
|
+
- **Cursor slots register even when two callers race the provider load.** The optional Cursor provider was imported by discovery and by `session_start` at the same time, and the second caller reached the module while it was still initializing — its hoisted functions were callable, its state was not — which killed every Cursor account for the session with `Cannot access 'tokenResolver' before initialization`. The in-flight load is now shared, so the module is imported exactly once however many callers race.
|
|
182
|
+
- **A Cursor account's real catalog is discovered at startup, not only on login.** Slots were registered with the bundled fallback list and only a login or token refresh replaced it with the account's actual catalog — so after every restart the picker showed the stale short list until the next refresh. Startup now reads the catalog from the first slot whose stored token answers and re-registers every slot with it, logging the outcome to the debug log.
|
|
183
|
+
- **`/multi-account only-active [on|off]` — narrow `/model` to the account you are actually on.** With a dozen rotation accounts the picker lists every model of every one of them, and the interesting list is almost always "what can the account I'm on right now do". With the flag on, every other provider is re-registered with an empty model list — Pi merges re-registrations, so its auth and OAuth configuration survive — and restored when the flag goes off. The filter follows every switch: the failover target's models are restored before the switch and the account just left is hidden after it, and catalog re-discoveries are re-narrowed at the start of each turn.
|
|
184
|
+
- **Rotation slots now live in Pi's own static registry, so extension-free processes resolve them.** A bare `pi -p` child (any tool that spawns one — memory review, consolidation, external CLIs) used to die with "model not found" on `kimi-coding-account-2/k3` or `cursor/cursor-grok-4.6`, because those providers existed only in the extension's in-memory registration. At slot login/discovery the slot is now provisioned into Pi's native `models.json` (Kimi slots against the Kimi endpoint, Cursor slots against the running local proxy). Nothing is written on failover, rotation, or limit events — that state stays in this extension's own state file, and `settings.json` is never rewritten by this extension at all.
|
|
185
|
+
- **Providers outside the five specially-managed families now take part in rotation.** Membership required a provider to be recognised as `anthropic`, `openai-codex`, `qwen`, `ollama` or `cursor` by name; everything else was dropped by a single `continue`, silently, with no mention anywhere in `status`. On a real machine that meant six of fourteen logged-in accounts — `openrouter`, `openai`, `zai`, `opencode-go-api`, `kimi-coding`, `minimax`, some 405 models — sat unused while the managed accounts burned out one by one. Any account with a usable key that Pi already knows how to call now joins, without being named in this file, so a provider added tomorrow works with no release. They sort last, after every managed family: a managed account has quota telemetry, OAuth refresh and a live catalogue, while an unmanaged one is a blind spend, so it is the account of last resort rather than a peer. They get rotation membership, failover, cooldowns and direct `switch`; they do not get quota display or OAuth refresh, and `status` says so. `includeOtherProviders: false` turns the behaviour off for anyone who does not want background failover spending a per-token key.
|
|
186
|
+
- **`/multi-account status` shows how to reach a specific account.** The direct switch existed all along, buried mid-way through a single pipe-separated line of eighteen commands, so pressing `next` until the wanted account came round was the only discoverable route. Switching now has its own line with a real account name filled in.
|
|
187
|
+
- **`next` and `switch` say where they landed and what is believed about it.** Landing silently on a cooled account and being silently moved off it a second later produced two switch notices and a session on an account the user never chose; it now reads as one sentence that names the account and admits it is believed spent.
|
|
188
|
+
- **A recovery horizon stated in prose is parsed.** Codex free-plan refusals state the most direct fact available about when an account returns — `Try again in ~41615 min` — in the message text rather than in a header or JSON field, and it was being discarded in favour of a quota percentage that cannot see that limit. Structured fields still win; the prose horizon is used only when the provider gave nothing machine-readable, and it is capped by the existing recheck ceiling like every other forecast, so a month-long reset still cannot park an account for more than six hours.
|
|
189
|
+
|
|
190
|
+
### Fixed
|
|
191
|
+
|
|
192
|
+
- **A forced OAuth refresh no longer destroys the account it was meant to save.** Anthropic rotates the refresh token on every refresh call and revokes the old one immediately, so a refresh that cannot be *persisted* does not fail — it burns the credential and throws the replacement away. On pi 0.84.x `AuthStorage` dropped the `set()` method this extension persisted with, so the post-refresh guard tripped every single time: `invalid_grant`, "Refresh token not found or invalid", and a manual `/login anthropic` roughly once a day. Persistence now goes through pi's supported locked `modify()`, falls back to `set()` on older hosts and to writing `auth.json` directly, and — crucially — the ability to store the result is checked *before* the network refresh, so a host that cannot persist never rotates a token it would lose. Reported in #22.
|
|
193
|
+
- **A forced Cursor refresh actually refreshes.** It still imported `~/.pi/agent/git/github.com/ndraiman/pi-cursor-provider/auth.ts` — a path that stopped existing for everyone when the Cursor provider was vendored into this extension — so every forced Cursor refresh threw before it could refresh anything. It now loads the vendored provider. Follow-up to #20.
|
|
194
|
+
- **`CLAUDE_CODE_VERSION` bumped to `2.1.241`** (Anthropic OAuth billing header). Reported in #21.
|
|
195
|
+
- **`only-active` no longer restores a stale `/model` list after a catalog sync.** Live discovery (Ollama Cloud, Codex, Cursor) updated the live registration, then a manual switch before the next message restored the hidden copy taken *before* the sync — so `/model` still showed the old six (`glm-5.2:cloud` …) even though `kimi-k3` had already been fetched. The filter is re-applied in the same tick as each catalog sync, so the stored copy is the fresh one.
|
|
196
|
+
- **Cursor `resource_exhausted` is treated as a quota limit, so failover moves off the spent account.** gRPC-fronted OpenAI-compatible backends (Cursor among them) surface a per-account quota wall as `resource_exhausted`, not a `429`. Unrecognised, the error was unclassified → no failover → every turn died on the dead account with `Provider finish_reason: error`. It now cools the account down and rotation moves on like any other limit.
|
|
197
|
+
- **Cursor tool-call turns no longer report `usage: 0`.** The proxy omitted usage on the tool-call pause and, when Cursor skipped `tokenDetails`, computed prompt tokens as zero. Pi then thought the session was tiny, skipped auto-compact, and `/compact` could say "session too small" while the footer showed 115% of a 200k window. Tool-call responses now include usage, and prompt size falls back to an estimate from the request.
|
|
198
|
+
- **Anthropic's third-party extra-usage 400 is treated as a quota limit.** `"Third-party apps now draw from your extra usage, not your plan limits"` used to look like a request bug, so consolidation/review child processes retried it forever. It now fails over like any other exhausted account.
|
|
199
|
+
- **A Codex usage-limit no longer jumps to Claude while another Codex account has not been asked.** Every Codex slot sitting at 100% was dropped from the candidate list, so plus-plan `gpt-5.6-sol` failed over to Opus. Automatic failover now tries a same-family sibling first — the exact model if that account has it, otherwise its flagship — at the session thinking level. A 100% forecast is not a refusal; only an actual limit error this session skips that sibling. The hop is pinned through both preflights of the continuation so the next check cannot bounce it to another family before it is tried.
|
|
200
|
+
- **Compaction no longer leaves "Compacting context…" spinning forever.** A spent Codex account was rerouted to Claude, the summary timed out, and the job was handed to Pi's default on the *spent* account with no timeout — spinner forever. A timed-out attempt is now aborted (not leaked) and the next live account is tried, including when the *current* account itself wedges. If none can finish, compaction is **cancelled**. The spinner stops. Pi's untimed default is never given a spent account, and a healthy current account stays on Pi's native compaction path.
|
|
201
|
+
- **Provisioning a rotation slot no longer invalidates `models.json`.** Slot catalogs were written as string ids (`"k3"`, `"cursor-grok-4.6"`), but Pi's schema requires each model to be an object (`{"id":"k3",...}`). The host then rejected the *entire* file, so every custom provider disappeared. Slots are now written as model objects.
|
|
202
|
+
- **Failover tries a sibling account with the same model first.** Exhausting one Kimi slot used to jump to Claude Opus (or Cursor's alphabetically-first catalog id, `claude-4-sonnet`) because confirmation and `preferLatestModel` ranked across families. Automatic failover now tries another account of the same family that still has the exact model — including effort-folded Cursor ids such as `cursor-grok-4.6-high` ≈ `cursor-grok-4.6` — and restores the session thinking level. Only when no such sibling is free does it move to another family. `/multi-account best` is unchanged: confirmation still outranks an unmeasurable guess. Same-family `preferLatestModel` upgrades (gpt-5.4 → gpt-5.5 on a healthy Codex sibling) still happen.
|
|
203
|
+
- **Restart restores the thinking level, not just the model.** Pi's `createAgentSession` clamps the saved level to the *fallback* model's caps while extensions are still registering, so a session that ran `max` on Grok came back at the wrong level even when the model itself restored. The last live level is now persisted alongside `lastUserModel` and re-asserted after the model is back — and when Pi already restored the right model, the level is still re-applied because the clamp still happened.
|
|
204
|
+
- **A cancelled compaction no longer strands failover-queued messages.** Pi runs its default compaction on the active account with no request timeout, and a stalled provider call left the session "Working…" forever; messages typed meanwhile sat in the multi-account cooldown queue which nothing re-visited. The extension now cancels the default compaction when it is holding queued user input, and flushes that queue on `compaction_end` instead of leaving it to die.
|
|
205
|
+
- **Cursor model names no longer bake thinking effort into the picker.** The catalog lists `Grok 4.6 Medium` / `Grok 4.6 High` as separate ids; after folding them into one model the representative kept the Medium label, so the powerline read "Cursor Grok 4.6 Medium" even while `/thinking` was `high`. The folded name is now `Grok 4.6`, and the model advertises `thinkingLevelMap` so Pi's session thinking level (settings / `/thinking`) is what selects the effort, same as every other provider.
|
|
206
|
+
- **A Pi restart no longer dumps the session onto `anthropic/claude-opus-4-8` or `kimi-coding/k3`.** The earlier "restore after catalog load" fix was too late: Pi's `createAgentSession` calls `getModel(cursor, cursor-grok-4.6)` *before* `session_start`, the baked-in Cursor fallback list did not contain Grok 4.6, and startup preflight then treated Pi's accidental kimi/anthropic pick as the user's choice and failed over away from it. Three changes close the hole: `cursor-grok-4.6` is in the bundled fallback so `getModel` succeeds; the factory returns the Cursor setup promise so Pi waits for registration; if Pi still parks on the wrong model, restore runs *before* startup preflight, and a state-version migration keeps `lastUserModel`.
|
|
207
|
+
- **Cursor subscription support is part of this extension.** It is no longer loaded from a separate clone. OAuth, the local proxy, and catalog discovery live under `cursor/` inside pi-multi-account. `/next` into Cursor prefers the last chosen model in that family instead of the bundled `composer-2.5` fallback.
|
|
208
|
+
- **A Cursor provider fixed mid-session is picked up without restarting Pi.** A module that throws while loading stays cached as failed under its own URL, so every later discovery pass replayed the first error — cloning or repairing the provider only took effect on the next launch. Retries now load a fresh instance, and only after the previous one proved unusable.
|
|
209
|
+
- **`switch` accepts the name a person would type.** The slot ids are internal — `kimi-coding`, `openai-codex-account-6` — and `switch` demanded one exactly, so `switch kimi` answered `unknown provider "kimi"` and left `next`, walking through every spent account in turn, as the only way to reach it. An exact id still wins; a short name resolves when it is unambiguous, and an ambiguous one lists the candidates instead of guessing, because guessing between two Codex slots would silently spend the wrong account's quota.
|
|
210
|
+
- **`/multi-account best` — one command that lands on an account which can work now.** Reaching a working account meant pressing `next` repeatedly, landing on and being bounced off each spent account on the way, or typing an exact slot name into `switch`. With a dozen accounts, most of them spent, neither is a usable answer to "just put me somewhere that works". `best` picks the top-ranked account that is available right now and switches once; when nothing is available it says so and states when the first one returns, rather than doing nothing.
|
|
211
|
+
- **The footer identifies the account, its plan, and whether there is anywhere to go.** It read `Codex A5 | 5h 12% left/3h` — a slot number, which says nothing about whose quota is burning when seven Codex slots are logged in. It now reads `Codex A5 · alice | free | 5h 12% left/3h | +2 ready`: the real account (from the email the provider already reports), the plan the percentage is a percentage of, and how many other accounts could take over. The last part answers the question that actually follows "this one is nearly out".
|
|
212
|
+
- **A quota window is labelled by its real length, and the account's own verdict is shown.** The label was positional — whatever sat in the "primary" slot was called `5h` — but a Codex free plan meters a **thirty-day** window there, so a number resetting next month read as one resetting this afternoon. Worse, an account answering `allowed: true` was displayed as `0% left`, because only the percentage was shown: a working account that looked dead. The footer now reads e.g. `Codex A6 · bob | free | ok | 30d 0% left/28d1h | +2 ready`, and `limits` states the verdict in words.
|
|
213
|
+
- **Kimi For Coding is reported honestly instead of erroring.** Promoting `kimi-coding` to a managed family left the usage layer unaware of it, so every probe fell through to the OAuth branch and threw `has no OAuth access token` for a healthy API key — blanking the footer and filling the log with failures for an account that was working. Kimi publishes no quota endpoint at all (`/usage`, `/quota`, `/me`, `/subscription` and the Moonshot balance path all 404 against `api.kimi.com/coding`), so it reports its plan and says the quota is not exposed.
|
|
214
|
+
- **`best` no longer prefers an unmeasurable account over a confirmed one.** It promised "an account that can work right now" and landed on an out-of-quota Kimi slot, because ranking treated "the provider answered `allowed: true`" and "we know nothing about this account" as equivalent — both merely lack a cooldown. Confirmation is evidence; absence of evidence is not. A confirmed account now outranks an unknown one, and when nothing confirms availability the switch says plainly that it is an unverified guess rather than presenting it as a considered choice.
|
|
215
|
+
- **An account with no usage endpoint is no longer re-tried every ten minutes.** The recheck ceiling rests on "asking again is nearly free", which holds for accounts we can poll in the background — Codex, Anthropic, Ollama, Cursor. Kimi publishes no such endpoint (every documented path 404s) and Qwen none at all, so their only "probe" is a real request: the user's message lands on the spent account, is refused, and is bounced onward. Doing that every ten minutes to an account that has just said its quota returns *in the next billing cycle* is the exact thrashing the ceiling exists to prevent. The ceiling now applies where re-probing is cheap, and the recorded cooldown stands where it is not.
|
|
216
|
+
- **A managed family missing from a saved `providerOrder` no longer drops out of the rotation.** `/multi-account` writes the whole config to disk, `providerOrder` included, so every installed config pins the family list as it stood that day. When a provider is promoted to a managed family in a later release — as `kimi-coding` just was — an existing config lists neither it (the order predates it) nor admits it as an "other" provider (it is managed now), and a working account silently vanishes from the ring. `rediscover` could not bring it back, because from discovery's point of view nothing was broken. The saved order is a preference about sequence, not a whitelist: families the user never ranked are appended after the ones they did.
|
|
217
|
+
- **Losing a race for an OAuth refresh is no longer reported as a dead account.** Anthropic rotates the refresh token on every use and kills the old one immediately, so any second holder of that credential — another Pi window, a usage probe that read the file a second earlier — presents a token that was valid when read and dead on arrival. The server answers `invalid_grant`, indistinguishable from a genuine revocation, and the slot was dropped with a demand to re-login that fixed nothing: the working token was already on disk, written by whoever won the race. That case is now retried once with the stored token. If disk holds the same token that just failed there is nothing new to send and it fails immediately, and any other error (a timeout, a 5xx) is never retried, since burning the disk token on a network blip would turn an outage into a lost account.
|
|
218
|
+
- **A revoked Claude login now says what revoked it.** "Authorization is invalid, run /login" sends people round a loop they have often already been round: log in, work for a few hours, get kicked out, log in again. The cause is not in the error text and cannot be guessed from it — every CLI signing into Claude Pro/Max uses the same client id, and Anthropic keeps one live refresh token per account for that client, so signing the same account into another tool, another machine, or a second slot here revokes this one hours later. The message now names that, because it is the only thing that ends the loop.
|
|
219
|
+
- **The provider's own "you can use this account right now" answer is finally read.** The Codex usage response states the verdict outright — `rate_limit.allowed` / `rate_limit.limit_reached` — and that field was parsed away entirely, leaving every availability decision to arithmetic on `used_percent`. On a real machine two accounts had recovered and were answering `allowed: true, limit_reached: false` while their monthly meter still read 98%; the extension went on skipping both for hours, because a percentage near the cap plus a bench recorded from an earlier refusal is all it ever consulted. It looked exactly like an extension that cannot see accounts that freed up — which was, functionally, what it was. The verdict is now carried on the snapshot and outranks every derived number in both directions: `usable` retires the bench *and* the meter-distrust flag recorded when the account refused (that distrust was about the meter, and the account has now spoken for itself), while `blocked` keeps an account out of rotation even when its window still shows headroom. Only honoured while the snapshot is fresh, since a verdict describes the moment it was taken; a response that states no verdict falls back to the forecast as before.
|
|
220
|
+
- **A refusal that cannot be classified no longer strands the session forever.** An account out of credits refuses with a 402 whose wording — `Prompt tokens limit exceeded: 38075 > 16958 … upgrade to a paid account` — matched nothing in the error vocabulary, so it classified as `unhandled`: no cooldown, no failover, no message. Every subsequent user message produced the identical refusal, indefinitely, while live accounts sat unused a few positions down the rotation. Payment/credit exhaustion is now recognised (matched on wording, never on the bare `402`, which also occurs inside token counts), and an unmanaged account that hits a quota or authorization refusal is benched at the account level for the normal cooldown rather than for one minute at the model level — a minute was short enough for the very next switch to land back on it and close the loop.
|
|
221
|
+
- **The error vocabulary is merged with the built-in one instead of replaced by it.** `/multi-account` writes the whole config to disk, defaults included, so nearly every installed config froze a snapshot of the vocabulary as it stood that day. Under replace-semantics a newly recognised refusal reached fresh installs only and was invisible on every machine that had ever run the command — that is, on the machines actually in use. User-added patterns still apply; they simply no longer silently exclude terms added later.
|
|
222
|
+
- **A manual choice survives the whole message, not just the first question about it.** Pi runs its readiness preflight twice for one user message (`input`, then `before_agent_start`). The one-attempt reprieve granted by `next` / `switch` was spent by the first, so the second found none and moved the user off the account they had just picked — surfacing as `last-moment preflight: selected account unavailable` one moment before the account would have been tried. The reprieve now covers the attempt and retires on the following message, so it cannot outlive its message even on a host that never reaches `before_agent_start`.
|
|
223
|
+
- **"No usable authenticated account exists" is only said when that is true.** Any empty selection produced that sentence, including the common case where several accounts were logged in with quota to spare and had simply lost their authorization. Being told there are no accounts, while `status` lists two with quota, reads as the extension failing to see them and buries the one action that fixes it. The message now names the accounts and states which need re-login and which have no model configured.
|
|
224
|
+
- **A quota forecast is no longer announced as a verdict.** `switched to … — believed spent (cooling down, ~672h left)` quoted the raw forecast while the extension's own rule re-probes any account within `maxRecheckIntervalMs` regardless. Reading that an account is locked for four weeks, when it is really re-asked within hours, is a large part of why a user concludes that freed-up accounts are never picked up again. The notice now states the forecast and the guarantee that bounds it.
|
|
225
|
+
- **A spent account is no longer re-selected seconds after it refused.** With seven Codex accounts — six reading 100% and one reading 98% — the 98% account was picked, greeted the user, then refused the first real request with `You have hit your ChatGPT usage limit (free plan). Try again in ~41615 min.` It was benched, but the next usage reconciliation saw 98% (below the cap), concluded the account was free, wiped the cooldown, and rotation walked straight back onto it. The loop only broke on the *second* refusal, and every new session reset that counter — so it recurred all day. A stated recovery horizon that contradicts a usage reading claiming headroom now distrusts that reading on the **first** refusal, because the refusal was measured while the percentage is a forecast about a quota window that cannot see a session or plan limit. Deliberately narrow: a bare throttle with no stated horizon (`429 rate limit`) still defers to the meter, so an account whose short window genuinely freed is not benched. A real success restores trust.
|
|
226
|
+
- **The distrust survives a restart.** The proof that an account's usage reading does not reflect its real limit lived only in memory, so every new Pi session started believing the meter again and re-selected the spent account once per session. It is now persisted alongside the recorded cooldowns and cleared by a successful response.
|
|
227
|
+
- **A manually chosen account is no longer overridden before it gets used.** `/multi-account next` and `/multi-account switch` deliberately ignore the cooldown bookkeeping, because that bookkeeping is a forecast and actually asking the account is the only way to prove it stale. But the preflight running on the user's next message re-applied the same forecast and moved them off — so the override held only until it was used, which is the one moment it had to hold. A manual choice now survives one attempt; normal routing resumes after that, so nobody can strand themselves on a genuinely dead account.
|
|
228
|
+
- **Registering the Ollama/Qwen base provider no longer narrows the user's own model list.** That registration exists because a placeholder `apiKey` in `models.json` can stop Pi exposing the provider at all — but it called `registerProvider` with only the one built-in tag, replacing a `models.json` that configured six. A user running six Ollama cloud models could reach exactly the one written into this extension. Configured models are now carried alongside the built-in list, which still leads so the known flagship stays the account's representative. Cloned `-account-N` slots inherit the same set, and are re-registered once the host registry becomes reachable instead of keeping the single tag they were created with. The `qwen` arm of that registration also read `OLLAMA_BASE` as its base id, so its "this is the base provider" guard could never fire.
|
|
229
|
+
|
|
230
|
+
## [1.17.0] - 2026-08-16
|
|
231
|
+
|
|
232
|
+
### Added
|
|
233
|
+
|
|
234
|
+
- **`max` thinking is available on models that advertise it.** Pi understands `max` — it is in `ThinkingLevel` and `--thinking max` works — but the extension's known-levels list stopped at `xhigh`, so the strongest level of a model offering it was filtered out. GPT-5.6 Sol/Terra/Luna all advertise `max` in the live Codex catalog. Reported by @devtm1123 in #15.
|
|
235
|
+
- **`reasoningLevel: "max"` is accepted in config.** Adding `max` to the catalog alone was not enough: the config parser enumerated the accepted levels and stopped at `xhigh`, so an explicit `max` fell through to `"auto"` — never forced, with nothing said about why. `max` also joins the weakest→strongest ordering, so guarantee #22 (a weaker fallback model's clamp is restored, never adopted) covers it like every other level.
|
|
236
|
+
|
|
237
|
+
### Notes
|
|
238
|
+
|
|
239
|
+
- The known-levels list is a **filter, not a grant**: a model's own advertised efforts are intersected with it, so a level named there only ever reaches a model that asked for it. Provider gradations differ wildly and do not nest — Claude Opus 4.6 advertises `max` alone, glm-5.2 has `max` but no `xhigh`, GPT-5.6 has `max`/`xhigh`/`minimal` but no `medium` — which is why the per-model intersection, not the list, decides what any given model gets. Locked by test.
|
|
240
|
+
- The fallback definition for an **unknown** Codex model is deliberately left at `xhigh`. That path is a guess about a model we have no catalog for, and guessing `max` would hand a level to a model that never claimed it.
|
|
241
|
+
|
|
242
|
+
### Tests
|
|
243
|
+
|
|
244
|
+
- Guarantee #27: a model that advertises `max` gets it, one that does not is left untouched (including not acquiring a `medium` it never claimed), `reasoningLevel: "max"` is honoured, and `max` is restored after a switch through a weaker model. Verified red→green.
|
|
245
|
+
|
|
246
|
+
## [1.16.0] - 2026-08-16
|
|
247
|
+
|
|
248
|
+
### Changed
|
|
249
|
+
|
|
250
|
+
- **Availability is now verified, not predicted.** Every number a provider gives us about the future is a forecast: reset timestamps move when a quota window is refreshed early and unannounced, and a used-percentage is a fraction whose denominator the provider can resize at will. Treating those as ground truth meant a single reading of "used 100%, resets in 29 days" could bench an account for weeks — the work simply waited while the account may have been live within the hour. A forecast may now only *order* the queue; it can no longer stop us from asking. `maxRecheckIntervalMs` (default 10 minutes) caps how long any prediction — a recorded cooldown, a usage window, a reset timestamp — may keep an account from being tried again. A refused request costs no tokens, so re-asking is close to free, and only the account's own answer proves anything.
|
|
251
|
+
- **Ranking keeps the old protection.** An account nothing predicts as spent is always tried before one that is only back in the pool because its forecast went stale, and among equals the account that refused longest ago wins. This is what stops the ceiling from turning into a loop between two spent accounts: without it, rotation order alone sent us straight back to the account that had just answered "usage limit reached" while a never-tried account sat further down the ring.
|
|
252
|
+
|
|
253
|
+
### Fixed
|
|
254
|
+
|
|
255
|
+
- **Failover no longer bounces back onto an account that refused moments ago.** The existing 60-second anti-ping-pong guard only remembers the single account we left last, so with three or more accounts the rotation could return to a spent one minutes later and loop.
|
|
256
|
+
|
|
257
|
+
### Tests
|
|
258
|
+
|
|
259
|
+
- Guarantee #26, with coverage for a month-long forecast no longer parking an account past the ceiling, and for an untried account outranking a freshly-refused one once the ceiling elapses. Verified red→green in isolation: disabling the ceiling fails the first, disabling the ranking fails the second **and** the pre-existing stale-snapshot guarantee — the two halves are load-bearing together.
|
|
260
|
+
|
|
261
|
+
## [1.15.1] - 2026-08-16
|
|
262
|
+
|
|
263
|
+
### Fixed
|
|
264
|
+
|
|
265
|
+
- **A same-account resume now auto-continues on every host without `pi.continueAgent`.** `currentPromptSwitch` is set only when accounts actually rotate, but the pending-resume path deliberately returns to the SAME account — a transient overload, or a cooldown that expired where it started — so it never has a switch record. The injection fallback required one, so on every build since pi-coding-agent 0.80.3 (where seamless resume was removed) that combination silently refused to continue and the session stalled until the user re-sent the prompt by hand. The resume context is now passed explicitly.
|
|
266
|
+
- **The stall no longer blames the Pi build.** The warning claimed "switched account, but this Pi build cannot auto-resume" even when no switch had happened and the real cause was elsewhere — a spent auto-continue budget, `autoContinue: false`, or a failed dispatch. A missing `pi.continueAgent` is only why the fallback path is taken, never why the fallback itself declined. The message now states what it knows and points at the recorded reason.
|
|
267
|
+
- **Every refused or failed continuation is recorded.** Refusals were entirely silent and the dispatch `catch` swallowed its error, so a session that stopped continuing by itself left nothing in the debug log to explain why. `continuation_injection_blocked` now names the specific reason and `continuation_injection_failed` carries the error.
|
|
268
|
+
- **A rejected dispatch can no longer be reported as success.** `pi.sendUserMessage` is async on the host, so a rejected promise escaped the synchronous `try`/`catch` as an unhandled rejection while the injection still returned `true`. The promise is now attached.
|
|
269
|
+
|
|
270
|
+
### Tests
|
|
271
|
+
|
|
272
|
+
- Live-harness coverage for a transient overload resuming on a host without `pi.continueAgent` (asserting the continuation is injected as `followUp`, not stalled), and for a blocked continuation naming its real reason. Verified red→green: both fail against the previous behaviour.
|
|
273
|
+
|
|
274
|
+
## [1.15.0] - 2026-08-16
|
|
275
|
+
|
|
276
|
+
### Added
|
|
277
|
+
|
|
278
|
+
- **The interrupted turn now survives the switch.** On a quota failover the turn that triggered the switch is exactly the turn pi-ai refuses to replay: `transform-messages` skips every assistant message with stopReason `error`/`aborted`, and degrades thinking blocks to plain text whenever the next request runs on a different model. The account taking over was therefore told "do not repeat completed work" with the record of that work already deleted, and its tool results left with no originating call. A new `context` hook rewrites each interrupted turn into a verbatim `user` handoff record — the one role `transform-messages` passes through unchanged on every provider — folding in the tool results that belonged to it and flagging every call that never returned. Rendering is deterministic (no timestamps, no rng) so replaying the hook on each request never moves the prompt-cache breakpoint, and every section is hard-capped so rescuing context cannot blow the context window of the account just switched to. Opt out with `preserveInterruptedContext: false`.
|
|
279
|
+
- **`continuationPrompt` no longer claims context that was deleted.** The default prompt told the next account the full conversation was still in the session while the interrupted turn had just been dropped. It now points at the `[handoff:interrupted-turn]` record and asks the model to verify state before redoing work.
|
|
280
|
+
|
|
281
|
+
### Fixed
|
|
282
|
+
|
|
283
|
+
- **`VERSION` in `index.ts` matched `package.json` again.** It had been left at `1.14.3` through the 1.14.4 and 1.14.5 releases, so `host_capabilities` debug entries and the startup notices reported a stale version.
|
|
284
|
+
|
|
285
|
+
### Tests
|
|
286
|
+
|
|
287
|
+
- Added `test/interrupted-context.test.ts`, including an integration assertion that runs the REAL pi-ai `transformMessages` over an interrupted transcript: it asserts the loss first (baseline) and then that reasoning, output, tool calls and folded results all survive a cross-provider switch. Added live-harness coverage in `test/failover.test.ts` for the full path (limit error → account switch → context handoff) and for the `preserveInterruptedContext: false` opt-out.
|
|
288
|
+
|
|
289
|
+
## [1.14.5] - 2026-08-15
|
|
290
|
+
|
|
291
|
+
### Fixed
|
|
292
|
+
|
|
293
|
+
- **Saved scoped models for numbered Codex accounts now survive restart.** Persisted, credential-free model catalogs seed each numbered alias synchronously before Pi resolves saved model scopes. Empty caches and disabled discovery retain the static/host fallback. Contributed by @carlosorch in PR #8; fixes #7.
|
|
294
|
+
- **Anthropic and Codex OAuth refresh always receive an `AbortSignal`.** Both legacy and provider-factory pi-ai bridges now forward the host signal, with a bounded fallback for internal refreshes, preventing `AbortSignal.any()` from rejecting `undefined`. Fixes #9.
|
|
295
|
+
|
|
296
|
+
### Tests
|
|
297
|
+
|
|
298
|
+
- Added startup-catalog regression coverage and made both legacy and modern OAuth bridge fixtures reject refresh calls that omit an `AbortSignal`.
|
|
299
|
+
|
|
300
|
+
## [1.14.4] - 2026-08-15
|
|
301
|
+
|
|
302
|
+
### Security
|
|
303
|
+
|
|
304
|
+
- Updated the Pi development/runtime dependency graph and pinned transitive `protobufjs` to a patched release; production `npm audit` now reports zero vulnerabilities.
|
|
305
|
+
|
|
306
|
+
### Changed
|
|
307
|
+
|
|
308
|
+
- Added a release gate that runs type checks, the hermetic test suite, and a package allowlist/secret-marker check before publication.
|
|
309
|
+
|
|
310
|
+
## [1.14.3] - 2026-08-02
|
|
311
|
+
|
|
312
|
+
### Fixed
|
|
313
|
+
|
|
314
|
+
- **Anthropic OAuth requests carry an up-to-date Claude Code version again.** `CLAUDE_CODE_VERSION`,
|
|
315
|
+
which is baked into the `x-anthropic-billing-header` on every OAuth-marked Anthropic request, had
|
|
316
|
+
been stuck at `2.1.172` while Claude Code shipped `2.1.220` — 48 releases of drift on a value
|
|
317
|
+
Anthropic reads to accept and count those requests. It is now `2.1.220`. This is the only change
|
|
318
|
+
that reaches the published package; everything below is repository plumbing that keeps it from
|
|
319
|
+
happening again.
|
|
320
|
+
|
|
321
|
+
- **The weekly version check no longer fails silently.** The workflow that exists to prevent exactly
|
|
322
|
+
this drift had failed every Monday since mid-June: it pushed a `chore/claude-code-version-*`
|
|
323
|
+
branch and then died on `gh pr create`, because this repository does not permit GitHub Actions to
|
|
324
|
+
open pull requests. No PR ever appeared, so nobody noticed — three orphan branches accumulated
|
|
325
|
+
instead, and the constant kept drifting.
|
|
326
|
+
|
|
327
|
+
It no longer asks for a permission it does not have. It type-checks and tests the bump itself,
|
|
328
|
+
pushes it straight to `main`, opens an **issue** if that push is ever refused, and sweeps up any
|
|
329
|
+
leftover `chore/claude-code-version-*` branch on the way. A renamed or reformatted constant now
|
|
330
|
+
fails CI instead of quietly turning the weekly job into a no-op (guarantee #23).
|
|
331
|
+
|
|
332
|
+
- **An automated bump can no longer arrive red.** The billing-header test asserted the literal
|
|
333
|
+
`2.1.172`, so the very bump this automation exists to make would have broken CI on arrival. It
|
|
334
|
+
reads the constant from `index.ts` now.
|
|
335
|
+
|
|
336
|
+
## [1.14.2] - 2026-07-30
|
|
337
|
+
|
|
338
|
+
### Fixed
|
|
339
|
+
|
|
340
|
+
- **A per-agent thinking level is no longer clobbered.** `captureDesiredThinking()` ran on every
|
|
341
|
+
`agent_start` and applied the *global* `config.reasoningLevel` (which defaulted to `high` and,
|
|
342
|
+
because the parser fell back to `high` for anything unset or invalid, could not be turned off).
|
|
343
|
+
A delegated agent configured `--thinking low` was therefore flipped to `high` on its very first
|
|
344
|
+
turn — the session recorded `thinking_level_change: low -> high` before the first user message —
|
|
345
|
+
and, because Pi's `setThinkingLevel()` also persists to settings, the override leaked into the
|
|
346
|
+
default level too. Reported and diagnosed in
|
|
347
|
+
[#6](https://github.com/Sarrius/pi-multi-account/pull/6) (thanks @fwhskr, confirmed by
|
|
348
|
+
@julius-retzer).
|
|
349
|
+
|
|
350
|
+
The intent is now read from the session itself (`pi.getThinkingLevel()`), so your Pi default,
|
|
351
|
+
`/thinking`, and per-agent `--thinking` all win. The original protection is kept and made
|
|
352
|
+
sharper: a level the *host* clamped down to (because a weaker fallback model caps out lower) is
|
|
353
|
+
recorded as a clamp, never adopted as intent, so it is restored the moment a capable model is
|
|
354
|
+
back — a naive "just read the session level" fix would let a single failover ratchet thinking
|
|
355
|
+
down for the rest of the session. An explicit `/thinking` change between turns is still honoured.
|
|
356
|
+
|
|
357
|
+
### Changed
|
|
358
|
+
|
|
359
|
+
- **`reasoningLevel` now defaults to `"auto"`** — follow the session, only restore after switches.
|
|
360
|
+
Setting an explicit level (`"off"`…`"xhigh"`) keeps the old behaviour and *forces* that level on
|
|
361
|
+
every turn, for anyone who wants a hard floor regardless of the session.
|
|
362
|
+
- New black-box log kinds `thinking_intent` and `thinking_clamped` make level changes traceable in
|
|
363
|
+
`/multi-account log`.
|
|
364
|
+
|
|
365
|
+
## [1.14.1] - 2026-07-27
|
|
366
|
+
|
|
367
|
+
### Fixed
|
|
368
|
+
|
|
369
|
+
- **A Cursor provider that fails to load can no longer damage the session.** Cursor lives in a
|
|
370
|
+
separate repo, on whatever Node the user runs; a clone that is incompatible with the running
|
|
371
|
+
Node (for example a JSON import newer Node rejects) threw during setup. That rejection escaped
|
|
372
|
+
the fire-and-forget discovery call as an **unhandled rejection** — which Node can turn into a
|
|
373
|
+
process exit — and aborted `session_start` partway, skipping the reset that clears a previous
|
|
374
|
+
session's pending auto-resume. A stale resume surviving into a new session means silently
|
|
375
|
+
restarting work the user never asked to restart. The failure is now contained, logged, and
|
|
376
|
+
reported once; everything else continues.
|
|
377
|
+
|
|
378
|
+
- **A newly released Claude flagship no longer needs a release of this extension.** The Anthropic
|
|
379
|
+
model list was hard-coded, so when `claude-opus-5` shipped in Pi's registry the extension still
|
|
380
|
+
ranked `claude-opus-4-8` highest and failover stayed on the older model — silently breaking the
|
|
381
|
+
project's hard rule of always using a provider's top model. Claude models known to the host are
|
|
382
|
+
now merged and ranked (tier first, then generation) exactly as Codex models already were, and
|
|
383
|
+
re-registered onto numbered account aliases so they are selectable there too. `claude-opus-5`
|
|
384
|
+
added to the built-in ordering as well.
|
|
385
|
+
|
|
386
|
+
### Changed
|
|
387
|
+
|
|
388
|
+
- The invented `gpt-5.6` / `gpt-5.6-mini` entries added in 1.14.0 were removed: OpenAI's real 5.6
|
|
389
|
+
family ships as `gpt-5.6-sol` / `-terra` / `-luna`, and those come from the host registry and the
|
|
390
|
+
live per-account catalog with correct metadata. Guessed ids risk offering a model a plan cannot
|
|
391
|
+
serve and re-introduce the release-per-generation treadmill the 1.14.0 fix removed.
|
|
392
|
+
|
|
393
|
+
## [1.14.0] - 2026-07-27
|
|
394
|
+
|
|
395
|
+
### Fixed
|
|
396
|
+
|
|
397
|
+
- **The extension no longer fails to load** with `undefined is not an object (evaluating
|
|
398
|
+
'_oauth.openaiCodexOAuthProvider.usesCallbackServer')` (issue #3). Two independent causes, both
|
|
399
|
+
closed:
|
|
400
|
+
- `@earendil-works/pi-ai` was only ever probed inside the extension's *own* `node_modules`, so
|
|
401
|
+
every hoisted `npm install` / `pi install` layout — where pi-ai sits next to the package —
|
|
402
|
+
found nothing. Resolution now walks ancestor `node_modules` the way Node does and falls back
|
|
403
|
+
to `require.resolve`.
|
|
404
|
+
- pi-ai 0.80 **removed** the runtime OAuth surface: `dist/oauth.js` is now types-only, `getModel`
|
|
405
|
+
moved to `dist/compat.js`, and the implementations live behind provider factories with a new
|
|
406
|
+
`login(interaction)` / `refresh(credential)` API. Both eras are now normalized behind one
|
|
407
|
+
internal bridge, including an adapter from Pi's legacy OAuth callbacks to pi-ai's
|
|
408
|
+
`AuthInteraction` (this fixes `interaction.notify is not a function` during browser login).
|
|
409
|
+
Diagnosis and the 0.80+ approach contributed by **@lfoscari** (PR #4).
|
|
410
|
+
|
|
411
|
+
Loading is now non-fatal in every case: a pi-ai that cannot be adapted degrades to "subscription
|
|
412
|
+
login unavailable" with an actionable message at session start, and API-key accounts keep
|
|
413
|
+
rotating instead of the whole extension dying at startup.
|
|
414
|
+
|
|
415
|
+
- **A brand-new OpenAI generation no longer needs a release of this extension** (issue #2). The
|
|
416
|
+
built-in model list was consulted *before* the host model registry, so a Pi that already shipped
|
|
417
|
+
`gpt-5.6` still failed over to `gpt-5.5`. Models the host knows about are now merged and ranked
|
|
418
|
+
by version, and re-registered onto numbered account aliases so they are selectable there too.
|
|
419
|
+
The live per-account catalog still outranks everything. `gpt-5.6` / `gpt-5.6-mini` metadata added.
|
|
420
|
+
|
|
421
|
+
- **Cursor no longer appears in sessions that never asked for it** (issue #5). With
|
|
422
|
+
`includeCursor` on by default but the (separately cloned) Cursor provider absent, the extension
|
|
423
|
+
registered a phantom `cursor-account-2` login slot backed by nothing and printed a `git clone`
|
|
424
|
+
warning at every start. Cursor slots are now created only once the provider is actually on disk,
|
|
425
|
+
and the install instructions appear only on the explicit `/multi-account add cursor` path.
|
|
426
|
+
Cloning the provider is picked up on the next discovery pass — no restart needed.
|
|
427
|
+
|
|
428
|
+
### Added
|
|
429
|
+
|
|
430
|
+
- `claude-sonnet-4-6` to the default Anthropic model list — contributed by **@RuslanAsadov** (PR #1).
|
|
431
|
+
|
|
432
|
+
## [1.13.16] - 2026-07-15
|
|
433
|
+
|
|
434
|
+
### Added
|
|
435
|
+
|
|
436
|
+
- **New OpenAI Codex models are discovered automatically per account.** The extension now calls
|
|
437
|
+
OpenAI's authenticated `/backend-api/codex/models` catalog at session start and on explicit
|
|
438
|
+
reload/rediscovery, mirrors each account's selectable models onto its numbered Pi alias, and
|
|
439
|
+
follows the catalog's server-defined priority. The credential-free catalog is cached for five
|
|
440
|
+
minutes and persisted, so transient network failures do not erase a known model. Pi's own model
|
|
441
|
+
registry and the static list remain offline fallbacks. Manual `preferredModels` overrides still
|
|
442
|
+
win when the user deliberately pins an order.
|
|
443
|
+
- **High reasoning is now the default contract.** `reasoningLevel` defaults to `"high"`, is applied
|
|
444
|
+
at the start of every turn, and is restored after every account/model switch. Extreme levels
|
|
445
|
+
such as `xhigh` / Max / Ultra are never selected automatically; `xhigh` is available only through
|
|
446
|
+
an explicit config override. Hosts/models with smaller capability clamp safely.
|
|
447
|
+
|
|
448
|
+
## [1.13.15] - 2026-07-15
|
|
449
|
+
|
|
450
|
+
### Fixed
|
|
451
|
+
|
|
452
|
+
- **Plan upgrades, purchased credits, and early provider resets now revive benched accounts.**
|
|
453
|
+
Usage is refreshed independently for every authenticated rotation account at startup and on the
|
|
454
|
+
existing status interval (with the existing per-family TTL and in-flight deduplication). A fresh
|
|
455
|
+
usage response with headroom clears the older cooldown, even when its previous `resetAt` is still
|
|
456
|
+
in the future. This closes the stale `100% Free` trap where an account upgraded to Plus/Pro stayed
|
|
457
|
+
excluded until the old plan's projected reset date because only the currently selected account
|
|
458
|
+
was ever polled.
|
|
459
|
+
- **`/multi-account next` is now a true manual override.** It walks the complete account ring in
|
|
460
|
+
rotation order without moving cached-cooling accounts behind every nominally-free provider.
|
|
461
|
+
Automatic failover still avoids known-spent accounts; only the explicit user command ignores
|
|
462
|
+
potentially stale quota metadata. The black-box log now records credential-free `usage_refresh`
|
|
463
|
+
decisions so future stale-limit reports show exactly which account was rechecked and why it stayed
|
|
464
|
+
blocked or became available.
|
|
465
|
+
|
|
466
|
+
## [1.13.14] - 2026-07-07
|
|
467
|
+
|
|
468
|
+
### Fixed
|
|
469
|
+
|
|
470
|
+
- **The quota footer no longer blanks out for the current account.** Two causes: (1) the OAuth
|
|
471
|
+
access token rotates, so the stored usage snapshot's credential hash stopped matching and the
|
|
472
|
+
footer was rejected as stale → for DISPLAY it now falls back to the last stored snapshot (a
|
|
473
|
+
slightly stale "% left" beats an empty footer); (2) a `theme.fg` exception (host theme API drift)
|
|
474
|
+
was silently swallowed by the render guard, wiping the footer → the colouring is now wrapped so
|
|
475
|
+
it always falls back to plain text and still renders. If the footer is still empty after this,
|
|
476
|
+
the info is always available via `/multi-account status` and `/multi-account limits`.
|
|
477
|
+
|
|
478
|
+
## [1.13.13] - 2026-07-07
|
|
479
|
+
|
|
480
|
+
### Added
|
|
481
|
+
|
|
482
|
+
- **Qwen/Alibaba now shows a live status instead of "no usage endpoint".** Alibaba publishes no
|
|
483
|
+
usage/quota API (verified: every usage/billing path 404s and no rate-limit headers come back),
|
|
484
|
+
so a real "% left" is impossible. Instead the footer and `/multi-account status` now show the
|
|
485
|
+
account's real operational state from our own tracking: `available`, `rate-limited · retry in
|
|
486
|
+
<time>` (from a caught 429), or `needs re-login` — colour-coded green/yellow/red.
|
|
487
|
+
- **Ollama status now includes the plan tier, renewal date, and suspended flag.** `/api/me`
|
|
488
|
+
carries `Plan`, `SubscriptionPeriodEnd`, and `SuspendedAt`; these are surfaced (e.g. `Ollama |
|
|
489
|
+
pro · renews 2026-07-16`). Ollama still exposes no session/weekly token counters, so those
|
|
490
|
+
remain unavailable — that limit is Ollama's, not ours.
|
|
491
|
+
|
|
492
|
+
## [1.13.12] - 2026-07-07
|
|
493
|
+
|
|
494
|
+
### Fixed
|
|
495
|
+
|
|
496
|
+
- **Qwen/Alibaba turns no longer fail with `400: developer is not one of [...]`.** Pi sends the
|
|
497
|
+
system instructions using the OpenAI-only `developer` role (the o1+/Codex convention), but
|
|
498
|
+
Qwen's OpenAI-compatible endpoint only accepts `system`, `assistant`, `user`, `tool`,
|
|
499
|
+
`function`. A `before_provider_request` shaper now rewrites `developer` → `system` for
|
|
500
|
+
qwen-family providers only (Codex/OpenAI, which DO support `developer`, are left untouched).
|
|
501
|
+
With a valid Model Studio (International/Singapore) key, Qwen now completes turns normally.
|
|
502
|
+
|
|
503
|
+
## [1.13.11] - 2026-07-07
|
|
504
|
+
|
|
505
|
+
### Fixed
|
|
506
|
+
|
|
507
|
+
- **A session/rate limit the usage-% window can't see is no longer hot-retried every second.**
|
|
508
|
+
The usage endpoint reports an account's QUOTA window; it does not reflect session or rate
|
|
509
|
+
limits. So a session-limited account kept returning 429 "usage limit has been reached" while
|
|
510
|
+
usage still showed headroom. Because v1.13.7 made usage "ground truth", the account was
|
|
511
|
+
reported *free now* — the pending resume scheduled a ~1s retry, got 429 again, and looped,
|
|
512
|
+
while the displayed cooldown said hours (`retry automatically in ~1s` next to `Cooldowns:
|
|
513
|
+
openai-codex: 2h 3m`). Now a **repeat** limit error (two in a row, no success between) marks
|
|
514
|
+
that account's usage reading as untrusted for a while, so its real recorded cooldown sticks
|
|
515
|
+
instead of being cleared — the session waits for the true recovery and polls, rather than
|
|
516
|
+
hammering a maxed account. The genuine "over-estimated cooldown, usage shows the window really
|
|
517
|
+
reset" fast-path is preserved (it only takes effect on the FIRST error).
|
|
518
|
+
- **`/multi-account switch <provider>` now revives a stuck invalidation instead of refusing.**
|
|
519
|
+
An account could stay invalidated long after its cause was gone — e.g. it was killed by the
|
|
520
|
+
wrong Qwen endpoint (fixed in 1.13.10), and because `markInvalid` records the key's hash, the
|
|
521
|
+
hash-based auto-revive never fires while the key is unchanged. `switch alibaba` then answered
|
|
522
|
+
"no usable model … make sure it is logged in" for a perfectly good key. A manual switch is an
|
|
523
|
+
explicit user override: it now clears any stale invalidation and cooldown for the target,
|
|
524
|
+
reloads auth, forces re-discovery, and selects the account — with a clearer message that
|
|
525
|
+
distinguishes "logged in but the host exposes no model yet" from "no credentials in auth.json".
|
|
526
|
+
|
|
527
|
+
## [1.13.10] - 2026-07-07
|
|
528
|
+
|
|
529
|
+
### Fixed
|
|
530
|
+
|
|
531
|
+
- **Auto-continue after a switch no longer silently dies with "Agent is already processing".** The
|
|
532
|
+
continuation-prompt injection called `pi.sendUserMessage(prompt)` with no delivery option, so when
|
|
533
|
+
it fired while the previous turn was still streaming — exactly the race right after a failover
|
|
534
|
+
switch — the host rejected it with *"Agent is already processing. Specify streamingBehavior
|
|
535
|
+
('steer' or 'followUp')"* and the continuation was lost. It now passes `{ deliverAs: "followUp" }`
|
|
536
|
+
(the extension-facing option the host maps to `streamingBehavior`), so the continuation is QUEUED
|
|
537
|
+
to run after the current turn settles. Locked with a test asserting the option is present.
|
|
538
|
+
- **A genuinely-spent account is benched from its usage endpoint even if it never threw an error.**
|
|
539
|
+
Selection used to treat an account with no *recorded* cooldown as available, so right after one
|
|
540
|
+
account hit its limit, failover would hop to the next Codex slot that was *also* maxed (its 100%
|
|
541
|
+
state known only from usage, not from a cooldown) and burn a request there instead of jumping
|
|
542
|
+
straight to a live account. Two changes: `providerRecoveryAt` now trusts a hard block (a usage
|
|
543
|
+
window ≥100% with a future reset) as authoritative *regardless of snapshot age* — a maxed 30-day
|
|
544
|
+
window cannot recover in the minutes since the last probe — and `storeUsage` records the cooldown
|
|
545
|
+
proactively the moment any probe reports the block. "Available now" is still only trusted while the
|
|
546
|
+
snapshot is fresh, so a stale pre-limit reading can never clear a real cooldown early.
|
|
547
|
+
- **A valid Qwen/Alibaba key is no longer misread as invalid (false 401 → wrongful eviction).** The
|
|
548
|
+
default Qwen endpoint was `token-plan.ap-southeast-1.maas.aliyuncs.com`, a promo "token plan"
|
|
549
|
+
endpoint that accepts the key on `/models` but returns `401 invalid_api_key` on `/chat/completions`
|
|
550
|
+
once the plan lapses — so a perfectly good key looked invalid and the account was dropped from
|
|
551
|
+
rotation ("worked yesterday, fails today"). Switched the default to the standard International
|
|
552
|
+
endpoint `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`, verified with a live request
|
|
553
|
+
returning 200 for the same key.
|
|
554
|
+
|
|
555
|
+
## [1.13.9] - 2026-07-07
|
|
556
|
+
|
|
557
|
+
### Fixed
|
|
558
|
+
|
|
559
|
+
- **Failover now actually resumes on hosts without `pi.continueAgent()` — no more dead-end
|
|
560
|
+
"Update @earendil-works/pi-coding-agent" error.** The seamless in-place resume relies on
|
|
561
|
+
`pi.continueAgent()`, but the shipped runtime (`@earendil-works/pi-coding-agent` 0.80.3) does
|
|
562
|
+
not expose it to extensions. The old code detected the missing method and gave up with a red
|
|
563
|
+
error, so after every provider switch the turn stalled and the user had to reload by hand — the
|
|
564
|
+
switch happened but the work never continued. It now degrades gracefully: when `continueAgent`
|
|
565
|
+
is unavailable it injects the continuation prompt as a fresh user turn (the same fallback already
|
|
566
|
+
used when the transcript tail is a completed assistant message), so the session keeps moving by
|
|
567
|
+
itself on the account it just switched to. Factored the injection into one `injectContinuationPrompt`
|
|
568
|
+
helper shared by both paths.
|
|
569
|
+
- **Genuinely spent monthly Codex accounts are benched for their REAL reset, so rotation advances
|
|
570
|
+
to Qwen/Ollama instead of ping-ponging between exhausted Codex slots.** `providerRecoveryAt` now
|
|
571
|
+
treats fresh usage-endpoint data as authoritative ground truth in BOTH directions: a maxed
|
|
572
|
+
long/rolling window (e.g. a free-tier Codex monthly limit at 100%) reports a real far-out reset,
|
|
573
|
+
and we trust it rather than letting the 6h re-probe cap keep un-benching the account every 6h.
|
|
574
|
+
That cap kept exhausted accounts looking "available soon", so auto-failover cycled
|
|
575
|
+
`account-3 ↔ account-4` forever and never reached a healthy Alibaba/Ollama account. The 6h clamp
|
|
576
|
+
still guards *error-text* estimates (`markExhausted` / `pruneCooldowns`); only the recovery time
|
|
577
|
+
computed for selection from live usage is affected.
|
|
578
|
+
- **Startup host-capability preflight — the recurring "pi changed its API from under us" class is
|
|
579
|
+
now caught loudly at load instead of weeks later under fire.** Every session start probes the REAL
|
|
580
|
+
`pi` object for the methods failover depends on (`setModel`, `sendUserMessage`, `continueAgent`,
|
|
581
|
+
`registerProvider`, …), records them in the debug log (`host_capabilities`, dated, with the running
|
|
582
|
+
version), and — once per process — tells the user in plain terms if switching is impossible
|
|
583
|
+
(`setModel` gone → error), if auto-continue is impossible (neither resume method → warning), or if
|
|
584
|
+
only the seamless path is missing (continueAgent gone → info: failover still works via injection).
|
|
585
|
+
Unit tests mock `pi` and always implement every method, so they can NEVER catch this drift; the
|
|
586
|
+
preflight is what turns a silent boundary regression into an immediate, self-diagnosing message.
|
|
587
|
+
- Regression tests added (fail on the old code, pass on the new): a host with no `pi.continueAgent`
|
|
588
|
+
still auto-continues via prompt injection; a session whose two Codex accounts are both at 100%
|
|
589
|
+
monthly fails over to the healthy Qwen account instead of ping-ponging; and the preflight flags a
|
|
590
|
+
continueAgent-less host as an expected fallback, warns when no resume path exists, and stays silent
|
|
591
|
+
on a fully-capable host.
|
|
592
|
+
|
|
593
|
+
## [1.13.8] - 2026-07-06
|
|
594
|
+
|
|
595
|
+
### Fixed
|
|
596
|
+
|
|
597
|
+
- **Failover never silently downgrades the model, and `/multi-account next` cycles
|
|
598
|
+
through every account.** Two related bugs made the rotation misbehave:
|
|
599
|
+
- **Model flap / silent downgrade.** Each account was expanded into *one candidate per
|
|
600
|
+
model* it exposes (`gpt-5.5`, `gpt-5.4`, `gpt-5.4-mini`, …). So a failover could drop
|
|
601
|
+
to a weaker model of the *same* account, and repeated `/multi-account next` ping-ponged
|
|
602
|
+
e.g. `gpt-5.4 ↔ gpt-5.4-mini`. Now each account contributes exactly **one candidate —
|
|
603
|
+
its newest/flagship model**. The model is only ever demoted when the flagship is
|
|
604
|
+
*individually* unavailable (a genuine "model unavailable" error), never to dodge a
|
|
605
|
+
provider-level usage limit and never to fill the rotation. The most powerful model of
|
|
606
|
+
every provider is always the one offered. A single-account session whose flagship is
|
|
607
|
+
unavailable now holds its model and reports "nothing better to move to" instead of
|
|
608
|
+
flapping down to a mini model.
|
|
609
|
+
- **Rotation collapsed onto one provider.** Manual `/multi-account next` recorded a
|
|
610
|
+
**5-minute cooldown on the account it left**. After one lap every account was "cooling"
|
|
611
|
+
and the round-robin collapsed onto whatever remained (typically the one openai slot).
|
|
612
|
+
Manual rotation is a user override, not a rate-limit event, so it no longer records any
|
|
613
|
+
cooldown — every account stays selectable and repeated `next` truly cycles through all
|
|
614
|
+
of them.
|
|
615
|
+
- As a consequence of one-candidate-per-account, the "same account just recovered → resume
|
|
616
|
+
on it" path now also covers the empty-candidate case, so a single-account session still
|
|
617
|
+
resumes immediately when fresh usage shows its cooldown was over-estimated (it no longer
|
|
618
|
+
depended on a weaker sibling model being in the queue).
|
|
619
|
+
|
|
620
|
+
## [1.13.7] - 2026-07-04
|
|
621
|
+
|
|
622
|
+
### Fixed
|
|
623
|
+
|
|
624
|
+
- **Bogus weeks-long cooldowns no longer evict a live account from rotation.** When a
|
|
625
|
+
Codex account maxed a long *rolling* limit window (weekly/monthly), the reset time
|
|
626
|
+
of that window (or a mis-parsed `resets_at`) was recorded literally as the account's
|
|
627
|
+
cooldown — e.g. `openai-codex-account-2` was locked until **2026-08-03 (30 days)** and
|
|
628
|
+
`openai-codex-account-3` until 2026-07-21. Because cooling-down accounts are never
|
|
629
|
+
re-probed, the estimate was a dead end: a perfectly healthy account (its short/primary
|
|
630
|
+
window already free) sat out of rotation for weeks, producing "no immediately available
|
|
631
|
+
fallback" even though fallbacks existed. Three-layer fix:
|
|
632
|
+
- `resolveLimitCooldownMs` now treats **fresh usage as ground truth**: if the usage probe
|
|
633
|
+
says the primary window has headroom (`usageMs === 0`), the account is available *now* and
|
|
634
|
+
the pessimistic error-text estimate is discarded (previously the `> 0` filter dropped the
|
|
635
|
+
`0` and a stale 30-day `resets_at` won the `Math.max`).
|
|
636
|
+
- New `MAX_LIVE_COOLDOWN_MS` (6h) caps **any** live-parsed cooldown at record time
|
|
637
|
+
(`markExhausted`) — no single estimate can lock an account longer than one re-probe cycle.
|
|
638
|
+
- Persisted far-future cooldowns are **clamped on load and in `pruneCooldowns`**, so an
|
|
639
|
+
already-poisoned state file self-heals on the next restart without `/multi-account reset`.
|
|
640
|
+
- **`VERSION` constant was stuck at `1.13.5`.** It was never bumped for the 1.13.6 release, so
|
|
641
|
+
every on-screen `[v1.13.5]` failover tag under-reported the actually-running code — defeating
|
|
642
|
+
the version stamp whose entire purpose is to tell a live window from a stale one. Now `1.13.7`.
|
|
643
|
+
|
|
644
|
+
## [1.13.6] - 2026-07-04
|
|
645
|
+
|
|
646
|
+
### Fixed
|
|
647
|
+
|
|
648
|
+
- **API-key providers no longer loop forever on a dead key.** A bare `401
|
|
649
|
+
Unauthorized` from a non-refreshable provider (Ollama Cloud, Alibaba, OpenRouter)
|
|
650
|
+
was treated as transient: the same key kept getting 1-minute cooldowns but the
|
|
651
|
+
consecutive-failure counter never advanced (same-hash repeats were deliberately
|
|
652
|
+
ignored to avoid false kills on OAuth refresh faults). This created an infinite
|
|
653
|
+
loop — the account was never invalidated, never told the user to re-login, and
|
|
654
|
+
consumed the entire fallback rotation one retry at a time. Now, for
|
|
655
|
+
non-refreshable (API-key) providers, repeated same-key 401s advance a separate
|
|
656
|
+
`MAX_SAME_KEY_AUTH_FAILURES` (3) counter and invalidate the slot after 3
|
|
657
|
+
consecutive failures. OAuth providers are unaffected — same-hash 401s on a
|
|
658
|
+
refreshable account still only re-arm the transient cooldown (refresh-fault
|
|
659
|
+
tolerance preserved). Regression test locks both paths.
|
|
660
|
+
|
|
661
|
+
- **Re-login now clears stale 401-streak tracking for transient-cooldown accounts.**
|
|
662
|
+
Previously, `clearReauthedInvalidations()` only cleared `authFailures` for
|
|
663
|
+
accounts in `invalidatedByProvider`. An account on transient cooldown (not
|
|
664
|
+
invalidated) kept its stale `authFailures` entry after the user re-logged in with
|
|
665
|
+
new credentials, so the next 401 inherited the old failure count and could
|
|
666
|
+
invalidate prematurely or loop. Now `refreshDiscovery()` clears `authFailures`
|
|
667
|
+
when: (a) the stable account fingerprint changes (different real account —
|
|
668
|
+
re-login to a new slot), and (b) the credential hash changes for a
|
|
669
|
+
non-refreshable (API-key) provider (user manually replaced the key). OAuth
|
|
670
|
+
token rotations (routine Pi refresh) do NOT clear the streak — the 401 counter
|
|
671
|
+
must survive so rotated-token failures can still accumulate toward the kill
|
|
672
|
+
threshold. Regression test locks the re-login fresh-start path.
|
|
673
|
+
|
|
674
|
+
## [1.13.5] - 2026-07-01
|
|
675
|
+
|
|
676
|
+
### Fixed
|
|
677
|
+
|
|
678
|
+
- **A "still busy" auto-retry no longer downgrades the model.** When a resumed turn had
|
|
679
|
+
not gone idle in time, the auto-retry treated the current model as failed and rotated to
|
|
680
|
+
an older sibling on the SAME account — the reported `openai-codex-account-4/gpt-5.5 →
|
|
681
|
+
openai-codex-account-4/gpt-5.4 (previous turn was still busy; auto-retry)`. But a
|
|
682
|
+
"still busy" state is a timing issue, not a model failure, and a same-account switch
|
|
683
|
+
shares the same quota pool, so the downgrade escaped nothing and only lost quality. The
|
|
684
|
+
busy auto-retry now resumes the **same** model (waiting for it if the account is briefly
|
|
685
|
+
cooling), exactly like a transient-server-error retry. Regression test locks it
|
|
686
|
+
(proved red→green: without the fix the resume produced `gpt-5.5 → gpt-5.4`).
|
|
687
|
+
|
|
688
|
+
## [1.13.4] - 2026-07-01
|
|
689
|
+
|
|
690
|
+
### Fixed
|
|
691
|
+
|
|
692
|
+
- **Never silently downgrade the model during a rotation.** When failover switched
|
|
693
|
+
accounts, the newest model (e.g. `gpt-5.5`) could be dropped in favour of an older one
|
|
694
|
+
(`gpt-5.4`) on a nearer account. Root cause: fallback candidates were ranked only by
|
|
695
|
+
account rotation index and cooldown — model recency was not part of the ranking at all,
|
|
696
|
+
so an older model on a lower-index account beat the newest model on a healthy account.
|
|
697
|
+
Now, when `preferLatestModel` is on (the default), model recency is the **primary**
|
|
698
|
+
tiebreak: the latest available model wins across accounts, and rotation order only
|
|
699
|
+
breaks ties between equally-new models. Regression test locks the behaviour
|
|
700
|
+
(proved red→green).
|
|
701
|
+
|
|
702
|
+
## [1.13.3] - 2026-06-30
|
|
703
|
+
|
|
704
|
+
### Fixed
|
|
705
|
+
|
|
706
|
+
- **Fail over when your ACTIVE model is on an unmanaged provider** (e.g. a plain
|
|
707
|
+
`openai` API key that returns "You exceeded your current quota / insufficient_quota").
|
|
708
|
+
Previously the extension only reacted to errors from providers it manages
|
|
709
|
+
(`anthropic`, `openai-codex`, `qwen`, `ollama`, `cursor`), so a quota error on a plain
|
|
710
|
+
`openai` model was ignored and no rotation happened. Now, if the model you are
|
|
711
|
+
currently using hits a limit/auth/quota error — even on an unmanaged provider — the
|
|
712
|
+
task is rescued by switching to a managed account (short model-scoped cooldown; the
|
|
713
|
+
unmanaged provider's lifecycle is left untouched). Background errors from unrelated
|
|
714
|
+
providers you are NOT on are still ignored, so nothing gets hijacked.
|
|
715
|
+
|
|
716
|
+
## [1.13.2] - 2026-06-29
|
|
717
|
+
|
|
718
|
+
### Fixed
|
|
719
|
+
|
|
720
|
+
- **Always use the newest model; never stay downgraded.** Once a turn dropped to an
|
|
721
|
+
older model (e.g. `gpt-5.4` after a momentary limit or model-cooldown on `gpt-5.5`),
|
|
722
|
+
the "keep the current model across same-family switches" logic carried the old model
|
|
723
|
+
forward forever. Failover now tries the newest preferred model **first**, so it
|
|
724
|
+
upgrades back to the latest the moment it is available again. New config
|
|
725
|
+
`preferLatestModel` (default `true`); set `false` for the old keep-current behavior.
|
|
726
|
+
|
|
727
|
+
### Added
|
|
728
|
+
|
|
729
|
+
- **`preferredModels` config** — pin the newest model per provider without a code
|
|
730
|
+
change, e.g. `"preferredModels": { "openai-codex": ["gpt-5.6","gpt-5.5"] }`. Keys:
|
|
731
|
+
`anthropic`, `openai-codex`, `cursor`, `qwen`, `ollama`. Newest first.
|
|
732
|
+
- **`/multi-account models`** — shows, per account, the model order the extension would
|
|
733
|
+
use (★ = selected), so you can see at a glance whether the latest model is available
|
|
734
|
+
and chosen everywhere.
|
|
735
|
+
|
|
736
|
+
## [1.13.1] - 2026-06-29
|
|
737
|
+
|
|
738
|
+
### Changed
|
|
739
|
+
|
|
740
|
+
- **Failover messages now carry the running version**, e.g.
|
|
741
|
+
`Provider failover [v1.13.1]: openai-codex → openai-codex-account-2 (...)`.
|
|
742
|
+
A running Pi keeps the extension code it started with, so restarting one window
|
|
743
|
+
does not update others — and an old window silently shows old behavior. Now the
|
|
744
|
+
version is printed in the exact messages you read when something goes wrong: if a
|
|
745
|
+
failover message has **no** `[v…]` tag (or an older number), that window is running
|
|
746
|
+
stale code and must be restarted. This is the single biggest source of "I fixed it
|
|
747
|
+
but it still breaks" confusion. Stamped on the switch, stuck-recovery, bounded-wait,
|
|
748
|
+
and breaker messages.
|
|
749
|
+
|
|
750
|
+
## [1.13.0] - 2026-06-29
|
|
751
|
+
|
|
752
|
+
Reliability floor: turn "it just sits there spinning" and "I have to re-type the
|
|
753
|
+
prompt" into automatic recovery, and guarantee the extension can never be *worse*
|
|
754
|
+
than switching accounts by hand.
|
|
755
|
+
|
|
756
|
+
### Changed
|
|
757
|
+
|
|
758
|
+
- **The stuck-resume watchdog now ACTS instead of only warning.** When a resumed
|
|
759
|
+
turn goes silent past `stuckWatchdogMs` (and no tool is running), it auto-cancels
|
|
760
|
+
the wedged turn and arms auto-resume, which continues the work the moment any
|
|
761
|
+
account frees up. You no longer have to press Esc and re-type the prompt. Opt out
|
|
762
|
+
with `autoRecoverStuck: false` (reverts to notify-only).
|
|
763
|
+
- **A running build/test is never mistaken for a wedge.** Tool start/stop is tracked,
|
|
764
|
+
so a long silent `xcodebuild`/test command is left alone.
|
|
765
|
+
- **The bounded idle-wait now schedules the retry it promised** instead of just
|
|
766
|
+
saying it would.
|
|
767
|
+
- **Un-continuable resumes self-heal.** If the transcript tail can't be continued
|
|
768
|
+
(e.g. after a recovery abort), the extension injects the continuation prompt as a
|
|
769
|
+
message so work proceeds — bounded by `maxAutoContinuesPerPrompt`, never a loop.
|
|
770
|
+
|
|
771
|
+
### Added
|
|
772
|
+
|
|
773
|
+
- **Circuit breaker (the reliability floor).** If automatic recovery fails
|
|
774
|
+
`BREAKER_FAILURE_THRESHOLD` (3) times in a row, the extension drops to *advisory
|
|
775
|
+
mode* for 10 min: it still flags rate limits and switches you to a fresh account,
|
|
776
|
+
but stops attempting the auto-continue that was failing — so a bad state can never
|
|
777
|
+
spiral into repeated hangs. It closes again on the first successful response, a new
|
|
778
|
+
user prompt, or `/multi-account reset`. Visible in `/multi-account status`.
|
|
779
|
+
- **Black box decision log.** Every meaningful decision (assistant error + how it was
|
|
780
|
+
classified, account switch, no-fallback, resume start/ok/stuck, watchdog action,
|
|
781
|
+
breaker open/close, compaction routing, internal errors) is appended to
|
|
782
|
+
`~/.pi/agent/provider-failover-debug.log`. This turns "it broke again" into a
|
|
783
|
+
precise, reproducible trail — the basis for fixing real-world bugs that unit tests
|
|
784
|
+
can't reach. Bounded size (one rotation at 4 MB), credential-free with defensive
|
|
785
|
+
token redaction. View with `/multi-account log [N]`; toggle with `log on|off`.
|
|
786
|
+
- New config keys `autoRecoverStuck` (default `true`) and `debugLog` (default `true`).
|
|
787
|
+
|
|
788
|
+
> Fully restart Pi (not `/reload`); confirm `/multi-account status` shows **v1.13.0**.
|
|
789
|
+
|
|
790
|
+
## [1.12.0] - 2026-06-29
|
|
791
|
+
|
|
792
|
+
Robustness pass: the two ways a failover could silently freeze the session are
|
|
793
|
+
now fixed at the root, plus a generic watchdog so any *future* stall surfaces as
|
|
794
|
+
an actionable message instead of an endless "Working…" spinner.
|
|
795
|
+
|
|
796
|
+
### Fixed
|
|
797
|
+
|
|
798
|
+
- **No more `Cannot continue from message role: assistant`.** After a switch, the
|
|
799
|
+
pending `currentPromptSwitch` was never cleared on a *successful* turn, so a
|
|
800
|
+
later `agent_end` re-dispatched a resume when there was nothing to continue —
|
|
801
|
+
`pi.continueAgent()` then threw that cryptic red error into the transcript. The
|
|
802
|
+
extension now only resumes when the turn actually ended in an **error** it can
|
|
803
|
+
continue from; a non-error end clears the switch. (This was the unexplained
|
|
804
|
+
first error users saw above a stuck spinner.)
|
|
805
|
+
- **Compaction survives account exhaustion.** New `session_before_compact` handler:
|
|
806
|
+
when the active account is rate-limited/invalidated and Pi needs to summarize
|
|
807
|
+
(context overflow or threshold), the summary is generated on a **healthy
|
|
808
|
+
fallback account** instead of dying on the dead one. This was the "rotated and
|
|
809
|
+
then it just hangs at high context" freeze. Strictly fail-safe — falls back to
|
|
810
|
+
Pi's default compaction whenever it cannot positively do better.
|
|
811
|
+
- **No unbounded waits.** `resumeWithExistingContext()` replaced its infinite
|
|
812
|
+
`while (!isIdle)` busy-loop with a bounded wait (`resumeIdleTimeoutMs`, default
|
|
813
|
+
90s) that retries later instead of wedging, and the routed compaction call is
|
|
814
|
+
bounded by a 150s timeout.
|
|
815
|
+
- **Never resume onto a still-cooling account.** Before continuing, the extension
|
|
816
|
+
reconciles live usage; if the just-switched-to account is itself spent (its 5h
|
|
817
|
+
limit only became visible after a usage refresh), it pauses for the first
|
|
818
|
+
account that *actually* recovers instead of burning a request / wedging.
|
|
819
|
+
|
|
820
|
+
### Added
|
|
821
|
+
|
|
822
|
+
- **Forward-progress watchdog.** A resumed turn that shows no activity (no stream
|
|
823
|
+
token, tool event, or provider response) for `stuckWatchdogMs` (default 180s)
|
|
824
|
+
raises a clear, actionable notice — *press Esc, then `/multi-account next` or
|
|
825
|
+
`/compact`* — and re-checks periodically, so a silent wedge can never again look
|
|
826
|
+
like normal "working".
|
|
827
|
+
- **`/multi-account status`** now shows the resume-watchdog state, compaction
|
|
828
|
+
routing mode, and the last context-overflow time.
|
|
829
|
+
- New config keys: `routeCompactionToHealthyAccount` (default `true`),
|
|
830
|
+
`resumeIdleTimeoutMs`, `stuckWatchdogMs`.
|
|
831
|
+
|
|
832
|
+
### Hardened (systemic — covers whole classes of failure, not just the bugs above)
|
|
833
|
+
|
|
834
|
+
Rather than patch individual crashes, the entire surface is now fail-safe by
|
|
835
|
+
construction:
|
|
836
|
+
|
|
837
|
+
- **Every one of the ~12 Pi event handlers is crash-isolated** (`safeOn`). A throw
|
|
838
|
+
or async rejection anywhere — a host payload-shape change, a formatter edge case,
|
|
839
|
+
a null deref we never imagined — is reported once and swallowed, the failover step
|
|
840
|
+
is skipped, and Pi keeps running. Node aborts the whole process on an unhandled
|
|
841
|
+
rejection; this removes that entire class of "the extension took Pi down with it".
|
|
842
|
+
- **Every background timer/async task is wrapped** (`runBackground`): the usage
|
|
843
|
+
footer interval, the pending-resume wake, the queued-input wake, and every
|
|
844
|
+
fire-and-forget `refreshUsage` can no longer leak an unhandled rejection.
|
|
845
|
+
- **Error reports are deduped** (same fault ≤ once / 30 s) so a repeating internal
|
|
846
|
+
fault can never become a notification storm, and the dedupe map is capped.
|
|
847
|
+
- **All persistence is best-effort.** `saveState` and the footer renderer can no
|
|
848
|
+
longer throw out of the code path they run in (locked/*read-only*/full disk, a
|
|
849
|
+
theme-shape change) — in-memory state stays correct and failover continues.
|
|
850
|
+
- **Timers are `unref`'d** so a pending wake can never keep the process alive after
|
|
851
|
+
the session ends.
|
|
852
|
+
|
|
853
|
+
> After updating you **must fully restart Pi** (not `/reload`) for the new code to
|
|
854
|
+
> load; confirm `/multi-account status` shows **v1.12.0**.
|
|
855
|
+
|
|
856
|
+
## [1.11.0] - 2026-06-28
|
|
857
|
+
|
|
858
|
+
### Added
|
|
859
|
+
|
|
860
|
+
- **`/multi-account remove`** — symmetric counterpart to `add`. Pass a family
|
|
861
|
+
(`anthropic`, `codex`, `cursor`, `ollama`, `qwen`) to drop the highest numbered
|
|
862
|
+
authed alias slot, or pass a full provider id (e.g. `openai-codex-account-3`)
|
|
863
|
+
to remove that exact account from `auth.json`, clear its failover state, and
|
|
864
|
+
refresh rotation. Aliases: `rm`, `delete`.
|
|
865
|
+
|
|
866
|
+
## [1.10.2] - 2026-06-26
|
|
867
|
+
|
|
868
|
+
### Fixed
|
|
869
|
+
|
|
870
|
+
- **Cross-provider failover no longer reuses the source model id on the target
|
|
871
|
+
provider.** Switching from Anthropic/Cursor/Ollama to Codex (or any other family)
|
|
872
|
+
now picks that family's default model (e.g. `gpt-5.5`) instead of trying
|
|
873
|
+
`claude-opus-4-8` on Codex, which caused confusing resumes and activation
|
|
874
|
+
failures.
|
|
875
|
+
- **Account selection now honours live usage when deciding if a slot is available.**
|
|
876
|
+
`findFallbackModels()` and `isCurrentModelReady()` use `providerRecoveryAt()`
|
|
877
|
+
(recorded cooldown reconciled against fresh usage) instead of blindly trusting
|
|
878
|
+
stale `exhaustedUntilByProvider` timestamps. Accounts with valid tokens whose
|
|
879
|
+
usage endpoint says they are free are selectable again.
|
|
880
|
+
- **Failover continuation is queued immediately after a successful switch in
|
|
881
|
+
`message_end`,** not only from `agent_end`. This removes a race where Pi could
|
|
882
|
+
end the turn before `currentPromptSwitch` was armed, leaving the next account
|
|
883
|
+
idle or starting from the wrong place.
|
|
884
|
+
- **`before_agent_start` no longer runs `ensureReadyModel()` for extension-owned
|
|
885
|
+
continuation prompts,** so the failover target is not re-switched away before
|
|
886
|
+
the resumed turn starts.
|
|
887
|
+
- **Continuation prompts now restate the original user task** captured at the
|
|
888
|
+
start of the interrupted turn, so the replacement provider knows what to
|
|
889
|
+
continue instead of guessing from a generic "keep going" message.
|
|
890
|
+
|
|
891
|
+
## [1.9.3] - 2026-06-21
|
|
892
|
+
|
|
893
|
+
### Fixed
|
|
894
|
+
|
|
895
|
+
- **`/multi-account clear` now removes alias slots from auth.json.** Previously
|
|
896
|
+
`clear` only wiped the fallbacks config and state, but left
|
|
897
|
+
`anthropic-account-2`, `openai-codex-account-N`, etc. in `auth.json` — so
|
|
898
|
+
`/multi-account add` offered account-3 instead of starting fresh at
|
|
899
|
+
account-2. `clear` now deletes every `-account-N` entry from `auth.json`,
|
|
900
|
+
resets `registeredSlots`, and reloads host auth so the next `add` starts
|
|
901
|
+
clean.
|
|
902
|
+
|
|
903
|
+
## [1.9.2] - 2026-06-21
|
|
904
|
+
|
|
905
|
+
### Added
|
|
906
|
+
|
|
907
|
+
- **`/multi-account clear`** — wipe all fallbacks, cooldowns, invalidations,
|
|
908
|
+
usage snapshots, pending work and rotation state so the user can rebuild
|
|
909
|
+
the fallback list from scratch. The `fallbacks` array in
|
|
910
|
+
`provider-failover.json` is reset to `[]` on disk; re-add accounts to
|
|
911
|
+
`auth.json` and run `/multi-account rediscover` to repopulate.
|
|
912
|
+
|
|
913
|
+
## [1.9.1] - 2026-06-21
|
|
914
|
+
|
|
915
|
+
### Fixed
|
|
916
|
+
|
|
917
|
+
- **Ollama/Alibaba not picked up by Pi.** The extension expected Pi to register
|
|
918
|
+
the base `ollama`/`alibaba` providers natively from `models.json`, but if the
|
|
919
|
+
`apiKey` field there was a placeholder (e.g. `"ollama"`), Pi never exposed the
|
|
920
|
+
provider to `modelRegistry` — so `resolveTargets()` returned `[]` and the
|
|
921
|
+
family never failovered. The extension now registers the base API-key
|
|
922
|
+
provider itself (with the real key from `auth.json`) via
|
|
923
|
+
`ensureApiKeyBaseProvider()`, making Ollama and Alibaba/Qwen first-class
|
|
924
|
+
rotation members.
|
|
925
|
+
- **`pi.registerProvider` error for spare API-key slots.** API-key families
|
|
926
|
+
(ollama, qwen) no longer auto-register a spare slot — there is no
|
|
927
|
+
interactive `/login` for them, so an empty spare triggered Pi's
|
|
928
|
+
`"apiKey or oauth is required when defining models"` error.
|
|
929
|
+
- **Test flake: api_key transient cooldown assertion.** Relaxed the sub-minute
|
|
930
|
+
bound to sub-2min to accommodate `markExhausted`'s 1-second floor.
|
|
931
|
+
|
|
932
|
+
## [1.9.0] - 2026-06-21
|
|
933
|
+
|
|
934
|
+
### Fixed
|
|
935
|
+
|
|
936
|
+
- **False permanent invalidation of live OAuth accounts.** A single transient
|
|
937
|
+
401 burst from OpenAI Codex (one physical event surfaced as three error hooks)
|
|
938
|
+
hit `MAX_CONSECUTIVE_AUTH_FAILURES = 3` instantly and permanently killed a
|
|
939
|
+
live account for a year, even while a parallel Pi session was successfully
|
|
940
|
+
using the same token. The threshold is raised to 8 and the dedup logic now
|
|
941
|
+
ignores same-hash repeat failures (refresh didn't reach the wire), so only
|
|
942
|
+
genuinely distinct refreshed-token failures advance the kill counter.
|
|
943
|
+
- **`refresh_token_invalidated` / `session has ended` no longer treated as
|
|
944
|
+
terminal.** OpenAI Codex returns these transiently under load. They are now
|
|
945
|
+
classified as transient — the account gets a short cooldown and the next
|
|
946
|
+
attempt can still refresh. Only `invalid_grant` and `revoked` remain terminal.
|
|
947
|
+
- **365-day "cooldown" entries removed.** `markInvalid` no longer writes a
|
|
948
|
+
year-long entry into `exhaustedUntilByProvider` — that polluted cooldown
|
|
949
|
+
displays ("Cooldowns: account-2: 8696h") and confused users into thinking
|
|
950
|
+
dead accounts were rate-limited. Invalidated providers are reported
|
|
951
|
+
separately. `switchToFallback` no longer applies `invalidCooldownMs` to a
|
|
952
|
+
killed account (it's already in `invalidatedByProvider`).
|
|
953
|
+
- **API-key providers (Ollama, Alibaba) survive a bare 401.** Previously a
|
|
954
|
+
single 401 on an api_key provider immediately invalidated it for a year.
|
|
955
|
+
Now only explicit terminal patterns (`invalid api key`, `incorrect api key`,
|
|
956
|
+
`revoked`) kill the slot; a bare 401 gets a transient cooldown and the same
|
|
957
|
+
consecutive-failure accounting as OAuth.
|
|
958
|
+
- **Warning messages separate invalidated from cooldowns.** The "no
|
|
959
|
+
immediately available fallback" warning no longer lists dead accounts with
|
|
960
|
+
8696h timers — they're shown as `Invalidated (need re-login)`.
|
|
961
|
+
|
|
962
|
+
### Added
|
|
963
|
+
|
|
964
|
+
- **Multi-account support for Ollama and Alibaba/Qwen.** API-key providers
|
|
965
|
+
now support numbered alias slots (`ollama-account-2`, `alibaba-account-3`,
|
|
966
|
+
…) exactly like OAuth providers. Each slot is a separate API key in
|
|
967
|
+
`auth.json` and joins the rotation automatically. `/multi-account add
|
|
968
|
+
ollama|qwen` registers the next free slot.
|
|
969
|
+
- **`/multi-account revive <provider|all>`** — clear a false invalidation
|
|
970
|
+
and return an account to rotation without wiping all state (unlike `reset`).
|
|
971
|
+
- **Ollama (GLM-5.2) and Alibaba (Qwen3.7-Max) in the default rotation.**
|
|
972
|
+
`classifyProvider` recognizes `ollama-account-N` and `alibaba-account-N`;
|
|
973
|
+
`resolveTargets` knows the preferred models for each family.
|
|
974
|
+
|
|
975
|
+
### Changed
|
|
976
|
+
|
|
977
|
+
- `DEFAULT_QWEN_MODELS = ["qwen3.7-max", "qwen-max", "qwen-plus"]`.
|
|
978
|
+
- `slotId` and `syncRegisteredSlots` generalized to all four provider
|
|
979
|
+
families. API-key families skip the "spare slot" auto-registration (no
|
|
980
|
+
interactive login) to avoid Pi's "apiKey or oauth required" error.
|
|
981
|
+
|
|
982
|
+
## [1.8.0] - 2026-06-20
|
|
983
|
+
|
|
984
|
+
### Fixed
|
|
985
|
+
|
|
986
|
+
- **Failover no longer triggers for unmanaged providers.** Previously, a
|
|
987
|
+
rate-limit (429) or quota error on *any* provider — including ones this
|
|
988
|
+
extension does not manage (Ollama, OpenRouter, DeepSeek, etc.) — triggered
|
|
989
|
+
the failover logic and switched the user to an unrelated managed account.
|
|
990
|
+
The `message_end` and `after_provider_response` handlers now check
|
|
991
|
+
`classifyProvider()` before reacting, so only errors from anthropic,
|
|
992
|
+
openai-codex, qwen, or ollama providers activate failover.
|
|
993
|
+
- **No more false “all limits exhausted” from setModel failures.** When
|
|
994
|
+
`activateFallback` tried to switch to a fallback account and the
|
|
995
|
+
`pi.setModel()` call failed (for any reason — model not found, SDK error,
|
|
996
|
+
etc.), it called `markExhausted()` on that account. If several candidates
|
|
997
|
+
failed in a row, *all* managed accounts appeared exhausted in the status
|
|
998
|
+
even though none had actually hit a limit. setModel failures now simply skip
|
|
999
|
+
the candidate for the current attempt without persisting a cooldown.
|
|
1000
|
+
|
|
1001
|
+
### Added
|
|
1002
|
+
|
|
1003
|
+
- **Ollama provider support.** Ollama is now a first-class provider family in
|
|
1004
|
+
the rotation, alongside Anthropic, OpenAI Codex, and Qwen. The default
|
|
1005
|
+
model is `glm-5.2:cloud`. Enable/disable with the `includeOllama` config
|
|
1006
|
+
option (default `true`).
|
|
1007
|
+
|
|
1008
|
+
## [1.7.0] - 2026-06-13
|
|
1009
|
+
|
|
1010
|
+
### Fixed
|
|
1011
|
+
|
|
1012
|
+
- **Cooldowns no longer reset on routine OAuth token refresh.** A rate-limit
|
|
1013
|
+
cooldown was keyed to the credential blob, so the periodic access-token refresh
|
|
1014
|
+
that Pi performs looked like a re-login and wiped the cooldown — the still-limited
|
|
1015
|
+
account was then re-selected and instantly hit the same 429. Cooldowns now clear
|
|
1016
|
+
only when the slot is genuinely re-logged into a *different* real account (stable
|
|
1017
|
+
account fingerprint changes); a token rotation keeps the recovery time intact.
|
|
1018
|
+
- **`/multi-account next` now cycles through every account.** It walked to the
|
|
1019
|
+
account with the shortest remaining cooldown, which made repeated presses bounce
|
|
1020
|
+
between just the two soonest-to-recover accounts and never reach the rest of the
|
|
1021
|
+
rotation. It now round-robins forward from the current account (offering any
|
|
1022
|
+
account that is free *right now* first), so each press advances through all slots.
|
|
1023
|
+
- **Paused sessions resume on the first account that *actually* recovers.** While
|
|
1024
|
+
every account is cooling down the session now re-checks availability on a short
|
|
1025
|
+
poll instead of sleeping on a single multi-hour estimate, and it reconciles each
|
|
1026
|
+
cooling account against its live usage endpoint. An account whose real limit reset
|
|
1027
|
+
earlier than the recorded estimate (or that a parallel `/login` freed) now picks
|
|
1028
|
+
the work back up promptly instead of waiting out a stale countdown.
|
|
1029
|
+
- Wait-time messages show an honest duration (e.g. `2h 20m`) instead of rounding
|
|
1030
|
+
up to a misleading whole hour (`~3h`).
|
|
1031
|
+
|
|
1032
|
+
### Added
|
|
1033
|
+
|
|
1034
|
+
- `pendingPollMs` config option (default 60s): how often a paused session re-checks
|
|
1035
|
+
account availability while waiting for a cooldown to clear.
|
|
1036
|
+
|
|
1037
|
+
## [1.6.0] - 2026-06-13
|
|
1038
|
+
|
|
1039
|
+
### Added
|
|
1040
|
+
|
|
1041
|
+
- Persistent Pi footer status for the active Codex or Anthropic OAuth account,
|
|
1042
|
+
showing remaining 5-hour and 7-day allowance with reset countdowns.
|
|
1043
|
+
- `/multi-account limits [refresh]` (also `usage` and `quota`) for detailed
|
|
1044
|
+
active-account percentages, reset timestamps, plan, and Codex credits.
|
|
1045
|
+
- Provider usage caching keyed by credential fingerprint. Codex response
|
|
1046
|
+
headers refresh the cache without another request; direct usage calls are
|
|
1047
|
+
deduplicated and Anthropic polling is limited to at most once per 10 minutes.
|
|
1048
|
+
|
|
1049
|
+
## [1.5.0] - 2026-06-11
|
|
1050
|
+
|
|
1051
|
+
### Fixed
|
|
1052
|
+
|
|
1053
|
+
- Failover decisions now happen only on the final assistant error. Intermediate
|
|
1054
|
+
provider HTTP retries can contribute reset metadata but can no longer switch
|
|
1055
|
+
the active model or falsely blame the next account.
|
|
1056
|
+
- A physical 401 is counted once instead of once in each response, message, and
|
|
1057
|
+
agent hook. Version-3 one-year invalidations created by that bug are removed
|
|
1058
|
+
during state migration.
|
|
1059
|
+
- Continuations queued from `agent_end` now use Pi's required `followUp`
|
|
1060
|
+
delivery mode while the agent is still active.
|
|
1061
|
+
- Manual model selection no longer permanently disables failover when that
|
|
1062
|
+
selected model later returns a real final limit.
|
|
1063
|
+
- Explicit fallback lists and auto-discovery now share real-account
|
|
1064
|
+
deduplication. Codex slots use the stable `accountId` stored by Pi.
|
|
1065
|
+
- New logins that provably duplicate an existing real account are rejected, and
|
|
1066
|
+
already-present duplicate slots are reported and omitted from rotation.
|
|
1067
|
+
- A fallback whose `setModel()` has no usable authorization is invalidated and
|
|
1068
|
+
skipped without preventing the next candidate from being tried.
|
|
1069
|
+
- Anthropic OAuth request shaping now identifies as the locally installed
|
|
1070
|
+
Claude Code `2.1.172` instead of the stale `2.1.150` billing-header version.
|
|
1071
|
+
- Explicit provider verdicts such as `authentication token has been
|
|
1072
|
+
invalidated` now force-refresh the access token even before its local expiry.
|
|
1073
|
+
A permanently invalid refresh token removes the account and prints the
|
|
1074
|
+
interactive `/login` recovery steps.
|
|
1075
|
+
- Slash commands and shell shortcuts bypass the all-accounts-cooling input
|
|
1076
|
+
queue, so `/login` and other recovery commands remain usable.
|
|
1077
|
+
- Consecutive account failures in one continuation chain are handled
|
|
1078
|
+
independently; a previous switch no longer hides the next account's error.
|
|
1079
|
+
- Manual `/multi-account next` can deliberately probe the next account even
|
|
1080
|
+
when every fallback has a recorded cooldown, without arming an automatic
|
|
1081
|
+
continuation.
|
|
1082
|
+
|
|
1083
|
+
### Added
|
|
1084
|
+
|
|
1085
|
+
- Session-bound delayed resume: when every account is cooling down, an open Pi
|
|
1086
|
+
session retries at the earliest known recovery and continues the task.
|
|
1087
|
+
- `/multi-account stop` to abort and cancel the current failover/resume chain.
|
|
1088
|
+
- State-machine tests covering retry ordering, final-error deduplication,
|
|
1089
|
+
authoritative message providers, duplicate accounts, failed model selection,
|
|
1090
|
+
continuation caps, cancellation, migration, and delayed resume.
|
|
1091
|
+
|
|
1092
|
+
## [1.4.0] - 2026-06-10
|
|
1093
|
+
|
|
1094
|
+
### Fixed
|
|
1095
|
+
|
|
1096
|
+
- **A single 401 no longer drops an account that still has valid tokens.** A 401 on
|
|
1097
|
+
an OAuth account usually just means the access token needs a refresh (Pi refreshes
|
|
1098
|
+
on the next call). Previously the first 401 permanently invalidated the account
|
|
1099
|
+
(≈1-year cooldown until re-login) and yanked you onto another — often broken —
|
|
1100
|
+
account. Now a refreshable account is given a brief cooldown and retried; it is
|
|
1101
|
+
only marked dead after 3 consecutive 401s with no success in between. A
|
|
1102
|
+
non-refreshable (API-key) 401 is still treated as immediately fatal.
|
|
1103
|
+
- Any successful response clears that account's 401 streak.
|
|
1104
|
+
|
|
1105
|
+
### Added
|
|
1106
|
+
|
|
1107
|
+
- Tests for transient-401 tolerance, the consecutive-401 kill threshold, and
|
|
1108
|
+
success-resets-streak (suite now 17 tests).
|
|
1109
|
+
|
|
1110
|
+
## [1.3.0] - 2026-06-10
|
|
1111
|
+
|
|
1112
|
+
### Fixed
|
|
1113
|
+
|
|
1114
|
+
- **Manual model/account selection is now respected.** Picking a model (e.g. Opus
|
|
1115
|
+
on another account) no longer gets auto-yanked onto a different provider on the
|
|
1116
|
+
next rate limit — the failover stays put and tells you, until you switch with
|
|
1117
|
+
`/model` or `/multi-account next`. The pin auto-releases after a successful
|
|
1118
|
+
response on that provider.
|
|
1119
|
+
- **No more self-resurrecting work.** All background resume timers were removed:
|
|
1120
|
+
continuation now happens only synchronously inside an active turn, so Esc and
|
|
1121
|
+
quitting always stop it. When every account is rate-limited the failover STOPS
|
|
1122
|
+
and asks you to retry, instead of churning between exhausted accounts.
|
|
1123
|
+
- **No more "Agent is already processing" / "Cannot continue from message role:
|
|
1124
|
+
assistant".** Continuations are sent only when the agent is idle and not aborting.
|
|
1125
|
+
|
|
1126
|
+
### Added
|
|
1127
|
+
|
|
1128
|
+
- Test suite (`npm test`) covering the failover edge cases: limit/401 failover,
|
|
1129
|
+
all-accounts-exhausted stop, Esc/abort, manual-selection pinning, idle gating,
|
|
1130
|
+
Anthropic OAuth shaping idempotency, and session shutdown. Wired into CI.
|
|
1131
|
+
|
|
1132
|
+
## [1.2.0] - 2026-06-10
|
|
1133
|
+
|
|
1134
|
+
### Added
|
|
1135
|
+
|
|
1136
|
+
- **Anthropic (Claude Pro/Max) OAuth now works out of the box.** OAuth login is
|
|
1137
|
+
enabled on the base `anthropic` provider and on every `anthropic-account-*`
|
|
1138
|
+
alias, and outgoing Anthropic OAuth requests are shaped (billing header +
|
|
1139
|
+
system-prompt normalization) directly by this package. A separate
|
|
1140
|
+
`pi-anthropic-auth` install is no longer required.
|
|
1141
|
+
|
|
1142
|
+
### Changed
|
|
1143
|
+
|
|
1144
|
+
- Request shaping is idempotent and only touches OAuth-marked Anthropic requests,
|
|
1145
|
+
so it coexists safely with `pi-anthropic-auth` if both are installed, and leaves
|
|
1146
|
+
API-key Anthropic and OpenAI Codex / Qwen requests untouched.
|
|
1147
|
+
|
|
1148
|
+
### Credits
|
|
1149
|
+
|
|
1150
|
+
- Anthropic OAuth request-shaping logic vendored from
|
|
1151
|
+
[`gotgenes/pi-anthropic-auth`](https://github.com/gotgenes/pi-anthropic-auth) (MIT).
|
|
1152
|
+
|
|
1153
|
+
## [1.1.0] - 2026-06-10
|
|
1154
|
+
|
|
1155
|
+
### Fixed
|
|
1156
|
+
|
|
1157
|
+
- **Runaway failover loop that could freeze the machine.** When every account was
|
|
1158
|
+
rate-limited the rotation ping-ponged between accounts every 1–9s indefinitely,
|
|
1159
|
+
growing session history until the system swapped itself to death. The
|
|
1160
|
+
auto-continue counter was reset on every agent start, so `maxAutoContinuesPerPrompt`
|
|
1161
|
+
never actually bounded the loop. The counter is now reset only by a genuine new
|
|
1162
|
+
user prompt, making the cap a real per-task limit.
|
|
1163
|
+
- **Escape did not stop the loop.** Auto-continuation ran from background event
|
|
1164
|
+
hooks and a timer, so cancelling the agent was immediately undone. User aborts
|
|
1165
|
+
(`stopReason: "aborted"` / `ctx.signal`) now stop the chain and cancel all timers.
|
|
1166
|
+
|
|
1167
|
+
### Added
|
|
1168
|
+
|
|
1169
|
+
- Anti-ping-pong guard: immediate failover only switches to an account usable right
|
|
1170
|
+
now and never bounces straight back to the account it just left within 60s.
|
|
1171
|
+
- Minimum 15s spacing between auto-continuations (no tight CPU/network loop, and a
|
|
1172
|
+
real window for Esc to take effect).
|
|
1173
|
+
- In-session auto-resume: when the whole fallback circle is exhausted, the extension
|
|
1174
|
+
waits and continues the agent's work as soon as any account recovers — for as long
|
|
1175
|
+
as the session stays open.
|
|
1176
|
+
|
|
1177
|
+
### Changed
|
|
1178
|
+
|
|
1179
|
+
- **Tight session binding.** Background activity is now scoped to the live session:
|
|
1180
|
+
ending or replacing a session (quit, reload, new, resume, fork) cancels all timers
|
|
1181
|
+
and drops any pending resume. A new session starts clean and never inherits a
|
|
1182
|
+
previous session's paused work; nothing survives once Pi exits.
|
|
1183
|
+
|
|
1184
|
+
## [1.0.0] - 2026-06-09
|
|
1185
|
+
|
|
1186
|
+
### Added
|
|
1187
|
+
|
|
1188
|
+
- Initial public release.
|
|
1189
|
+
- Automatic multi-account failover & rotation across Anthropic (Claude),
|
|
1190
|
+
OpenAI / ChatGPT Codex, and Qwen / Alibaba.
|
|
1191
|
+
- Auto-discovery of authenticated accounts from `~/.pi/agent/auth.json`; the
|
|
1192
|
+
rotation grows on login and drops accounts on logout, token expiry, or
|
|
1193
|
+
authorization errors.
|
|
1194
|
+
- Quota / rate-limit failover with provider-reset-aware cooldowns and circular
|
|
1195
|
+
fallback ordering.
|
|
1196
|
+
- Optional auto-continue of the interrupted task after a switch.
|
|
1197
|
+
- Thinking-level preservation across model switches.
|
|
1198
|
+
- Commands `/multi-account`, `/provider-failover`, `/failover` with
|
|
1199
|
+
`status | rediscover | add | next | reset | reload | enable | disable`.
|
|
1200
|
+
- Plaintext-free credential handling (SHA-256 fingerprints only); `0600`
|
|
1201
|
+
config/state files.
|
|
1202
|
+
|
|
1203
|
+
[1.6.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.6.0
|
|
1204
|
+
[1.5.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.5.0
|
|
1205
|
+
[1.4.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.4.0
|
|
1206
|
+
[1.3.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.3.0
|
|
1207
|
+
[1.2.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.2.0
|
|
1208
|
+
[1.1.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.1.0
|
|
1209
|
+
[1.0.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.0.0
|