@herbertgao/pi-extensions 2026.9.14 → 2026.9.16

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 (206) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +28 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  5. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  6. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  7. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  8. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  9. package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
  10. package/node_modules/pi-jev-auto-mode/README.md +7 -0
  11. package/node_modules/pi-jev-auto-mode/package.json +1 -1
  12. package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
  13. package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
  14. package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
  15. package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
  16. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  17. package/node_modules/pi-lens/README.md +45 -2
  18. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  19. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  20. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  21. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  22. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  23. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  24. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  25. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  26. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  27. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  28. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  29. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  30. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  31. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  71. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  72. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  73. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  74. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  75. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  76. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  77. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  78. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  79. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  80. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  81. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  82. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  83. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  84. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  85. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  86. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  87. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  88. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  89. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  90. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  91. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  92. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  93. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  94. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  95. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  96. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  97. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  98. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  99. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  100. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  101. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  102. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  103. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  104. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  105. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  106. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  107. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  108. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  109. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  110. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  111. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  112. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  113. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  114. package/node_modules/pi-lens/dist/index.js +5695 -3733
  115. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  116. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  117. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  118. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  119. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  120. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  121. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  122. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  123. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  124. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  125. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  126. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  127. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  128. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  129. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  130. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  131. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  132. package/node_modules/pi-lens/docs/configuration.md +55 -1
  133. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  134. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  135. package/node_modules/pi-lens/docs/features.md +2 -2
  136. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  137. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  138. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  139. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  140. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  141. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  142. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  143. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  144. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  145. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  146. package/node_modules/pi-lens/docs/settings.md +12 -3
  147. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  148. package/node_modules/pi-lens/package.json +5 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  157. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  158. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  159. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  160. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  161. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  162. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  163. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  164. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  165. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  166. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  167. package/node_modules/pi-mcp-adapter/README.md +12 -5
  168. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  169. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  170. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  171. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  173. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  175. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  176. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  177. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  178. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  179. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  180. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  181. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  182. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  183. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  184. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  185. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  186. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  187. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  188. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  189. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  190. package/node_modules/pi-mcp-adapter/package.json +3 -2
  191. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  192. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  193. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  194. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  195. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  196. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  197. package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
  198. package/node_modules/pi-multi-account/README.md +15 -1
  199. package/node_modules/pi-multi-account/index.ts +295 -41
  200. package/node_modules/pi-multi-account/package.json +1 -1
  201. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  202. package/node_modules/pi-next-cue/LICENSE +21 -0
  203. package/node_modules/pi-next-cue/README.md +81 -0
  204. package/node_modules/pi-next-cue/index.ts +358 -0
  205. package/node_modules/pi-next-cue/package.json +28 -0
  206. package/package.json +10 -7
@@ -179,7 +179,21 @@ Background attempts share the same cooldown and invalidation state as foreground
179
179
 
180
180
  ### pi-subagents and delegation-broker compatibility
181
181
 
182
- `pi-subagents` marks native child processes with `PI_SUBAGENT_CHILD=1` and owns their explicit model plus `fallbackModels` chain. Concurrent in-process SDK sessions also stay passive while a root session activation is live. The root lease is released at session shutdown, so `/reload`, `/new` and replacement root sessions do not permanently lose routing ownership. This is process-local ownership, not detection based on model names or a permanent first-factory flag. In those children this extension stays loaded only for provider/account registration, OAuth request shaping, and catalog support. It deliberately does **not** restore the interactive process's remembered model, persist the child's model as a user preference, switch models, queue work, or auto-continue after errors. The original provider error is returned unchanged so the parent runner can advance its verified fallback chain without a second router competing for model identity.
182
+ `pi-subagents` marks native child processes with `PI_SUBAGENT_CHILD=1` and owns their explicit model plus `fallbackModels` chain. Under terminal Pi, a distinct concurrent in-process child also stays passive while a root activation is live. A replacement with the **same nonempty session ID** reacquires that root's failover ownership without closing its child proxy. The root lease is released at final session shutdown, so `/reload` and `/new` do not permanently lose routing ownership. In passive children this extension stays loaded only for provider/account registration, OAuth request shaping, and catalog support. It deliberately does **not** restore the interactive process's remembered model, persist the child's model as a user preference, switch models, queue work, or auto-continue after errors. The original provider error is returned unchanged so the parent runner can advance its verified fallback chain without a second router competing for model identity.
183
+
184
+ ### Multi-session hosts (pi-web and other in-process SDK hosts)
185
+
186
+ Some hosts run many independent sessions in a single long-lived process — pi-web's session daemon, or in-process SDK hosts such as Enso (one session per chat thread). Terminal Pi's in-process root-activation lease would incorrectly demote every session but the first to passive in those hosts.
187
+
188
+ - Under pi-web (`PI_WEB_SESSION=1`, set by the daemon itself) every session activates as an independent root automatically.
189
+ - Any other multi-session host opts in by exporting `PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1` in the host process. Set this only when each session truly owns its model; sessions that share one model identity must keep the default single-root behavior.
190
+ - Genuine `pi-subagents` children are unaffected either way: they run in a runner process marked `PI_SUBAGENT_CHILD=1`, which always takes precedence and stays passive.
191
+
192
+ Hosts should not set `PI_WEB_SESSION` themselves — other extensions read it to detect pi-web specifically.
193
+
194
+ In one process, independent roots share one canonical loopback proxy and its child-facing auth/models publication while any root has the child proxy enabled. Closing one root cannot expose its sibling's real OAuth or remove the route its bare children use; the final live root closes the listener and restores the original auth. A rehydrated session with the same ID replaces the old instance's failover ownership rather than counting as a second root. Another process holding the canonical port remains the publisher; a foreign listener is never allowed to overwrite its shared files.
195
+
196
+ Roots with `childProxy: false` still participate in the shared lifetime if another root publishes placeholders. Any public numbered alias they registered earlier is moved to loopback **before** the placeholder appears; they keep the canonical listener alive if the publisher exits first, and only the final root restores auth. With no other publishing root, disabling `childProxy` retains the usual direct route. Hosts should still keep one consistent setting across concurrent roots.
183
197
 
