@herbertgao/pi-extensions 2026.9.20 → 2026.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/README.md +13 -8
  2. package/THIRD_PARTY_NOTICES.md +29 -3
  3. package/node_modules/@herbertgao/pi-bark/README.md +39 -3
  4. package/node_modules/@herbertgao/pi-bark/package.json +1 -1
  5. package/node_modules/@herbertgao/pi-bark/src/index.ts +275 -15
  6. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +50 -50
  7. package/node_modules/@herbertgao/pi-cc-extensions/README.md +7 -7
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +25 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +24 -1
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -4
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +47 -5
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/mcp-chip.ts +136 -0
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +56 -11
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +36 -4
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +11 -2
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +303 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +1 -1
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +36 -0
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +2 -2
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +88 -34
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +6 -5
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -1
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +31 -6
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +3 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +173 -36
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +1 -0
  29. package/node_modules/@herbertgao/pi-cc-extensions/package.json +6 -4
  30. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  31. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  32. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +3 -2
  33. package/node_modules/@herbertgao/sol-pi/CONTRIBUTING.md +22 -0
  34. package/node_modules/@herbertgao/sol-pi/README.md +36 -17
  35. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +10 -6
  36. package/node_modules/@herbertgao/sol-pi/agents-install.md +15 -12
  37. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +24 -8
  38. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +2 -2
  39. package/node_modules/@herbertgao/sol-pi/docs/star-history.md +18 -0
  40. package/node_modules/@herbertgao/sol-pi/package.json +4 -2
  41. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +1 -1
  42. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +1 -1
  43. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +23 -4
  44. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +32 -1
  45. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +3 -2
  46. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +1 -1
  47. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +27 -7
  48. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +137 -41
  49. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +1 -0
  50. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +2 -1
  51. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/projection.ts +58 -0
  52. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +28 -6
  53. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +2 -1
  54. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +15 -2
  55. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  56. package/node_modules/@pi-plugins/fast-mode/README.md +7 -1
  57. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
  58. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +7 -1
  59. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  60. package/node_modules/@pi-plugins/fast-mode/package.json +5 -5
  61. package/node_modules/pi-antigravity/README.md +3 -0
  62. package/node_modules/pi-antigravity/package.json +8 -8
  63. package/node_modules/pi-antigravity/src/auth/oauth.ts +6 -3
  64. package/node_modules/pi-antigravity/src/index.ts +114 -110
  65. package/node_modules/pi-antigravity/src/stream/stream.ts +1 -1
  66. package/node_modules/pi-antigravity/src/utils/util.ts +18 -1
  67. package/node_modules/pi-multi-account/CHANGELOG.md +20 -0
  68. package/node_modules/pi-multi-account/README.md +46 -8
  69. package/node_modules/pi-multi-account/config-file-transaction.ts +42 -0
  70. package/node_modules/pi-multi-account/index.ts +255 -269
  71. package/node_modules/pi-multi-account/model-pins.ts +36 -0
  72. package/node_modules/pi-multi-account/package.json +9 -7
  73. package/node_modules/pi-multi-account/slot-proxy-auth.ts +12 -2
  74. package/node_modules/pi-multi-account/slot-proxy.ts +27 -5
  75. package/node_modules/pi-web-access/CHANGELOG.md +17 -0
  76. package/node_modules/pi-web-access/README.md +6 -3
  77. package/node_modules/pi-web-access/brave-rate-limit.ts +170 -0
  78. package/node_modules/pi-web-access/brave.ts +28 -11
  79. package/node_modules/pi-web-access/dist/index.js +202 -33
  80. package/node_modules/pi-web-access/gemini-search.ts +2 -5
  81. package/node_modules/pi-web-access/index.ts +13 -7
  82. package/node_modules/pi-web-access/openai-search.ts +24 -10
  83. package/node_modules/pi-web-access/package.json +1 -1
  84. package/node_modules/pi-web-access/summary-review.ts +8 -2
  85. package/node_modules/{@herbertgao/resume-from → resume-from}/README.md +16 -12
  86. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.d.ts +0 -5
  87. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/adapter.js +2 -3
  88. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.d.ts +2 -2
  89. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.d.ts +3 -0
  90. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/reader.js +27 -15
  91. package/node_modules/{@herbertgao/resume-from/dist/adapters/pi → resume-from/dist/adapters/claude-code}/redaction.js +16 -3
  92. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.d.ts +2 -4
  93. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/writer.js +1 -1
  94. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.d.ts +2 -4
  95. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.js +1 -2
  96. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/read.d.ts +16 -0
  97. package/node_modules/resume-from/dist/adapters/codex/read.js +723 -0
  98. package/node_modules/{@herbertgao/resume-from/dist/adapters/claude-code → resume-from/dist/adapters/codex}/redaction.js +16 -3
  99. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.d.ts +44 -4
  100. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/rollout.js +88 -4
  101. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.d.ts +2 -2
  102. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/write.js +2 -2
  103. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.d.ts +5 -1
  104. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.js +10 -6
  105. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.d.ts +2 -2
  106. package/node_modules/{@herbertgao/resume-from/dist/adapters/codex → resume-from/dist/adapters/pi}/redaction.js +16 -3
  107. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.d.ts +2 -4
  108. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/serialize.js +2 -2
  109. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.d.ts +1 -1
  110. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/render.js +4 -4
  111. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.js +2 -2
  112. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.d.ts +2 -2
  113. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.js +31 -8
  114. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.d.ts +1 -1
  115. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/wiring.js +1 -2
  116. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.d.ts +4 -1
  117. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/confirmation.js +2 -2
  118. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.d.ts +8 -6
  119. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.d.ts +10 -5
  120. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/finder.d.ts +2 -1
  121. package/node_modules/resume-from/dist/import/discovery/finder.js +269 -0
  122. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.d.ts +3 -3
  123. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.js +3 -3
  124. package/node_modules/resume-from/dist/import/pipeline.d.ts +28 -0
  125. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/pipeline.js +31 -11
  126. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.d.ts +2 -2
  127. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/builder.js +9 -5
  128. package/node_modules/resume-from/dist/import/preview/warnings.d.ts +14 -0
  129. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/warnings.js +26 -20
  130. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.js +12 -2
  131. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.d.ts +9 -3
  132. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.d.ts +4 -4
  133. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/git.js +7 -4
  134. package/node_modules/resume-from/dist/platform/repo/reader.d.ts +3 -0
  135. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/reader.js +60 -20
  136. package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.d.ts +8 -1
  137. package/node_modules/{@herbertgao/resume-from → resume-from}/package.json +19 -32
  138. package/node_modules/{@herbertgao/resume-from → resume-from}/shims/pi/extensions/resume-from.js +2 -2
  139. package/package.json +17 -17
  140. package/node_modules/@herbertgao/resume-from/dist/adapters/codex/read.js +0 -270
  141. package/node_modules/@herbertgao/resume-from/dist/import/discovery/finder.js +0 -129
  142. package/node_modules/@herbertgao/resume-from/dist/import/pipeline.d.ts +0 -17
  143. package/node_modules/@herbertgao/resume-from/dist/import/preview/warnings.d.ts +0 -11
  144. package/node_modules/@herbertgao/resume-from/dist/platform/repo/reader.d.ts +0 -8
  145. /package/node_modules/{@herbertgao/resume-from → resume-from}/LICENSE +0 -0
  146. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/contract.js +0 -0
  147. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.d.ts +0 -0
  148. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/entries.js +0 -0
  149. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.d.ts +0 -0
  150. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/index.js +0 -0
  151. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.d.ts +0 -0
  152. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/layout.js +0 -0
  153. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.d.ts +0 -0
  154. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/readback.js +0 -0
  155. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/redaction.d.ts +0 -0
  156. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.d.ts +0 -0
  157. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/claude-code/validation.js +0 -0
  158. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/contract.js +0 -0
  159. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/index.d.ts +0 -0
  160. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.d.ts +0 -0
  161. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/readback.js +0 -0
  162. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/redaction.d.ts +0 -0
  163. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.d.ts +0 -0
  164. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/codex/validation.js +0 -0
  165. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/contract.js +0 -0
  166. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/adapter.d.ts +0 -0
  167. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/contract.js +0 -0
  168. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.d.ts +0 -0
  169. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/format.js +0 -0
  170. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.d.ts +0 -0
  171. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/index.js +0 -0
  172. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.d.ts +0 -0
  173. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/parse.js +0 -0
  174. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/redaction.d.ts +0 -0
  175. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.d.ts +0 -0
  176. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/adapters/pi/validate.js +0 -0
  177. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.d.ts +0 -0
  178. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/bin.js +0 -0
  179. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.d.ts +0 -0
  180. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/contract.js +0 -0
  181. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.d.ts +0 -0
  182. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/agents.js +0 -0
  183. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.d.ts +0 -0
  184. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/args.js +0 -0
  185. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.d.ts +0 -0
  186. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/contract.js +0 -0
  187. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.d.ts +0 -0
  188. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/index.js +0 -0
  189. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.d.ts +0 -0
  190. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/presentation.js +0 -0
  191. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/cli/runner.d.ts +0 -0
  192. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/contract.js +0 -0
  193. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.d.ts +0 -0
  194. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/entry.js +0 -0
  195. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.d.ts +0 -0
  196. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/index.js +0 -0
  197. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/command.d.ts +0 -0
  198. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.d.ts +0 -0
  199. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/contract.js +0 -0
  200. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.d.ts +0 -0
  201. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/index.js +0 -0
  202. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.d.ts +0 -0
  203. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/picker.js +0 -0
  204. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.d.ts +0 -0
  205. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/presentation.js +0 -0
  206. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.d.ts +0 -0
  207. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/register.js +0 -0
  208. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.d.ts +0 -0
  209. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/pi-extension/ui.js +0 -0
  210. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.d.ts +0 -0
  211. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/profile.js +0 -0
  212. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.d.ts +0 -0
  213. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/host/registry.js +0 -0
  214. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/contract.js +0 -0
  215. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/contract.js +0 -0
  216. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.d.ts +0 -0
  217. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/errors.js +0 -0
  218. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.d.ts +0 -0
  219. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/homes.js +0 -0
  220. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.d.ts +0 -0
  221. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/index.js +0 -0
  222. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.d.ts +0 -0
  223. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/discovery/ordering.js +0 -0
  224. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.d.ts +0 -0
  225. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/errors.js +0 -0
  226. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.d.ts +0 -0
  227. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/index.js +0 -0
  228. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/contract.js +0 -0
  229. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.d.ts +0 -0
  230. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/errors.js +0 -0
  231. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.d.ts +0 -0
  232. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/handover.js +0 -0
  233. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.d.ts +0 -0
  234. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/index.js +0 -0
  235. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/lander.d.ts +0 -0
  236. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.d.ts +0 -0
  237. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/landing/marker.js +0 -0
  238. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.d.ts +0 -0
  239. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/contract.js +0 -0
  240. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.d.ts +0 -0
  241. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/format.js +0 -0
  242. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.d.ts +0 -0
  243. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/preview/index.js +0 -0
  244. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.d.ts +0 -0
  245. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/contract.js +0 -0
  246. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.d.ts +0 -0
  247. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/index.js +0 -0
  248. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.d.ts +0 -0
  249. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/transfer/rules.js +0 -0
  250. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/import/wiring.d.ts +0 -0
  251. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.d.ts +0 -0
  252. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/index.js +0 -0
  253. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.d.ts +0 -0
  254. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/contract.js +0 -0
  255. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.d.ts +0 -0
  256. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/defaults.js +0 -0
  257. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.d.ts +0 -0
  258. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/errors.js +0 -0
  259. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.d.ts +0 -0
  260. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/index.js +0 -0
  261. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.d.ts +0 -0
  262. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/loader.js +0 -0
  263. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.d.ts +0 -0
  264. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/paths.js +0 -0
  265. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.d.ts +0 -0
  266. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/config/validate.js +0 -0
  267. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/contract.js +0 -0
  268. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.d.ts +0 -0
  269. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/repo/index.js +0 -0
  270. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.d.ts +0 -0
  271. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/contract.js +0 -0
  272. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.d.ts +0 -0
  273. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/file-committer.js +0 -0
  274. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.d.ts +0 -0
  275. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/store/index.js +0 -0
  276. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.d.ts +0 -0
  277. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/contract.js +0 -0
  278. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.d.ts +0 -0
  279. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/estimator.js +0 -0
  280. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.d.ts +0 -0
  281. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/platform/tokens/index.js +0 -0
  282. /package/node_modules/{@herbertgao/resume-from → resume-from}/dist/session/contract.js +0 -0
