@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.
- package/README.md +8 -3
- package/THIRD_PARTY_NOTICES.md +28 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
- package/node_modules/pi-jev-auto-mode/README.md +7 -0
- package/node_modules/pi-jev-auto-mode/package.json +1 -1
- package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
- package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
- package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
- package/node_modules/pi-multi-account/README.md +15 -1
- package/node_modules/pi-multi-account/index.ts +295 -41
- package/node_modules/pi-multi-account/package.json +1 -1
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- 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.
|
|
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.
|
|
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
|
-
//
|
|
3937
|
-
//
|
|
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 & {
|
|
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)
|
|
10658
|
-
|
|
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
|
-
//
|
|
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
|
|
10711
|
-
*
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10933
|
-
resolve(
|
|
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
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
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)
|
|
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
|
-
//
|
|
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.
|
|
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.
|