184
198
  [`pi-delegation-broker`](https://github.com/Sarrius/pi-delegation-broker) is an optional companion for splitting work among isolated child agents, with task budgets, reports and verification. Each extension works independently and depends on Agent Pi, not on the other extension. Together, Multi Account manages your interactive account rotation while the broker delegates through Pi's registered providers and models. Neither installs or initializes the other.
185
199
 
@@ -42,7 +42,7 @@ import {
42
42
  statSync,
43
43
  writeFileSync,
44
44
  } from "node:fs";
45
- import { createServer, type Server } from "node:http";
45
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
46
46
  import { homedir } from "node:os";
47
47
  import { Readable } from "node:stream";
48
48
  import { dirname, join } from "node:path";
@@ -1195,7 +1195,7 @@ const ANTI_PINGPONG_MS = 60 * 1000; // don't switch straight back to the account
1195
1195
  // Bumped on every release. Printed at startup and in `/multi-account status` so you can verify
1196
1196
  // which version Pi actually loaded (a running Pi keeps the version it started with — /login and
1197
1197
  // /reload do NOT reload extension code; only a full restart does).
1198
- const VERSION = "1.23.1";
1198
+ const VERSION = "1.23.2";
1199
1199
  function sourceFingerprint(): string {
1200
1200
  try {
1201
1201
  const root = dirname(fileURLToPath(import.meta.url));
@@ -3933,11 +3933,48 @@ export default function piMultiAccount(pi: ExtensionAPI) {
3933
3933
  let subagentChild = isSubagentChildProcess();
3934
3934
  let hostOwnsSessionModel = false;
3935
3935
  let startupModel: { provider: string; id: string } | undefined;
3936
- // A live activation lease, not a permanent first-factory flag: /reload and /new
3937
- // must be able to acquire root ownership after the previous root shuts down.
3936
+ // Terminal Pi keeps the conservative one-root lease. A replacement of the SAME host
3937
+ // session can reacquire it; an unrelated in-process child remains passive.
3938
3938
  const activationKey = Symbol.for("pi-multi-account:active-root-session");
3939
- const activations = globalThis as typeof globalThis & { [activationKey]?: object };
3939
+ const activations = globalThis as typeof globalThis & {
3940
+ [activationKey]?: { owner: object; sessionId?: string; relinquish: () => void };
3941
+ };
3940
3942
  const activationOwner = {};
3943
+ // Independent SDK roots share the canonical child listener and its auth/models
3944
+ // publication by port. Their failover state remains private to each session.
3945
+ type SharedProxyMember = {
3946
+ sessionId?: string;
3947
+ ready: boolean;
3948
+ proxyEnabled: boolean;
3949
+ prepareRoutes: (slotIds: readonly string[], port: number) => boolean;
3950
+ relinquish: () => void;
3951
+ handleRequest: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
3952
+ };
3953
+ type SharedProxyCoordinator = {
3954
+ members: Map<object, SharedProxyMember>;
3955
+ // Every published slot stays routable when its discovering root exits first.
3956
+ routes: Map<string, ProxyRoute>;
3957
+ sessionOwners: Map<string, object>;
3958
+ server?: Server;
3959
+ starting?: Promise<number | undefined>;
3960
+ publisher?: object;
3961
+ handlerOwner?: object;
3962
+ };
3963
+ const coordinatorKey = Symbol.for("pi-multi-account:shared-slot-proxy-coordinators");
3964
+ const coordinatorGlobals = globalThis as typeof globalThis & {
3965
+ [coordinatorKey]?: Map<number, SharedProxyCoordinator>;
3966
+ };
3967
+ const coordinators = coordinatorGlobals[coordinatorKey] ??= new Map<number, SharedProxyCoordinator>();
3968
+ let slotProxyCoordinator: SharedProxyCoordinator | undefined;
3969
+ // pi-web's session daemon hosts many independent root sessions in ONE process. The
3970
+ // terminal-Pi lease heuristic would demote every session but the first to passive,
3971
+ // silently disabling failover there. Genuine subagent children under pi-web run in a
3972
+ // runner process marked PI_SUBAGENT_CHILD, which is checked above and still wins.
3973
+ // Any other multi-session SDK host (e.g. Enso) opts in with the host-neutral switch
3974
+ // PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS=1 instead of borrowing pi-web's marker.
3975
+ const multiSessionHost =
3976
+ process.env.PI_WEB_SESSION === "1" ||
3977
+ process.env.PI_MULTI_ACCOUNT_INDEPENDENT_ROOTS === "1";
3941
3978
  const explicitCliArgs = parseExplicitCliArgs();
3942
3979
  const explicitCli = {
3943
3980
  model: explicitCliArgs.model !== undefined,
@@ -3952,6 +3989,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
3952
3989
  const sessionInstanceId = `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
3953
3990
  let config = loadConfig();
3954
3991
  let sessionClosed = false;
3992
+ let supersededRoot = false;
3955
3993
  let manualRouteOwnsErrors = false;
3956
3994
  const automaticFailoverEnabled = () => config.enabled && !subagentChild && !sessionClosed;
3957
3995
  debugLogEnabled = config.debugLog;
@@ -10647,15 +10685,90 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10647
10685
  const SLOT_PROXY_PORT =
10648
10686
  Number(process.env.PI_MULTI_ACCOUNT_SLOT_PROXY_PORT) || 41977;
10649
10687
 
10650
- /**
10651
- * One process owns every child-facing file publication. With the normal proxy enabled, the
10652
- * canonical listening port is the ownership token. A pi-subagents child is never an owner,
10653
- * even if no parent happens to be listening when it starts.
10654
- */
10688
+ /** A shared publisher must still own the canonical socket and a live root membership. */
10655
10689
  function ownsSharedChildPublication(): boolean {
10656
10690
  if (subagentChild) return false;
10657
- if (!config.childProxy) return true;
10658
- return !slotProxyForeignOwner && slotProxyPort === SLOT_PROXY_PORT;
10691
+ if (!config.childProxy) {
10692
+ const cohort = slotProxyCoordinator ?? coordinators.get(SLOT_PROXY_PORT);
10693
+ if (cohort?.server || [...(cohort?.members.values() ?? [])].some((member) => member.proxyEnabled)) {
10694
+ return false;
10695
+ }
10696
+ return !Object.entries(readAuthFileRaw()).some(([id, entry]) =>
10697
+ isChildFacingPlaceholderForSlot(entry, id));
10698
+ }
10699
+ if (sessionClosed) return false;
10700
+ const coordinator = slotProxyCoordinator;
10701
+ return !!coordinator && coordinator.members.has(activationOwner) &&
10702
+ coordinator.publisher === activationOwner && coordinator.server?.listening === true &&
10703
+ !slotProxyForeignOwner && slotProxyPort === SLOT_PROXY_PORT;
10704
+ }
10705
+
10706
+ function readyProxyOwner(coordinator: SharedProxyCoordinator): object | undefined {
10707
+ return [...coordinator.members].reverse().find(([, member]) => member.ready)?.[0];
10708
+ }
10709
+
10710
+ function relinquishRoot(): void {
10711
+ if (supersededRoot) return;
10712
+ supersededRoot = true;
10713
+ subagentChild = true;
10714
+ sessionClosed = true;
10715
+ chainEpoch++;
10716
+ completionRouterContext = undefined;
10717
+ if (pendingWakeTimer) clearTimeout(pendingWakeTimer);
10718
+ pendingWakeTimer = undefined;
10719
+ clearUsageStatusTimer();
10720
+ endResumeWatch();
10721
+ clearQueuedInputs();
10722
+ }
10723
+
10724
+ function prepareSharedRoutes(slotIds: readonly string[], port: number): boolean {
10725
+ if (config.childProxy || sessionClosed || subagentChild) return false;
10726
+ slotProxyPort = port;
10727
+ for (const id of slotIds) {
10728
+ if (!registeredSlots.has(id)) continue;
10729
+ const baseUrl = publishedRouteFor(port, id);
10730
+ const family = proxyFamilyOf(id);
10731
+ if (family === "codex") {
10732
+ const cached = codexModelCatalogByProvider.get(id)?.models;
10733
+ registerCodexSlot(pi, id, config.autoDiscoverModels && cached?.length
10734
+ ? (cached as Array<Record<string, unknown>>) : undefined, baseUrl);
10735
+ } else if (family === "anthropic") {
10736
+ registerAnthropicSlot(pi, id, DEFAULT_ANTHROPIC_MODELS, baseUrl);
10737
+ }
10738
+ }
10739
+ return true;
10740
+ }
10741
+
10742
+ function joinSharedProxy(sessionId?: string): void {
10743
+ let coordinator = coordinators.get(SLOT_PROXY_PORT);
10744
+ if (!coordinator) {
10745
+ coordinator = { members: new Map(), routes: new Map(), sessionOwners: new Map() };
10746
+ coordinators.set(SLOT_PROXY_PORT, coordinator);
10747
+ }
10748
+ slotProxyCoordinator = coordinator;
10749
+ if (sessionId) {
10750
+ const previousOwner = coordinator.sessionOwners.get(sessionId);
10751
+ if (previousOwner && previousOwner !== activationOwner) {
10752
+ coordinator.members.get(previousOwner)?.relinquish();
10753
+ coordinator.members.delete(previousOwner);
10754
+ if (coordinator.publisher === previousOwner) coordinator.publisher = readyProxyOwner(coordinator);
10755
+ if (coordinator.handlerOwner === previousOwner) coordinator.handlerOwner = readyProxyOwner(coordinator);
10756
+ }
10757
+ coordinator.sessionOwners.set(sessionId, activationOwner);
10758
+ }
10759
+ coordinator.members.set(activationOwner, {
10760
+ sessionId, ready: false, proxyEnabled: config.childProxy,
10761
+ prepareRoutes: prepareSharedRoutes, relinquish: relinquishRoot,
10762
+ handleRequest: handleProxyRequest,
10763
+ });
10764
+ }
10765
+
10766
+ function activateSharedProxy(): void {
10767
+ const coordinator = slotProxyCoordinator;
10768
+ const member = coordinator?.members.get(activationOwner);
10769
+ if (!member || sessionClosed || subagentChild || !coordinator?.server?.listening) return;
10770
+ member.ready = true;
10771
+ coordinator.handlerOwner = activationOwner;
10659
10772
  }
10660
10773
 
10661
10774
  refreshDiscovery(true);
@@ -10683,12 +10796,11 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10683
10796
  return proxyFamilyFor(slotId);
10684
10797
  }
10685
10798
 
10686
- //
10687
- // Behavior is unchanged from the single helper this replaced: a numbered Anthropic alias
10688
- // falls back to the public API when this process has no loopback route yet.
10689
- //
10799
+ // An existing child-facing placeholder (possibly published by another root)
10800
+ // must also stay on its canonical loopback, even when this instance's proxy is off.
10690
10801
  function numberedAnthropicBaseUrl(id: string): string {
10691
10802
  if (typeof slotProxyPort === "number") return publishedRouteFor(slotProxyPort, id);
10803
+ if (isChildFacingPlaceholderForSlot(readAuthFileRaw()[id], id)) return publishedRouteFor(SLOT_PROXY_PORT, id);
10692
10804
  return "https://api.anthropic.com";
10693
10805
  }
10694
10806
 
@@ -10707,12 +10819,15 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10707
10819
  * slot. If the port is never served the request fails to connect, which is loud and
10708
10820
  * harmless — unlike a credential that reaches a provider that cannot read it.
10709
10821
  *
10710
- * With the proxy disabled no placeholder is ever published: the real credential is what Pi
10711
- * presents, so the public upstream is the correct destination.
10822
+ * With the proxy disabled AND no other root's placeholder published, Pi presents a real
10823
+ * credential and the public upstream is correct. A published placeholder always takes
10824
+ * the canonical loopback instead, even if this sibling disabled its own proxy.
10712
10825
  */
10713
10826
  function numberedCodexBaseUrl(id: string): string {
10714
10827
  if (typeof slotProxyPort === "number") return publishedRouteFor(slotProxyPort, id);
10715
- if (!config.childProxy) return "https://chatgpt.com/backend-api";
10828
+ if (!config.childProxy && !isChildFacingPlaceholderForSlot(readAuthFileRaw()[id], id)) {
10829
+ return "https://chatgpt.com/backend-api";
10830
+ }
10716
10831
  return publishedRouteFor(SLOT_PROXY_PORT, id);
10717
10832
  }
10718
10833
 
@@ -10776,7 +10891,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10776
10891
  const verdict = admitRequest({
10777
10892
  rawUrl: req.url ?? "",
10778
10893
  headers: req.headers ?? {},
10779
- routes: slotProxyRoutes,
10894
+ routes: slotProxyCoordinator?.server ? slotProxyCoordinator.routes : slotProxyRoutes,
10780
10895
  acceptedSecrets: presentedSecret ? [presentedSecret] : [],
10781
10896
  });
10782
10897
  if (!verdict.ok) {
@@ -10860,11 +10975,38 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10860
10975
  /** Start the loopback listener once, preferring a stable port so published routes survive. */
10861
10976
  function startSlotProxy(): Promise<number | undefined> {
10862
10977
  if (slotProxyPort !== undefined) return Promise.resolve(slotProxyPort);
10978
+ const coordinator = slotProxyCoordinator;
10979
+ if (coordinator?.server?.listening) {
10980
+ slotProxyPort = SLOT_PROXY_PORT;
10981
+ return Promise.resolve(SLOT_PROXY_PORT);
10982
+ }
10983
+ if (coordinator?.starting) {
10984
+ return coordinator.starting.then((port) => {
10985
+ if (port === SLOT_PROXY_PORT && coordinator.server?.listening) {
10986
+ slotProxyPort = SLOT_PROXY_PORT;
10987
+ return SLOT_PROXY_PORT;
10988
+ }
10989
+ return startSlotProxy(); // a foreign owner forced the first member onto a local port
10990
+ });
10991
+ }
10863
10992
  if (slotProxyStarting) return slotProxyStarting;
10864
10993
  slotProxyStarting = new Promise<number | undefined>((resolve) => {
10865
10994
  let retriedOnEphemeralPort = false;
10995
+ let canonicalListener = false;
10866
10996
  const server = createServer((req, res) => {
10867
- handleProxyRequest(req, res).catch((error) => {
10997
+ // The socket outlives its first session. Dispatch only to a live, ready root;
10998
+ // a handoff gap must fail closed, never use a closed root's refresh context.
10999
+ const owner = coordinator?.handlerOwner;
11000
+ const member = owner ? coordinator?.members.get(owner) : undefined;
11001
+ const handler = canonicalListener
11002
+ ? (member?.ready ? member.handleRequest : undefined)
11003
+ : handleProxyRequest;
11004
+ if (!handler) {
11005
+ res.writeHead(503, { "content-type": "application/json" });
11006
+ res.end(JSON.stringify({ error: { message: "slot proxy handoff in progress" } }));
11007
+ return;
11008
+ }
11009
+ handler(req, res).catch((error) => {
10868
11010
  logEvent("slot_proxy_handler_failed", {
10869
11011
  reason: error instanceof Error ? error.message : String(error),
10870
11012
  });
@@ -10926,31 +11068,93 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10926
11068
  // retries on an ephemeral port.
10927
11069
  server.once("listening", () => {
10928
11070
  const address = server.address();
10929
- slotProxyPort = typeof address === "object" && address ? address.port : undefined;
11071
+ const port = typeof address === "object" && address ? address.port : undefined;
11072
+ if (port === SLOT_PROXY_PORT && coordinator) {
11073
+ if (coordinator.members.size === 0) {
11074
+ server.close();
11075
+ resolve(undefined);
11076
+ return;
11077
+ }
11078
+ coordinator.server = server;
11079
+ canonicalListener = true;
11080
+ coordinator.publisher = [...coordinator.members.keys()].at(-1);
11081
+ }
11082
+ // An instance replaced mid-bind must not retain a private listener.
11083
+ if (sessionClosed && port !== SLOT_PROXY_PORT) {
11084
+ server.close();
11085
+ resolve(undefined);
11086
+ return;
11087
+ }
11088
+ slotProxyPort = port;
10930
11089
  slotProxyServer = server;
10931
11090
  server.unref(); // never hold Pi open on our account
10932
- logEvent("slot_proxy_listening", { port: slotProxyPort });
10933
- resolve(slotProxyPort);
11091
+ logEvent("slot_proxy_listening", { port });
11092
+ resolve(port);
10934
11093
  });
10935
11094
  server.listen(SLOT_PROXY_PORT, "127.0.0.1");
10936
11095
  }).finally(() => {
11096
+ if (coordinator && coordinator.starting === slotProxyStarting) coordinator.starting = undefined;
11097
+ if (coordinator && coordinator.members.size === 0 && coordinators.get(SLOT_PROXY_PORT) === coordinator) {
11098
+ coordinators.delete(SLOT_PROXY_PORT);
11099
+ }
10937
11100
  slotProxyStarting = undefined;
10938
11101
  });
11102
+ if (coordinator) coordinator.starting = slotProxyStarting;
10939
11103
  return slotProxyStarting;
10940
11104
  }
10941
11105
 
10942
11106
  function stopSlotProxy() {
10943
- // Capture authority before clearing the port that proves it.
10944
- const publishedSharedFiles = ownsSharedChildPublication();
10945
- try {
10946
- slotProxyServer?.close();
10947
- } catch {
10948
- /* closing a server that never opened is not a problem */
11107
+ const coordinator = slotProxyCoordinator;
11108
+ if (coordinator) {
11109
+ const member = coordinator.members.get(activationOwner);
11110
+ const cleanupWithoutProxy = !config.childProxy && ownsSharedChildPublication();
11111
+ if (member) {
11112
+ coordinator.members.delete(activationOwner);
11113
+ if (member.sessionId && coordinator.sessionOwners.get(member.sessionId) === activationOwner) {
11114
+ coordinator.sessionOwners.delete(member.sessionId);
11115
+ }
11116
+ if (coordinator.publisher === activationOwner) coordinator.publisher = readyProxyOwner(coordinator);
11117
+ if (coordinator.handlerOwner === activationOwner) coordinator.handlerOwner = readyProxyOwner(coordinator);
11118
+ }
11119
+ // An ephemeral listener never owns shared files, even when another process
11120
+ // owns the canonical port. A superseded root cannot close its old canonical
11121
+ // listener: the coordinator still serves it for the replacement.
11122
+ if (slotProxyServer && slotProxyServer !== coordinator.server) {
11123
+ try { slotProxyServer.close(); } catch { /* already closed */ }
11124
+ }
11125
+ if (member && coordinator.members.size === 0) {
11126
+ if (coordinator.server) {
11127
+ // Keep the canonical port claimed until shared files are restored; another
11128
+ // process must not publish a new placeholder during our final cleanup.
11129
+ coordinator.routes.clear();
11130
+ restoreChildFacingAuth();
11131
+ unprovisionOwnLoopbacks();
11132
+ try { coordinator.server.close(); } catch { /* already closed */ }
11133
+ coordinator.server = undefined;
11134
+ }
11135
+ if (!coordinator.server && cleanupWithoutProxy) {
11136
+ unprovisionOwnLoopbacks();
11137
+ restoreChildFacingAuth();
11138
+ }
11139
+ if (!coordinator.starting && coordinators.get(SLOT_PROXY_PORT) === coordinator) {
11140
+ coordinators.delete(SLOT_PROXY_PORT);
11141
+ }
11142
+ coordinator.routes.clear();
11143
+ }
11144
+ slotProxyCoordinator = undefined;
11145
+ slotProxyServer = undefined;
11146
+ slotProxyPort = undefined;
11147
+ slotProxyRoutes.clear();
11148
+ return;
10949
11149
  }
11150
+ // Passive children and non-coordinated (proxy-disabled) instances own only
11151
+ // their process-local server. Preserve the pre-existing disabled-proxy cleanup.
11152
+ const publishedSharedFiles = ownsSharedChildPublication();
11153
+ try { slotProxyServer?.close(); } catch { /* already closed */ }
10950
11154
  slotProxyServer = undefined;
10951
11155
  slotProxyPort = undefined;
10952
11156
  slotProxyRoutes.clear();
10953
- if (!publishedSharedFiles) return; // the owner's state outlives this process
11157
+ if (!publishedSharedFiles) return;
10954
11158
  restoreChildFacingAuth();
10955
11159
  unprovisionOwnLoopbacks();
10956
11160
  }
@@ -10962,6 +11166,10 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10962
11166
  async function publishProxiedSlots(ctx: any): Promise<void> {
10963
11167
  if (!config.childProxy) return;
10964
11168
  slotProxyContext = ctx ?? slotProxyContext;
11169
+ const coordinator = slotProxyCoordinator;
11170
+ if (coordinator?.server?.listening && coordinator.members.has(activationOwner) && !sessionClosed) {
11171
+ coordinator.publisher = activationOwner;
11172
+ }
10965
11173
  const parent = readAuthFile();
10966
11174
  const slots = [
10967
11175
  ...new Set([
@@ -10971,12 +11179,20 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10971
11179
  ].filter((id) => proxyFamilyOf(id) !== undefined && isEntryUsable(parent[id]));
10972
11180
  if (slots.length === 0) {
10973
11181
  if (ownsSharedChildPublication()) {
10974
- restoreChildFacingAuth();
11182
+ // Keep OAuth in the parent-only sidecar while any root is active, even
11183
+ // when every credential is unusable. Only final shutdown restores it.
11184
+ coordinator?.routes.clear();
10975
11185
  unprovisionOwnLoopbacks();
10976
11186
  }
11187
+ slotProxyRoutes.clear();
11188
+ activateSharedProxy();
10977
11189
  return;
10978
11190
  }
10979
11191
  const port = await startSlotProxy();
11192
+ if (sessionClosed) return;
11193
+ if (port === SLOT_PROXY_PORT && coordinator?.members.has(activationOwner)) {
11194
+ coordinator.publisher = activationOwner;
11195
+ }
10980
11196
  if (port === undefined) {
10981
11197
  if (ownsSharedChildPublication()) {
10982
11198
  restoreChildFacingAuth();
@@ -10992,8 +11208,21 @@ export default function piMultiAccount(pi: ExtensionAPI) {
10992
11208
  }
10993
11209
  if (!ownsSharedChildPublication()) {
10994
11210
  logEvent("slot_proxy_local_routes_ready", { port, slots });
11211
+ activateSharedProxy();
10995
11212
  return;
10996
11213
  }
11214
+ coordinator?.routes.clear();
11215
+ for (const id of slots) {
11216
+ const family = proxyFamilyOf(id);
11217
+ if (family) coordinator?.routes.set(id, { slotId: id, family });
11218
+ }
11219
+ // A proxy-disabled root may have registered a public URL before this root
11220
+ // started. Retire every such route synchronously BEFORE publishing a placeholder.
11221
+ for (const [owner, member] of coordinator?.members ?? []) {
11222
+ if (owner === activationOwner || member.proxyEnabled) continue;
11223
+ if (!member.prepareRoutes(slots, port)) return;
11224
+ member.ready = true;
11225
+ }
10997
11226
  shadowChildFacingAuth(slots);
10998
11227
  for (const id of slots) {
10999
11228
  const family = proxyFamilyOf(id);
@@ -11029,6 +11258,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11029
11258
  });
11030
11259
  }
11031
11260
  }
11261
+ activateSharedProxy();
11032
11262
  logEvent("slot_proxy_published", { port, slots });
11033
11263
  }
11034
11264
 
@@ -11236,13 +11466,23 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11236
11466
  const startEpoch = ++chainEpoch;
11237
11467
  sessionClosed = false;
11238
11468
  hostOwnsSessionModel = typeof ctx?.sessionManager?.getBranch === "function";
11239
- if (hostOwnsSessionModel && !subagentChild) {
11240
- if (activations[activationKey] && activations[activationKey] !== activationOwner) {
11241
- subagentChild = true;
11242
- } else {
11243
- activations[activationKey] = activationOwner;
11469
+ const rawSessionId = ctx?.sessionManager?.getSessionId?.();
11470
+ const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0
11471
+ ? rawSessionId : undefined;
11472
+ if (hostOwnsSessionModel && !subagentChild && !multiSessionHost) {
11473
+ const previous = activations[activationKey];
11474
+ if (previous && previous.owner !== activationOwner) {
11475
+ if (sessionId && previous.sessionId === sessionId && typeof previous.relinquish === "function") {
11476
+ previous.relinquish();
11477
+ } else {
11478
+ subagentChild = true;
11479
+ }
11244
11480
  }
11481
+ if (!subagentChild) activations[activationKey] = {
11482
+ owner: activationOwner, sessionId, relinquish: relinquishRoot,
11483
+ };
11245
11484
  }
11485
+ if (!subagentChild) joinSharedProxy(sessionId);
11246
11486
  // Pi's branch and SDK launch model belong to THIS session. Shared account
11247
11487
  // telemetry (or another pane's legacy preference) cannot override them.
11248
11488
  startupModel = ctx?.model;
@@ -11276,11 +11516,19 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11276
11516
  slotProxyContext = ctx;
11277
11517
  // Bind the loopback before discovery so numbered slots register against this process,
11278
11518
  // not the real upstream, while a child-facing placeholder is in auth.json.
11279
- if (config.childProxy) await startSlotProxy();
11519
+ if (config.childProxy) {
11520
+ await startSlotProxy();
11521
+ } else if (slotProxyCoordinator) {
11522
+ const cohort = slotProxyCoordinator;
11523
+ if (cohort.starting) await cohort.starting;
11524
+ if (cohort.server?.listening) prepareSharedRoutes([...cohort.routes.keys()], SLOT_PROXY_PORT);
11525
+ }
11526
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11280
11527
  refreshDiscovery(true, ctx);
11281
11528
  // Publish the OAuth slots against a route this process serves, so anything spawned
11282
11529
  // without this extension can actually run on the account the rotation chose.
11283
11530
  await publishProxiedSlots(ctx);
11531
+ if (!config.childProxy) activateSharedProxy();
11284
11532
  publishOwnedNativeAliases(ctx);
11285
11533
  // Not installed → silent skip (refreshCursorSlots warns only on the explicit path).
11286
11534
  if (config.includeCursor) await refreshCursorSlots(readAuthFile(), ctx);
@@ -11334,22 +11582,28 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11334
11582
  // runner owns fallback selection in a child, so multiplying that fleet-wide probe across
11335
11583
  // every parallel child buys nothing.
11336
11584
  if (!subagentChild) await refreshRotationUsage(ctx);
11585
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11337
11586
  await syncCodexModelCatalog(ctx);
11587
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11338
11588
  await syncOllamaModelCatalog(ctx);
11589
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11339
11590
  // Pi restores the session model BEFORE extension catalogs finish registering
11340
11591
  // unless the factory returned the Cursor setup promise. Re-apply anyway: a
11341
11592
  // cold catalog, a compaction inner session, or a git-reset leftover can still
11342
11593
  // leave getModel(cursor, grok-4.6) empty.
11343
11594
  if (cursorReady) await cursorReady.catch(() => undefined);
11595
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11344
11596
  // Cursor slot catalogs changed too — same stale-hidden-copy repair as the model syncs.
11345
11597
  applyOnlyActiveFilter(ctx);
11346
11598
  if (!subagentChild) {
11347
11599
  await restoreRememberedModel(ctx);
11600
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11348
11601
  await ensureReadyModel(
11349
11602
  ctx,
11350
11603
  "startup preflight: selected account unavailable",
11351
11604
  );
11352
11605
  }
11606
+ if (sessionClosed || chainEpoch !== startEpoch) return;
11353
11607
  startUsageStatusTimer(ctx);
11354
11608
  });
11355
11609
 
@@ -11358,10 +11612,10 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11358
11612
  // Kill every timer and drop the pending continuation so nothing survives the session.
11359
11613
  safeOn("session_shutdown", async (_event, ctx) => {
11360
11614
  chainEpoch++;
11361
- if (activations[activationKey] === activationOwner) delete activations[activationKey];
11615
+ if (activations[activationKey]?.owner === activationOwner) delete activations[activationKey];
11362
11616
  sessionClosed = true;
11363
11617
  completionRouterContext = undefined;
11364
- rememberUserModel(ctx?.model);
11618
+ if (!supersededRoot) rememberUserModel(ctx?.model);
11365
11619
  if (pendingWakeTimer) {
11366
11620
  clearTimeout(pendingWakeTimer);
11367
11621
  pendingWakeTimer = undefined;
@@ -11373,7 +11627,7 @@ export default function piMultiAccount(pi: ExtensionAPI) {
11373
11627
  contextGuardCompactionRetryAfter = 0;
11374
11628
  settleContextGuardCompaction();
11375
11629
  endResumeWatch();
11376
- // The published routes point at this process; nothing must be left listening after it.
11630
+ // Release this root's membership; only the final root closes shared child routes.
11377
11631
  stopSlotProxy();
11378
11632
  watchdogAborting = false;
11379
11633
  expectingInjectedContinuation = false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multi-account",
3
- "version": "1.23.1",
3
+ "version": "1.23.2",
4
4
  "description": "Automatic multi-account failover & rotation for Pi Agent across Anthropic (Claude), OpenAI/ChatGPT Codex, Kimi For Coding, Cursor, Qwen/Alibaba, and Ollama. Auto-discovers authenticated accounts, grows the rotation on login, and drops accounts on logout, expiry, or quota/rate-limit errors",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,20 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ - uses: actions/setup-node@v6
16
+ with:
17
+ node-version: 24
18
+ registry-url: https://registry.npmjs.org
19
+ package-manager-cache: false
20
+ - run: npm publish --access public
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ouzhenkun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.