@@ -28,6 +28,8 @@ import { AsyncLocalStorage } from "node:async_hooks";
28
28
  import { createPayloadStream } from "./provider-payload-stream.ts";
29
29
  import { mutateProxyAuth } from "./auth-file-transaction.ts";
30
30
  import { mergeStateDeltas, mutateStateFile } from "./state-file-transaction.ts";
31
+ import { normalizeModelPins, persistModelPin, validModelPinScope } from "./model-pins.ts";
32
+ import { mutateConfigFile } from "./config-file-transaction.ts";
31
33
  import { createHash } from "node:crypto";
32
34
  import {
33
35
  accessSync,
@@ -46,7 +48,8 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
46
48
  import { homedir } from "node:os";
47
49
  import { Readable } from "node:stream";
48
50
  import { dirname, join } from "node:path";
49
- import { createRequire } from "node:module";
51
+ import { getModel as getHostModel } from "@earendil-works/pi-ai/compat";
52
+ import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
50
53
  import { fileURLToPath } from "node:url";
51
54
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
52
55
  import {
@@ -120,33 +123,15 @@ import {
120
123
  } from "./context-guard.ts";
121
124
 
122
125
  // ---------------------------------------------------------------------------
123
- // pi-ai OAuth bridge (version-agnostic)
126
+ // Host-bound pi-ai OAuth bridge
124
127
  // ---------------------------------------------------------------------------
125
- //
126
- // This is the single most fragile boundary in the extension, and it has now broken
127
- // twice in the field:
128
- //
129
- // * pi-coding-agent's extension loader aliases `@earendil-works/pi-ai/oauth` to an
130
- // empty stub in its own node_modules, so a plain static import yields undefined —
131
- // the `undefined is not an object (evaluating
132
- // '_oauth.openaiCodexOAuthProvider.usesCallbackServer')` crash at startup.
133
- // * pi-ai 0.80 REMOVED the runtime OAuth surface: `dist/oauth.js` is now literally
134
- // `export {}` (types only), `getModel` moved to `dist/compat.js`, and the OAuth
135
- // implementations live behind provider factories
136
- // (`anthropicProvider().auth.oauth`) with a new `login(interaction)` /
137
- // `refresh(credential)` shape.
138
- //
139
- // So we resolve pi-ai on disk ourselves and normalize BOTH eras behind one internal
140
- // surface. Everything here is best-effort: a pi-ai we cannot adapt degrades to
141
- // "subscription logins unavailable" and the extension still loads, so API-key
142
- // accounts keep rotating instead of the whole extension dying at load time.
143
- //
144
- // The 0.80+ adaptation follows the approach contributed by @lfoscari in PR #4.
145
-
128
+ // Pi's extension loader binds these public imports to the running host SDK.
129
+ // Never probe a private SDK copy: its provider catalog/auth may differ from Pi's.
130
+ // Supported Pi versions use provider factories; legacy callbacks are adapted below.
146
131
  /** Normalized OAuth surface — identical for every supported pi-ai version. */
147
132
  type PiAiOauthBridge = {
148
133
  /** pi-ai release line this was adapted from, for the debug log. */
149
- era: "legacy-oauth-entry" | "provider-factories";
134
+ era: "provider-factories";
150
135
  anthropic: {
151
136
  login: (callbacks: any) => Promise<any>;
152
137
  refresh: (credentials: any, signal: AbortSignal) => Promise<any>;
@@ -174,57 +159,6 @@ type PiAiOauthBridge = {
174
159
 
175
160
  let piAiOauthBridge: PiAiOauthBridge | undefined;
176
161
  let piAiOauthLoadError: string | undefined;
177
- let piAiPackageRoot: string | undefined | null; // null = searched, not found
178
-
179
- /**
180
- * Locate the `@earendil-works/pi-ai` package directory, nearest-first.
181
- *
182
- * A git checkout keeps pi-ai in the extension's OWN node_modules, but `npm i` /
183
- * `pi install` HOIST it next to the package instead
184
- * (`~/.pi/agent/npm/node_modules/pi-multi-account` +
185
- * `~/.pi/agent/npm/node_modules/@earendil-works/pi-ai`), and pnpm hides it behind a
186
- * symlinked store. Probing only the nested path made the extension fail to load on
187
- * every hoisted install, so walk ancestors the way Node's own resolver does and fall
188
- * back to require.resolve().
189
- */
190
- export function piAiRootCandidates(
191
- fromFile: string,
192
- resolver?: (specifier: string) => string,
193
- ): string[] {
194
- const candidates: string[] = [];
195
- let dir = dirname(fromFile);
196
- for (let depth = 0; depth < 16; depth++) {
197
- candidates.push(join(dir, "node_modules", "@earendil-works", "pi-ai"));
198
- const parent = dirname(dir);
199
- if (parent === dir) break;
200
- dir = parent;
201
- }
202
- if (resolver) {
203
- try {
204
- // Resolves through "exports"/symlinks even when no ancestor path matched.
205
- candidates.push(dirname(resolver("@earendil-works/pi-ai/package.json")));
206
- } catch {
207
- // Not resolvable from here — the ancestor paths above may still hit.
208
- }
209
- }
210
- return [...new Set(candidates)];
211
- }
212
-
213
- function findPiAiRoot(): string | undefined {
214
- if (piAiPackageRoot !== undefined) return piAiPackageRoot ?? undefined;
215
- const here = fileURLToPath(import.meta.url);
216
- const localRequire = createRequire(import.meta.url);
217
- for (const candidate of piAiRootCandidates(here, (specifier) =>
218
- localRequire.resolve(specifier),
219
- )) {
220
- if (existsSync(join(candidate, "package.json"))) {
221
- piAiPackageRoot = candidate;
222
- return candidate;
223
- }
224
- }
225
- piAiPackageRoot = null;
226
- return undefined;
227
- }
228
162
 
229
163
  /**
230
164
  * Bridge Pi's legacy extension OAuth callbacks (`onAuth`/`onDeviceCode`/`onPrompt`/
@@ -272,48 +206,15 @@ function toAuthInteraction(callbacks: any) {
272
206
  };
273
207
  }
274
208
 
275
- /** pi-ai <= 0.79: runtime OAuth helpers exported straight from `dist/oauth.js`. */
276
- function adaptLegacyOauthEntry(mod: any): PiAiOauthBridge | undefined {
277
- const codex = mod?.openaiCodexOAuthProvider;
278
- if (
279
- typeof mod?.loginAnthropic !== "function" ||
280
- typeof mod?.refreshAnthropicToken !== "function" ||
281
- typeof codex?.login !== "function" ||
282
- typeof codex?.refreshToken !== "function"
283
- ) {
284
- return undefined;
285
- }
286
- return {
287
- era: "legacy-oauth-entry",
288
- anthropic: {
289
- login: (callbacks) => mod.loginAnthropic(callbacks),
290
- // This era exchanges the bare refresh token, not the whole credential.
291
- refresh: (credentials, signal) =>
292
- mod.refreshAnthropicToken(credentials.refresh, signal),
293
- },
294
- codex: {
295
- usesCallbackServer: codex.usesCallbackServer ?? true,
296
- login: (callbacks) => codex.login(callbacks),
297
- refresh: (credentials, signal) => codex.refreshToken(credentials, signal),
298
- getApiKey: (credentials) =>
299
- typeof codex.getApiKey === "function"
300
- ? codex.getApiKey(credentials)
301
- : credentials.access,
302
- },
303
- };
304
- }
305
209
 
306
210
  /** pi-ai >= 0.80: OAuth lives behind provider factories, with an AuthInteraction API. */
307
- function adaptProviderFactories(
308
- anthropicMod: any,
309
- codexMod: any,
310
- kimiMod?: any,
311
- xaiMod?: any,
312
- ): PiAiOauthBridge | undefined {
313
- const anthropicOauth = anthropicMod?.anthropicProvider?.()?.auth?.oauth;
314
- const codexOauth = codexMod?.openaiCodexProvider?.()?.auth?.oauth;
315
- const kimiOauth = kimiMod?.kimiCodingProvider?.()?.auth?.oauth;
316
- const xaiOauth = xaiMod?.xaiProvider?.()?.auth?.oauth;
211
+ function adaptProviderFactories(): PiAiOauthBridge | undefined {
212
+ const providers = builtinProviders();
213
+ const oauth = (id: string): any => providers.find((provider) => provider.id === id)?.auth?.oauth;
214
+ const anthropicOauth = oauth("anthropic");
215
+ const codexOauth = oauth("openai-codex");
216
+ const kimiOauth = oauth("kimi-coding");
217
+ const xaiOauth = oauth("xai");
317
218
  if (
318
219
  typeof anthropicOauth?.login !== "function" ||
319
220
  typeof anthropicOauth?.refresh !== "function" ||
@@ -355,58 +256,18 @@ function adaptProviderFactories(
355
256
  };
356
257
  }
357
258
 
358
- /**
359
- * Best-effort load + normalization of pi-ai's OAuth surface. NEVER throws.
360
- *
361
- * Uses a synchronous require() (Node >= 22 supports require() of ESM without
362
- * top-level await, and pi-ai qualifies) because providers are registered
363
- * synchronously — `usesCallbackServer` is read while Pi merely LISTS providers.
364
- */
259
+ /** Adapt the running host's public factories without loading another SDK instance. */
365
260
  function tryLoadPiAiOauth(): PiAiOauthBridge | undefined {
366
261
  if (piAiOauthBridge) return piAiOauthBridge;
367
- const root = findPiAiRoot();
368
- if (!root) {
369
- piAiOauthLoadError = `@earendil-works/pi-ai was not found near ${fileURLToPath(import.meta.url)}. Install @earendil-works/pi-ai alongside pi-multi-account.`;
370
- return undefined;
371
- }
372
- const localRequire = createRequire(import.meta.url);
373
- const load = (relative: string): any => {
374
- const file = join(root, "dist", relative);
375
- if (!existsSync(file)) return undefined;
376
- try {
377
- return localRequire(file);
378
- } catch (error) {
379
- tried.push(
380
- `${relative}: ${error instanceof Error ? error.message : String(error)}`,
381
- );
382
- return undefined;
383
- }
384
- };
385
- const tried: string[] = [];
386
-
387
- const legacy = adaptLegacyOauthEntry(load("oauth.js"));
388
- if (legacy) {
389
- piAiOauthBridge = legacy;
390
- piAiOauthLoadError = undefined;
391
- return legacy;
392
- }
393
- tried.push("dist/oauth.js exports no runtime OAuth helpers (pi-ai >= 0.80)");
394
-
395
- const modern = adaptProviderFactories(
396
- load(join("providers", "anthropic.js")),
397
- load(join("providers", "openai-codex.js")),
398
- load(join("providers", "kimi-coding.js")),
399
- load(join("providers", "xai.js")),
400
- );
401
- if (modern) {
402
- piAiOauthBridge = modern;
403
- piAiOauthLoadError = undefined;
404
- return modern;
262
+ try {
263
+ piAiOauthBridge = adaptProviderFactories();
264
+ piAiOauthLoadError = piAiOauthBridge
265
+ ? undefined
266
+ : "the running Pi host exposes no usable pi-ai OAuth provider factories";
267
+ } catch (error) {
268
+ piAiOauthLoadError = error instanceof Error ? error.message : String(error);
405
269
  }
406
- tried.push("dist/providers/{anthropic,openai-codex}.js exposed no auth.oauth");
407
-
408
- piAiOauthLoadError = `the @earendil-works/pi-ai at ${root} exposes no OAuth surface this extension can use. ${tried.join("; ")}`;
409
- return undefined;
270
+ return piAiOauthBridge;
410
271
  }
411
272
 
412
273
  /** Load reason, or undefined when OAuth is available. */
@@ -425,36 +286,10 @@ function requirePiAiOauth(): PiAiOauthBridge {
425
286
  return bridge;
426
287
  }
427
288
 
428
- /**
429
- * pi-ai's static model catalog lookup. It lived on the package root until 0.79 and
430
- * moved to `dist/compat.js` in 0.80, so resolve it lazily from whichever is present
431
- * — and treat "absent" as "no canonical metadata", never as a load failure.
432
- */
433
- let piAiGetModelFn: ((provider: string, id: string) => any) | null | undefined;
434
-
289
+ /** Canonical chat metadata comes from the same SDK catalog as the running host. */
435
290
  function piAiGetModel(provider: string, id: string): any {
436
- if (piAiGetModelFn === undefined) {
437
- piAiGetModelFn = null;
438
- const root = findPiAiRoot();
439
- if (root) {
440
- const localRequire = createRequire(import.meta.url);
441
- for (const relative of ["compat.js", "index.js"]) {
442
- const file = join(root, "dist", relative);
443
- if (!existsSync(file)) continue;
444
- try {
445
- const candidate = localRequire(file)?.getModel;
446
- if (typeof candidate === "function") {
447
- piAiGetModelFn = candidate;
448
- break;
449
- }
450
- } catch {
451
- // Try the next entry point.
452
- }
453
- }
454
- }
455
- }
456
291
  try {
457
- return piAiGetModelFn?.(provider, id);
292
+ return getHostModel(provider as any, id as any);
458
293
  } catch {
459
294
  return undefined;
460
295
  }
@@ -653,6 +488,9 @@ type ProviderFailoverConfig = {
653
488
  // { "openai-codex": ["gpt-5.6", "gpt-5.5"], "anthropic": ["claude-opus-4-9"] }.
654
489
  // Keys: anthropic | openai-codex | cursor | qwen | ollama.
655
490
  preferredModels?: Record<string, string[]>;
491
+ // Explicit per-account-group model preference. It never selects a foreground model,
492
+ // bypasses a cooldown, or authorizes an automatic quality-tier change.
493
+ pinnedModels?: Record<string, string>;
656
494
  // Forward-progress watchdog tunables (see constants above). 0/absent ⇒ built-in default.
657
495
  resumeIdleTimeoutMs?: number;
658
496
  stuckWatchdogMs?: number;
@@ -702,6 +540,7 @@ type RuntimeConfig = Required<
702
540
  | "preferLatestModel"
703
541
  | "reasoningLevel"
704
542
  | "preferredModels"
543
+ | "pinnedModels"
705
544
  | "resumeIdleTimeoutMs"
706
545
  | "stuckWatchdogMs"
707
546
  | "compactionWatchdogMs"
@@ -830,6 +669,7 @@ function configuredModelIds(provider: string): string[] {
830
669
  const models = parsed?.providers?.[provider]?.models;
831
670
  if (Array.isArray(models)) {
832
671
  return models
672
+ .filter((model: any) => !model?.type || model.type === "chat")
833
673
  .map((model: any) => (typeof model === "string" ? model : model?.id))
834
674
  .filter((id: unknown): id is string => typeof id === "string" && id.length > 0);
835
675
  }
@@ -840,6 +680,21 @@ function configuredModelIds(provider: string): string[] {
840
680
  }
841
681
  }
842
682
 
683
+ // Pi 0.99's provider composer replaces a supplied models array in full. Preserve
684
+ // non-chat definitions verbatim; they must never become account chat aliases.
685
+ function configuredOperationModels(provider: string): any[] {
686
+ try {
687
+ const parsed = JSON.parse(readFileSync(MODELS_CONFIG_PATH, "utf8"));
688
+ const models = parsed?.providers?.[provider]?.models;
689
+ return Array.isArray(models)
690
+ ? models.filter((model: any) => model && typeof model === "object" &&
691
+ (model.type === "image" || model.type === "classifier"))
692
+ : [];
693
+ } catch {
694
+ return [];
695
+ }
696
+ }
697
+
843
698
  function readHostDefaultModel(): { provider: string; id: string } | undefined {
844
699
  try {
845
700
  const parsed = JSON.parse(readFileSync(SETTINGS_PATH, "utf8"));
@@ -1195,7 +1050,7 @@ const ANTI_PINGPONG_MS = 60 * 1000; // don't switch straight back to the account
1195
1050
  // Bumped on every release. Printed at startup and in `/multi-account status` so you can verify
1196
1051
  // which version Pi actually loaded (a running Pi keeps the version it started with — /login and
1197
1052
  // /reload do NOT reload extension code; only a full restart does).
1198
- const VERSION = "1.23.2";
1053
+ const VERSION = "1.24.0";
1199
1054
  function sourceFingerprint(): string {
1200
1055
  try {
1201
1056
  const root = dirname(fileURLToPath(import.meta.url));
@@ -1816,6 +1671,7 @@ const DEFAULT_CONFIG: ProviderFailoverConfig = {
1816
1671
  preferLatestModel: true,
1817
1672
  reasoningLevel: "auto",
1818
1673
  preferredModels: {},
1674
+ pinnedModels: {},
1819
1675
  resumeIdleTimeoutMs: RESUME_IDLE_TIMEOUT_MS,
1820
1676
  stuckWatchdogMs: STUCK_WATCHDOG_MS,
1821
1677
  compactionWatchdogMs: COMPACTION_WATCHDOG_MS,
@@ -1828,11 +1684,12 @@ const DEFAULT_CONFIG: ProviderFailoverConfig = {
1828
1684
  function ensureDefaultConfig() {
1829
1685
  if (existsSync(CONFIG_PATH)) return;
1830
1686
  mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
1831
- writeFileSync(
1832
- CONFIG_PATH,
1833
- `${JSON.stringify(DEFAULT_CONFIG, null, "\t")}\n`,
1834
- { encoding: "utf8", mode: 0o600 },
1835
- );
1687
+ try {
1688
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(DEFAULT_CONFIG, null, "\t")}\n`,
1689
+ { encoding: "utf8", mode: 0o600, flag: "wx" });
1690
+ } catch (error: any) {
1691
+ if (error?.code !== "EEXIST") throw error; // Another root initialized it first.
1692
+ }
1836
1693
  }
1837
1694
 
1838
1695
  function positiveOr(value: unknown, fallback: number) {
@@ -2036,6 +1893,7 @@ function normalizeConfig(raw: ProviderFailoverConfig): RuntimeConfig {
2036
1893
  .filter(([, v]) => v.length > 0),
2037
1894
  )
2038
1895
  : {},
1896
+ pinnedModels: normalizeModelPins(raw.pinnedModels),
2039
1897
  resumeIdleTimeoutMs: positiveOr(
2040
1898
  raw.resumeIdleTimeoutMs,
2041
1899
  RESUME_IDLE_TIMEOUT_MS,
@@ -2287,6 +2145,20 @@ export async function persistRefreshedCredentials(
2287
2145
  write?: (data: Record<string, any>) => void;
2288
2146
  },
2289
2147
  ): Promise<boolean> {
2148
+ // Even legacy/set-only hosts must never unhide a numbered slot while saving a refresh.
2149
+ if (!io && isChildFacingPlaceholderForSlot(readAuthFileRaw()[provider], provider)) {
2150
+ try {
2151
+ let persisted = false;
2152
+ mutateProxyAuth(AUTH_PATH, PROXY_OAUTH_PATH, (auth, sidecar) => {
2153
+ if (!isChildFacingPlaceholderForSlot(auth[provider], provider) || sidecar[provider]?.type !== "oauth") {
2154
+ return { auth, sidecar, changed: false };
2155
+ }
2156
+ persisted = true;
2157
+ return { auth, sidecar: { ...sidecar, [provider]: credential }, changed: true };
2158
+ }, "shadow");
2159
+ return persisted;
2160
+ } catch { return false; }
2161
+ }
2290
2162
  if (typeof authStorage?.modify === "function") {
2291
2163
  try {
2292
2164
  await authStorage.modify(provider, () => credential);
@@ -2371,6 +2243,17 @@ function jwtExpMs(token: string): number | undefined {
2371
2243
  : undefined;
2372
2244
  }
2373
2245
 
2246
+ /**
2247
+ * When an OAuth access token expires. Anthropic's `sk-ant-oat…` tokens are opaque, so the JWT
2248
+ * `exp` is absent and the stored `expires` (seconds or milliseconds) is the only signal.
2249
+ */
2250
+ function oauthExpiryMs(entry: AuthEntry): number | undefined {
2251
+ const fromJwt = typeof entry.access === "string" ? jwtExpMs(entry.access) : undefined;
2252
+ if (fromJwt !== undefined) return fromJwt;
2253
+ if (typeof entry.expires !== "number" || !Number.isFinite(entry.expires)) return undefined;
2254
+ return entry.expires < 10_000_000_000 ? entry.expires * 1000 : entry.expires;
2255
+ }
2256
+
2374
2257
  function getCursorSubFromAccessToken(token: string): string | undefined {
2375
2258
  const sub = decodeJwtPayload(token)?.sub;
2376
2259
  return typeof sub === "string" && sub.length > 0 ? sub : undefined;
@@ -2501,13 +2384,7 @@ function isEntryUsable(entry: AuthEntry | undefined): boolean {
2501
2384
  if (typeof entry.access !== "string" || entry.access.length === 0)
2502
2385
  return false;
2503
2386
  // Expired access token with no refresh token → unrecoverable.
2504
- const storedExpiry =
2505
- typeof entry.expires === "number" && Number.isFinite(entry.expires)
2506
- ? entry.expires < 10_000_000_000
2507
- ? entry.expires * 1000
2508
- : entry.expires
2509
- : undefined;
2510
- const expMs = jwtExpMs(entry.access) ?? storedExpiry;
2387
+ const expMs = oauthExpiryMs(entry);
2511
2388
  if (
2512
2389
  expMs !== undefined &&
2513
2390
  expMs <= Date.now() &&
@@ -3072,7 +2949,7 @@ function registerCodexCatalog(
3072
2949
  baseUrl,
3073
2950
  api: "openai-codex-responses" as any,
3074
2951
  oauth: codexOAuthOverride(id, name),
3075
- models: models as any,
2952
+ models: [...models, ...configuredOperationModels(id)] as any,
3076
2953
  });
3077
2954
  }
3078
2955
 
@@ -3548,7 +3425,7 @@ const MINIMAL_ANTHROPIC_OAUTH_PROMPT = [
3548
3425
  ].join("\n");
3549
3426
  const CLAUDE_CODE_IDENTITY_PREFIX =
3550
3427
  "You are Claude Code, Anthropic's official CLI";
3551
- const CLAUDE_CODE_VERSION = "2.1.280";
3428
+ const CLAUDE_CODE_VERSION = "2.1.288";
3552
3429
  const BILLING_HEADER_SALT = "59cf53e54c78";
3553
3430
  const BILLING_HEADER_POSITIONS = [4, 7, 20] as const;
3554
3431
  const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
@@ -3925,10 +3802,8 @@ interface HostCompletionRouterAPI {
3925
3802
  export default function piMultiAccount(pi: ExtensionAPI) {
3926
3803
  // Warm up the OAuth helpers before any provider registration: providers are
3927
3804
  // registered synchronously below and their `usesCallbackServer`/`getApiKey`
3928
- // read from the cached module. Deliberately NON-fatal — if pi-ai's oauth entry
3929
- // cannot be resolved (hoisted npm layout, incompatible pi-ai), the extension
3930
- // still loads and every non-OAuth account keeps working; only subscription
3931
- // logins are unavailable, and the user is told once at session start.
3805
+ // use the cached host adapter. An unusable OAuth factory remains non-fatal:
3806
+ // API-key accounts keep working and session start reports unavailable OAuth.
3932
3807
  const oauthUnavailable = piAiOauthUnavailableReason();
3933
3808
  let subagentChild = isSubagentChildProcess();
3934
3809
  let hostOwnsSessionModel = false;
@@ -4881,11 +4756,19 @@ export default function piMultiAccount(pi: ExtensionAPI) {
4881
4756
  let refreshed: AuthEntry | undefined;
4882
4757
  let persistedUnderLock = false;
4883
4758
  if (family === "anthropic") {
4884
- refreshed = await refreshAnthropicCredentials(
4885
- entry,
4886
- undefined,
4887
- provider,
4888
- );
4759
+ const refreshAnthropic = (current: AuthEntry) => refreshAnthropicCredentials(current, undefined, provider);
4760
+ if (typeof authStorage?.modify === "function") {
4761
+ refreshed = await refreshAndPersistWithStorageLock({
4762
+ provider, credentials: entry, authStorage,
4763
+ readLatest: () => readAuthFile()[provider],
4764
+ refresh: refreshAnthropic,
4765
+ isShadowed: stored => isChildFacingPlaceholderForSlot(stored, provider),
4766
+ persistShadowed: credential => writeProxyOAuthSidecar({ ...readProxyOAuthSidecar(), [provider]: credential }),
4767
+ });
4768
+ persistedUnderLock = true;
4769
+ } else {
4770
+ refreshed = await refreshAnthropic(entry);
4771
+ }
4889
4772
  } else if (family === "openai-codex") {
4890
4773
  const refreshCodex = async (current: AuthEntry) => {
4891
4774
  const next = mergeRefreshedCredentials(
@@ -5281,6 +5164,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
5281
5164
  return; // keep the previous (or empty) discovered set; configured ids still serve
5282
5165
  }
5283
5166
  const models = ollamaRotationModelIds().map((m) => ollamaModelDef(m, OLLAMA_BASE));
5167
+ models.push(...configuredOperationModels(OLLAMA_BASE));
5284
5168
  pi.registerProvider(OLLAMA_BASE, {
5285
5169
  name: "Ollama",
5286
5170
  baseUrl: OLLAMA_CLOUD_BASE_URL,
@@ -6682,16 +6566,37 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6682
6566
  return [last, ...defaults.filter((id) => id !== last)];
6683
6567
  }
6684
6568
 
6685
- // Rank of a model within its family's newest-first preferred order (0 = newest).
6686
- // Unknown models sort last. Used to keep the latest model across accounts, not
6687
- // just within one account, during fallback ranking.
6569
+ function pinnedModelForProvider(provider: string): string | undefined {
6570
+ const scope = accountGroup(provider, config.qwenProvider);
6571
+ return Object.hasOwn(config.pinnedModels, scope) ? config.pinnedModels[scope] : undefined;
6572
+ }
6573
+
6574
+ function pinQualityCompatible(model: any, source: any): boolean {
6575
+ const pin = pinnedModelForProvider(model.provider);
6576
+ const sourceBand = modelQualityBand(source?.id, source?.provider);
6577
+ return !pin || !sameModelIdentity(model.id, pin) || !sourceBand || modelQualityBand(model.id, model.provider) === sourceBand;
6578
+ }
6579
+
6580
+ function preferredModelsForProvider(provider: string): string[] {
6581
+ const order = familyPreferredModels(classifyProvider(provider, config.qwenProvider));
6582
+ const pin = pinnedModelForProvider(provider);
6583
+ return pin ? [pin, ...order.filter(id => !sameModelIdentity(id, pin))] : order;
6584
+ }
6585
+
6586
+ // Rank includes an explicit pin, but a pin is not evidence of a quality upgrade.
6587
+ // Unknown models sort last; identity/liveness/quality constraints still apply first.
6688
6588
  function modelRecencyRank(model: any): number {
6689
- const family = classifyProvider(model?.provider, config.qwenProvider);
6690
- const order = familyPreferredModels(family);
6589
+ const order = preferredModelsForProvider(String(model?.provider ?? ""));
6691
6590
  const idx = order.findIndex((id) => sameModelIdentity(id, model?.id));
6692
6591
  return idx < 0 ? Number.MAX_SAFE_INTEGER : idx;
6693
6592
  }
6694
6593
 
6594
+ // Virtual and non-chat operations belong to Pi, not account chat rotation.
6595
+ function isAccountChatModel(model: any): boolean {
6596
+ return !!model && model.api !== "pi-virtual" &&
6597
+ (!model.type || model.type === "chat");
6598
+ }
6599
+
6695
6600
  function resolveTargets(
6696
6601
  ctx: any,
6697
6602
  target: string,
@@ -6702,7 +6607,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6702
6607
  if (!parsed) return [];
6703
6608
  if (parsed.modelId) {
6704
6609
  const model = findModelIncludingHidden(ctx, parsed.provider, parsed.modelId);
6705
- return model ? [model] : [];
6610
+ return isAccountChatModel(model) ? [model] : [];
6706
6611
  }
6707
6612
 
6708
6613
  const family = classifyProvider(parsed.provider, config.qwenProvider);
@@ -6710,15 +6615,15 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6710
6615
  ? classifyProvider(currentModel.provider, config.qwenProvider)
6711
6616
  : undefined;
6712
6617
  const sameFamily = !!family && !!currentFamily && family === currentFamily;
6713
- // A per-family config override (preferredModels) wins, so a new flagship model can be
6714
- // pinned without a code change. Order is newest-first either way.
6715
- const preferred = familyPreferredModels(family);
6618
+ // A pin leads this account's model order without synthesizing a model or losing the
6619
+ // underlying preferredModels/catalog order when that pin is missing or ineligible.
6620
+ const preferred = preferredModelsForProvider(parsed.provider);
6716
6621
  const keepCurrent = sameFamily && currentModel?.id ? [currentModel.id] : [];
6717
6622
  // `preferredOnly` means "take this family's flagship, not an arbitrary model". A provider
6718
6623
  // outside the managed families has no flagship list at all, so honouring it literally
6719
6624
  // yields no candidate and the switch fails — which is what made an unmanaged account
6720
6625
  // unreachable by name. Fall back to what Pi knows for it.
6721
- const usePreferredOnly = preferredOnly && preferred.length > 0;
6626
+ const usePreferredOnly = preferredOnly && preferred.length > 0 && !pinnedModelForProvider(parsed.provider);
6722
6627
  const registryModels = usePreferredOnly
6723
6628
  ? []
6724
6629
  : [
@@ -6731,7 +6636,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6731
6636
  // once downgraded (e.g. gpt-5.4 after a momentary limit on gpt-5.5) is upgraded back to
6732
6637
  // the latest the moment it is available again — instead of carrying the old model forever.
6733
6638
  // Legacy mode keeps the current model first (never changes the model unless it has to).
6734
- const modelIds = config.preferLatestModel
6639
+ const modelIds = config.preferLatestModel || pinnedModelForProvider(parsed.provider)
6735
6640
  ? [...preferred, ...keepCurrent, ...registryModels]
6736
6641
  : [...keepCurrent, ...preferred, ...registryModels];
6737
6642
  const result: any[] = [];
@@ -6739,7 +6644,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6739
6644
  for (const modelId of modelIds) {
6740
6645
  if (seen.has(modelId)) continue;
6741
6646
  const model = findModelIncludingHidden(ctx, parsed.provider, modelId);
6742
- if (!model) {
6647
+ if (!isAccountChatModel(model)) {
6743
6648
  seen.add(modelId);
6744
6649
  continue;
6745
6650
  }
@@ -6817,6 +6722,8 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6817
6722
  const sourceBand = modelQualityBand(currentModel?.id, currentModel?.provider);
6818
6723
  const sourceExceptional = apexIdentity(currentModel?.id);
6819
6724
  models = models.filter((model: any) => {
6725
+ // A preference is not tier consent, including routed automatic compaction.
6726
+ if (!pinQualityCompatible(model, currentModel)) return false;
6820
6727
  const candidateBand = modelQualityBand(model.id, model.provider);
6821
6728
  const candidateExceptional = apexIdentity(model.id);
6822
6729
  // Cursor's Fable-named aliases are deliberately uncalibrated. A user may select one
@@ -6865,8 +6772,10 @@ export default function piMultiAccount(pi: ExtensionAPI) {
6865
6772
  // preferLatestModel still upgrades a known older sibling (gpt-5.4 → gpt-5.5)
6866
6773
  // on a healthy account. An unranked catalog leftover (claude-4-sonnet beating
6867
6774
  // grok only because it sorts first) must never steal the user's model.
6775
+ const pinRepresentative = sameModelIdentity(pinnedModelForProvider(flagshipPick.provider), flagshipPick.id);
6868
6776
  const upgrade =
6869
6777
  config.preferLatestModel &&
6778
+ !pinRepresentative &&
6870
6779
  idRank !== Number.MAX_SAFE_INTEGER &&
6871
6780
  flagRank < idRank;
6872
6781
  pick = upgrade ? flagshipPick : identityPick;
@@ -7116,6 +7025,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
7116
7025
  waitForQuotaRecovery?: boolean;
7117
7026
  } = {},
7118
7027
  ) {
7028
+ if (!isAccountChatModel(failedModel) || (ctx.model && !isAccountChatModel(ctx.model))) return false;
7119
7029
  if (!options.manual && (isFailoverExempt(failedModel?.provider) || isFailoverExempt(ctx.model?.provider))) return false;
7120
7030
  const switchEpoch = chainEpoch;
7121
7031
  if (!automaticFailoverEnabled() || !failedModel?.provider || !failedModel?.id)
@@ -7376,6 +7286,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
7376
7286
  }
7377
7287
 
7378
7288
  async function ensureReadyModel(ctx: any, reason: string) {
7289
+ if (ctx.model && !isAccountChatModel(ctx.model)) return true;
7379
7290
  // The launch candidate is authoritative in a pi-subagents child. Let the
7380
7291
  // request produce its real error so the parent runner can advance its own
7381
7292
  // fallbackModels chain instead of starting a competing router here.
@@ -9093,10 +9004,85 @@ export default function piMultiAccount(pi: ExtensionAPI) {
9093
9004
  return true;
9094
9005
  }
9095
9006
 
9007
+ function modelPinLines(ctx: any): string[] {
9008
+ const entries = Object.entries(config.pinnedModels).sort(([a], [b]) => a.localeCompare(b));
9009
+ if (!entries.length) return [" none — /multi-account pin <provider/model>"];
9010
+ const models = [...ctx.modelRegistry.getAll(), ...[...hiddenProviderModels.values()].flat()];
9011
+ const now = Date.now();
9012
+ return entries.map(([scope, id]) => {
9013
+ const matching = models.filter((model: any) => accountGroup(model.provider, config.qwenProvider) === scope &&
9014
+ sameModelIdentity(model.id, id) && isAccountChatModel(model));
9015
+ const usable = matching.filter((model: any) => !isInvalidated(model.provider) && providerHasUsableAuth(ctx, model.provider));
9016
+ const sourceBand = modelQualityBand(ctx.model?.id, ctx.model?.provider);
9017
+ const compatible = usable.filter((model: any) => {
9018
+ const band = modelQualityBand(model.id, model.provider);
9019
+ if (sourceBand === "apex") return band === "apex" && explicitlyAllowsPaidApexFallback(model, ctx.model);
9020
+ return band !== "apex" && (!sourceBand || band === sourceBand) && (!apexIdentity(model.id) || sameModelIdentity(model.id, ctx.model?.id));
9021
+ });
9022
+ const free = compatible.filter((model: any) => providerRecoveryAt(model.provider, now) <= now &&
9023
+ (exhaustedUntilByModel.get(`${model.provider}/${model.id}`) ?? 0) <= now);
9024
+ const status = !matching.length ? "missing from catalog" : !usable.length ? "no usable account" :
9025
+ !compatible.length ? "incompatible with current quality band" : !free.length ? "cooling" : "preference available";
9026
+ return ` ${scope}: ${id} — ${status}`;
9027
+ });
9028
+ }
9029
+
9096
9030
  async function handleCommand(args: string, ctx: any) {
9097
- const [commandRaw, arg1] = args.trim().split(/\s+/);
9031
+ const parts = args.trim().split(/\s+/);
9032
+ const [commandRaw, arg1] = parts;
9098
9033
  const command = (commandRaw || "status").toLowerCase();
9099
9034
 
9035
+ if (command === "pins") {
9036
+ if (parts.length > 2 || (arg1 && arg1 !== "list")) {
9037
+ ctx.ui.notify("pi-multi-account: usage: /multi-account pins [list]", "warning");
9038
+ return;
9039
+ }
9040
+ ctx.ui.notify(["pi-multi-account model pins (routing preferences, not foreground selections):", ...modelPinLines(ctx),
9041
+ "Exact siblings, authentication, health and quality checks still apply. Other sessions adopt edits on reload/restart."].join("\n"), "info");
9042
+ return;
9043
+ }
9044
+
9045
+ if (command === "pin" || command === "unpin") {
9046
+ const parsed = command === "pin" ? parseTarget(arg1) : undefined;
9047
+ const provider = command === "pin" ? parsed?.provider : arg1;
9048
+ if (parts.length !== 2 || !provider || !validModelPinScope(provider) ||
9049
+ (command === "pin" && (!parsed?.modelId || /\s/.test(parsed.modelId)))) {
9050
+ ctx.ui.notify("pi-multi-account: usage: /multi-account pin <provider/model> | unpin <provider-or-family> | pins [list]", "warning");
9051
+ return;
9052
+ }
9053
+ if (subagentChild || sessionClosed || !ctx.isIdle()) {
9054
+ ctx.ui.notify("pi-multi-account: change model pins from an idle live parent session; no preference was saved", "warning");
9055
+ return;
9056
+ }
9057
+ const epoch = chainEpoch;
9058
+ const selected = ctx.model ? ref(ctx.model.provider, ctx.model.id) : undefined;
9059
+ const session = ctx.sessionManager?.getSessionId?.();
9060
+ const canCommit = () => !subagentChild && !sessionClosed && ctx.isIdle() && epoch === chainEpoch &&
9061
+ (ctx.model ? ref(ctx.model.provider, ctx.model.id) : undefined) === selected && ctx.sessionManager?.getSessionId?.() === session;
9062
+ const scope = accountGroup(provider, config.qwenProvider);
9063
+ const modelId = parsed?.modelId;
9064
+ const exact = modelId ? findModelIncludingHidden(ctx, provider, modelId) : undefined;
9065
+ const known = modelId ? [exact, ...ctx.modelRegistry.getAll(), ...[...hiddenProviderModels.values()].flat()].filter((model: any) =>
9066
+ model && accountGroup(model.provider, config.qwenProvider) === scope && sameModelIdentity(model.id, modelId)) : [];
9067
+ if ((exact && !isAccountChatModel(exact)) || (known.length > 0 && !known.some(isAccountChatModel))) {
9068
+ ctx.ui.notify("pi-multi-account: only account chat models can be pinned; virtual/image/classifier models remain host-owned", "warning");
9069
+ return;
9070
+ }
9071
+ try {
9072
+ const pins = persistModelPin(CONFIG_PATH, scope, modelId, canCommit);
9073
+ config = { ...config, pinnedModels: pins };
9074
+ claimUserControl("model pin preference changed");
9075
+ logEvent("model_pin_set", { scope, model: modelId ?? null });
9076
+ ctx.ui.notify(command === "unpin"
9077
+ ? `pi-multi-account: removed model pin for ${scope}; the underlying preferredModels/catalog order is unchanged`
9078
+ : `pi-multi-account: pinned ${scope}/${modelId} across sibling accounts.${known.length ? "" : " Saved but currently absent; inactive until the catalog supplies it."} Current model was not changed; select a cheaper tier explicitly. Other sessions adopt this preference on reload/restart.`,
9079
+ known.length || command === "unpin" ? "info" : "warning");
9080
+ } catch {
9081
+ ctx.ui.notify("pi-multi-account: model pin was not saved; check config syntax, permissions/lock and session ownership. Runtime preference is unchanged", "error");
9082
+ }
9083
+ return;
9084
+ }
9085
+
9100
9086
  if (command === "log" || command === "logs" || command === "debug") {
9101
9087
  // Show the diagnostic black box so a misbehaviour can be reported precisely.
9102
9088
  if (arg1 === "off") {
@@ -9389,14 +9375,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
9389
9375
  configuredFallbacks = [];
9390
9376
  config = { ...config, fallbacks: [] };
9391
9377
  try {
9392
- const raw = existsSync(CONFIG_PATH)
9393
- ? JSON.parse(readFileSync(CONFIG_PATH, "utf8"))
9394
- : {};
9395
- raw.fallbacks = [];
9396
- writeFileSync(CONFIG_PATH, `${JSON.stringify(raw, null, "\t")}\n`, {
9397
- encoding: "utf8",
9398
- mode: 0o600,
9399
- });
9378
+ mutateConfigFile(CONFIG_PATH, () => ({ fallbacks: [] }));
9400
9379
  } catch {
9401
9380
  // non-fatal: in-memory state is still cleared
9402
9381
  }
@@ -9610,7 +9589,8 @@ export default function piMultiAccount(pi: ExtensionAPI) {
9610
9589
  ? `${parsed.provider}/${parsed.modelId}`
9611
9590
  : parsed.provider;
9612
9591
  const candidates = resolveTargets(ctx, resolvedTarget, ctx.model, true).filter(
9613
- (model: any) => providerHasUsableAuth(ctx, model.provider),
9592
+ (model: any) => providerHasUsableAuth(ctx, model.provider) &&
9593
+ (parsed.modelId !== undefined || pinQualityCompatible(model, ctx.model)),
9614
9594
  );
9615
9595
  if (candidates.length === 0) {
9616
9596
  const hasAuth = providerHasUsableAuth(ctx, parsed.provider);
@@ -9755,14 +9735,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
9755
9735
  onlyActiveModels = next2;
9756
9736
  config = { ...config, onlyActive: next2 };
9757
9737
  try {
9758
- const raw = existsSync(CONFIG_PATH)
9759
- ? JSON.parse(readFileSync(CONFIG_PATH, "utf8"))
9760
- : {};
9761
- raw.onlyActive = next2;
9762
- writeFileSync(CONFIG_PATH, `${JSON.stringify(raw, null, "\t")}\n`, {
9763
- encoding: "utf8",
9764
- mode: 0o600,
9765
- });
9738
+ mutateConfigFile(CONFIG_PATH, () => ({ onlyActive: next2 }));
9766
9739
  } catch {
9767
9740
  // non-fatal: the in-memory flag still applies for this session
9768
9741
  }
@@ -9821,14 +9794,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
9821
9794
 
9822
9795
  config = { ...config, providerPriority: next };
9823
9796
  try {
9824
- const raw = existsSync(CONFIG_PATH)
9825
- ? JSON.parse(readFileSync(CONFIG_PATH, "utf8"))
9826
- : {};
9827
- raw.providerPriority = next;
9828
- writeFileSync(CONFIG_PATH, `${JSON.stringify(raw, null, "\t")}\n`, {
9829
- encoding: "utf8",
9830
- mode: 0o600,
9831
- });
9797
+ mutateConfigFile(CONFIG_PATH, () => ({ providerPriority: next }));
9832
9798
  } catch {
9833
9799
  // non-fatal: the ladder still applies for this session
9834
9800
  }
@@ -10004,7 +9970,8 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10004
9970
  // list below, `switch` was effectively undiscoverable and `next` pressed repeatedly
10005
9971
  // was the only way anyone found to reach a chosen account.
10006
9972
  `Switch accounts: /multi-account best — jump straight to an account that can work now · /multi-account switch <provider> — e.g. /multi-account switch ${rotation.find((p) => p !== ctx.model?.provider) ?? rotation[0] ?? "<provider>"} · /multi-account next steps through the rotation in order`,
10007
- `Other commands: status | accounts [refresh] | best | priority [...] | limits [refresh] | models | pick | save-default | log [N|on|off] | only-active [on|off] | rediscover | add [anthropic|codex|kimi|cursor|ollama|qwen] | remove [anthropic|codex|kimi|cursor|ollama|qwen|<provider-id>] | revive <provider|all> | clear | stop | reset | reload | enable | disable`,
9973
+ `Model pins: ${Object.entries(config.pinnedModels).map(([scope, id]) => `${scope}/${id}`).join(", ") || "none"} · /multi-account pins to inspect`,
9974
+ `Other commands: status | accounts [refresh] | best | priority [...] | limits [refresh] | models | pick | pin <provider/model> | unpin <provider-or-family> | pins [list] | save-default | log [N|on|off] | only-active [on|off] | rediscover | add [anthropic|codex|kimi|cursor|ollama|qwen] | remove [anthropic|codex|kimi|cursor|ollama|qwen|<provider-id>] | revive <provider|all> | clear | stop | reset | reload | enable | disable`,
10008
9975
  ].join("\n"),
10009
9976
  "info",
10010
9977
  );
@@ -10084,6 +10051,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10084
10051
  const models = ids.map((m) =>
10085
10052
  family === "ollama" ? ollamaModelDef(m, baseId) : qwenModelDef(m, baseId),
10086
10053
  );
10054
+ models.push(...configuredOperationModels(baseId));
10087
10055
  pi.registerProvider(baseId, {
10088
10056
  name: family === "ollama" ? "Ollama" : "Alibaba/Qwen",
10089
10057
  baseUrl,
@@ -10799,8 +10767,10 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10799
10767
  // An existing child-facing placeholder (possibly published by another root)
10800
10768
  // must also stay on its canonical loopback, even when this instance's proxy is off.
10801
10769
  function numberedAnthropicBaseUrl(id: string): string {
10770
+ const raw = readAuthFileRaw()[id];
10771
+ if (raw?.type === "api_key" && !isChildFacingPlaceholderForSlot(raw, id)) return "https://api.anthropic.com";
10802
10772
  if (typeof slotProxyPort === "number") return publishedRouteFor(slotProxyPort, id);
10803
- if (isChildFacingPlaceholderForSlot(readAuthFileRaw()[id], id)) return publishedRouteFor(SLOT_PROXY_PORT, id);
10773
+ if (isChildFacingPlaceholderForSlot(raw, id)) return publishedRouteFor(SLOT_PROXY_PORT, id);
10804
10774
  return "https://api.anthropic.com";
10805
10775
  }
10806
10776
 
@@ -10824,6 +10794,8 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10824
10794
  * the canonical loopback instead, even if this sibling disabled its own proxy.
10825
10795
  */
10826
10796
  function numberedCodexBaseUrl(id: string): string {
10797
+ const raw = readAuthFileRaw()[id];
10798
+ if (raw?.type === "api_key" && !isChildFacingPlaceholderForSlot(raw, id)) return "https://chatgpt.com/backend-api";
10827
10799
  if (typeof slotProxyPort === "number") return publishedRouteFor(slotProxyPort, id);
10828
10800
  if (!config.childProxy && !isChildFacingPlaceholderForSlot(readAuthFileRaw()[id], id)) {
10829
10801
  return "https://chatgpt.com/backend-api";
@@ -10847,7 +10819,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10847
10819
  let entry = readAuthFile()[slotId];
10848
10820
  if (!entry) return undefined;
10849
10821
  if (entry.type !== "oauth" || typeof entry.access !== "string") return entry;
10850
- const expiry = jwtExpMs(entry.access);
10822
+ const expiry = oauthExpiryMs(entry);
10851
10823
  // A minute of slack: a token that expires mid-flight fails the child's whole request, and
10852
10824
  // refreshing one turn early costs nothing.
10853
10825
  if (expiry !== undefined && expiry - 60_000 <= Date.now()) {
@@ -10884,10 +10856,9 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10884
10856
  return;
10885
10857
  }
10886
10858
  const parsed = parseProxyPath(req.url ?? "");
10887
- const presentedSecret =
10888
- parsed && typeof readAuthFile()[parsed.slotId]?.access === "string"
10889
- ? readAuthFile()[parsed.slotId]?.access
10890
- : undefined;
10859
+ const presentedEntry = parsed ? readAuthFile()[parsed.slotId] : undefined;
10860
+ const presentedSecret = presentedEntry?.type === "oauth" ? presentedEntry.access :
10861
+ presentedEntry?.type === "api_key" ? presentedEntry.key : undefined;
10891
10862
  const verdict = admitRequest({
10892
10863
  rawUrl: req.url ?? "",
10893
10864
  headers: req.headers ?? {},
@@ -10920,9 +10891,9 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10920
10891
  let body: Buffer | undefined =
10921
10892
  req.method === "GET" || req.method === "HEAD" ? undefined : await readRequestBody(req);
10922
10893
  if (body && verdict.route.family === "anthropic" && credential?.type === "oauth") {
10923
- // The same shaping the in-process path applies: without it Anthropic rejects a
10924
- // subscription token outright. Done here because the child cannot do it — it has no
10925
- // idea it is talking to a proxy.
10894
+ // The extension-owned billing shaping also applies to bare children. Their native
10895
+ // host supplies OAuth identity/tool mapping through the token-shaped placeholder,
10896
+ // but does not load this extension's billing-header maintenance.
10926
10897
  try {
10927
10898
  const parsed = JSON.parse(body.toString("utf8"));
10928
10899
  const reshaped = shapeAnthropicOAuthPayload(parsed);
@@ -10932,11 +10903,18 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10932
10903
  }
10933
10904
  }
10934
10905
 
10906
+ if (res.destroyed) return;
10907
+ const upstreamAbort = new AbortController();
10908
+ const callerClosed = () => {
10909
+ if (!res.writableFinished) upstreamAbort.abort();
10910
+ };
10911
+ res.once("close", callerClosed);
10935
10912
  try {
10936
10913
  const upstream = await fetch(shaped.url, {
10937
10914
  method: req.method ?? "POST",
10938
10915
  headers: shaped.headers,
10939
10916
  body: body && body.length ? new Uint8Array(body) : undefined,
10917
+ signal: upstreamAbort.signal,
10940
10918
  });
10941
10919
  const outgoing: Record<string, string> = {};
10942
10920
  upstream.headers.forEach((value, name) => {
@@ -10948,7 +10926,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10948
10926
  if (upstream.body) {
10949
10927
  await new Promise<void>((resolve) => {
10950
10928
  const stream = Readable.fromWeb(upstream.body as any);
10951
- stream.on("error", () => resolve());
10929
+ stream.on("error", () => { res.destroy(); resolve(); });
10952
10930
  res.on("close", () => resolve());
10953
10931
  stream.pipe(res);
10954
10932
  res.on("finish", () => resolve());
@@ -10956,11 +10934,13 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10956
10934
  } else {
10957
10935
  res.end();
10958
10936
  }
10959
- logEvent("slot_proxy_forwarded", {
10937
+ if (!upstreamAbort.signal.aborted) logEvent("slot_proxy_forwarded", {
10960
10938
  slot: verdict.route.slotId,
10961
10939
  status: upstream.status,
10962
10940
  });
10963
10941
  } catch (error) {
10942
+ // A caller cancellation is not an upstream/provider failure or a quota verdict.
10943
+ if (upstreamAbort.signal.aborted || res.destroyed) return;
10964
10944
  const reason = error instanceof Error ? error.message : String(error);
10965
10945
  logEvent("slot_proxy_upstream_failed", { slot: verdict.route.slotId, reason });
10966
10946
  if (!res.headersSent) {
@@ -10969,6 +10949,8 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10969
10949
  } else {
10970
10950
  res.end();
10971
10951
  }
10952
+ } finally {
10953
+ res.off("close", callerClosed);
10972
10954
  }
10973
10955
  }
10974
10956
 
@@ -11227,12 +11209,14 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11227
11209
  for (const id of slots) {
11228
11210
  const family = proxyFamilyOf(id);
11229
11211
  if (!family) continue;
11230
- const baseUrl = publishedRouteFor(port, id);
11212
+ const oauth = parent[id]?.type === "oauth";
11213
+ const baseUrl = oauth ? publishedRouteFor(port, id) :
11214
+ family === "anthropic" ? "https://api.anthropic.com" : "https://chatgpt.com/backend-api";
11231
11215
  if (family === "anthropic") {
11232
11216
  provisionNativeSlot(id, {
11233
11217
  api: "anthropic-messages",
11234
11218
  baseUrl,
11235
- apiKey: placeholderKeyFor(family),
11219
+ apiKey: oauth ? placeholderKeyFor(family) : undefined,
11236
11220
  models: DEFAULT_ANTHROPIC_MODELS.map((modelId) => ({
11237
11221
  ...anthropicModelDef(modelId, id),
11238
11222
  baseUrl,
@@ -11253,7 +11237,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11253
11237
  provisionNativeSlot(id, {
11254
11238
  api: "openai-codex-responses",
11255
11239
  baseUrl,
11256
- apiKey: placeholderKeyFor(family),
11240
+ apiKey: oauth ? placeholderKeyFor(family) : undefined,
11257
11241
  models,
11258
11242
  });
11259
11243
  }
@@ -12072,6 +12056,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
12072
12056
  });
12073
12057
 
12074
12058
  safeOn("message_end", async (event, ctx) => {
12059
+ if (!isAccountChatModel(ctx.model)) return;
12075
12060
  const errorEpoch = chainEpoch;
12076
12061
  const staleError = () => sessionClosed || errorEpoch !== chainEpoch || userAbortedChain || ctx.signal?.aborted;
12077
12062
  const message = (event as any).message;
@@ -12287,6 +12272,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
12287
12272
  });
12288
12273
 
12289
12274
  safeOn("agent_end", async (event, ctx) => {
12275
+ if (!isAccountChatModel(ctx.model)) return;
12290
12276
  if (!automaticFailoverEnabled()) return;
12291
12277
  const stop = lastAssistantStopReason((event as any).messages ?? []);
12292
12278
  // Our own watchdog aborted a wedged resumed turn — this is RECOVERY, not a user cancel.