@code-yeongyu/senpi 2026.8.27 → 2026.8.28-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +2 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +1 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +3 -0
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/credential-accounts.d.ts +31 -0
- package/dist/core/credential-accounts.d.ts.map +1 -0
- package/dist/core/credential-accounts.js +135 -0
- package/dist/core/credential-accounts.js.map +1 -0
- package/dist/core/credential-pool/classify.d.ts +6 -1
- package/dist/core/credential-pool/classify.d.ts.map +1 -1
- package/dist/core/credential-pool/classify.js +22 -5
- package/dist/core/credential-pool/classify.js.map +1 -1
- package/dist/core/credential-pool/failover.d.ts +8 -0
- package/dist/core/credential-pool/failover.d.ts.map +1 -1
- package/dist/core/credential-pool/failover.js +24 -6
- package/dist/core/credential-pool/failover.js.map +1 -1
- package/dist/core/credential-pool/rotation-stream.d.ts +13 -2
- package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
- package/dist/core/credential-pool/rotation-stream.js +97 -10
- package/dist/core/credential-pool/rotation-stream.js.map +1 -1
- package/dist/core/extensions/builtin/account/index.d.ts +9 -0
- package/dist/core/extensions/builtin/account/index.d.ts.map +1 -0
- package/dist/core/extensions/builtin/account/index.js +69 -0
- package/dist/core/extensions/builtin/account/index.js.map +1 -0
- package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/goal/index.js +7 -0
- package/dist/core/extensions/builtin/goal/index.js.map +1 -1
- package/dist/core/extensions/builtin/imagegen/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/imagegen/index.js +10 -3
- package/dist/core/extensions/builtin/imagegen/index.js.map +1 -1
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +4 -0
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/builtin/service-tier.d.ts.map +1 -1
- package/dist/core/extensions/builtin/service-tier.js +13 -0
- package/dist/core/extensions/builtin/service-tier.js.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
- package/dist/core/extensions/builtin/terminal/extension.js +4 -2
- package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
- package/dist/core/model-config-schema.d.ts +4 -4
- package/dist/core/model-runtime.d.ts +2 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +74 -37
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/retry-fallback/chains.d.ts +23 -1
- package/dist/core/retry-fallback/chains.d.ts.map +1 -1
- package/dist/core/retry-fallback/chains.js +59 -3
- package/dist/core/retry-fallback/chains.js.map +1 -1
- package/dist/core/retry-fallback/controller.d.ts.map +1 -1
- package/dist/core/retry-fallback/controller.js +9 -2
- package/dist/core/retry-fallback/controller.js.map +1 -1
- package/dist/core/retry-fallback/settings.d.ts.map +1 -1
- package/dist/core/retry-fallback/settings.js +9 -0
- package/dist/core/retry-fallback/settings.js.map +1 -1
- package/dist/core/retry-fallback/validate.js +1 -1
- package/dist/core/retry-fallback/validate.js.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +9 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +31 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/main.d.ts +7 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/app-server/server/account.d.ts.map +1 -1
- package/dist/modes/app-server/server/account.js +5 -5
- package/dist/modes/app-server/server/account.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +11 -3
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +31 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/grok/chrome.d.ts +4 -4
- package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
- package/dist/modes/interactive/grok/chrome.js.map +1 -1
- package/dist/modes/interactive/grok/footer.d.ts +3 -3
- package/dist/modes/interactive/grok/footer.d.ts.map +1 -1
- package/dist/modes/interactive/grok/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.d.ts +16 -1
- package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-host-runtime.js +321 -36
- package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +295 -250
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
- package/dist/modes/rpc/connection-handler.js +144 -29
- package/dist/modes/rpc/connection-handler.js.map +1 -1
- package/dist/modes/rpc/multi-session-host.js +14 -4
- package/dist/modes/rpc/multi-session-host.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +90 -8
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +118 -23
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +3 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +3 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +164 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/modes/rpc/session-command-router.d.ts +22 -0
- package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
- package/dist/modes/rpc/session-command-router.js +153 -14
- package/dist/modes/rpc/session-command-router.js.map +1 -1
- package/dist/modes/rpc/session-event-writer.d.ts +2 -0
- package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
- package/dist/modes/rpc/session-event-writer.js +4 -0
- package/dist/modes/rpc/session-event-writer.js.map +1 -1
- package/dist/modes/rpc/session-registry.d.ts +16 -0
- package/dist/modes/rpc/session-registry.d.ts.map +1 -1
- package/dist/modes/rpc/session-registry.js +40 -3
- package/dist/modes/rpc/session-registry.js.map +1 -1
- package/docs/providers.md +48 -0
- package/docs/rpc.md +43 -4
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +26 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +13 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +15 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +14 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js +20 -4
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts +21 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js +27 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js.map +1 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts +24 -0
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js +7 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js +2 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/auth/types.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/alibaba-token-plan.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
|
@@ -6,6 +6,11 @@ export type RunSlot = {
|
|
|
6
6
|
blockedUntil?: number;
|
|
7
7
|
blockReason?: string;
|
|
8
8
|
failureCount?: number;
|
|
9
|
+
lease?: {
|
|
10
|
+
id: string;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
};
|
|
13
|
+
pinned?: boolean;
|
|
9
14
|
};
|
|
10
15
|
export type CredentialFailoverEvent<TSlot extends RunSlot> = {
|
|
11
16
|
slot: TSlot;
|
|
@@ -26,11 +31,14 @@ export type RunCredentialFailoverOptions<TEvent, TSlot extends RunSlot> = {
|
|
|
26
31
|
isCommittedOutput: (event: TEvent) => boolean;
|
|
27
32
|
classify?: (error: unknown, context: {
|
|
28
33
|
failureCount: number;
|
|
34
|
+
cooldownBaseMs?: number;
|
|
35
|
+
cooldownCapMs?: number;
|
|
29
36
|
}) => CredentialAction;
|
|
30
37
|
errorFromEvent?: (event: TEvent) => unknown | undefined;
|
|
31
38
|
/** Persisted BEFORE a replacement slot is selected so a crash never forgets a block. */
|
|
32
39
|
persistBlock: (slot: TSlot, block: CredentialBlock) => void | Promise<void>;
|
|
33
40
|
onRotate?: (event: CredentialFailoverEvent<TSlot>) => void | Promise<void>;
|
|
41
|
+
onSuccess?: (slot: TSlot) => void | Promise<void>;
|
|
34
42
|
now?: () => number;
|
|
35
43
|
};
|
|
36
44
|
export declare class CredentialFailoverError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"failover.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/failover.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,0CAA0C,CAAC;AACzF,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEvG,OAAO,EAAE,6BAA6B,EAAE,CAAC;AAEzC,MAAM,MAAM,OAAO,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"failover.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/failover.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,0CAA0C,CAAC;AACzF,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAA6B,MAAM,eAAe,CAAC;AAEvG,OAAO,EAAE,6BAA6B,EAAE,CAAC;AAEzC,MAAM,MAAM,OAAO,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,KAAK,SAAS,OAAO,IAAI;IAC5D,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,4BAA4B,CAAC,MAAM,EAAE,KAAK,SAAS,OAAO,IAAI;IACzE,2FAA2F;IAC3F,SAAS,EAAE,MAAM,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,EAAE,CAAC;IAC9D,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,EAAE,KAAK,KAAK,CAAC;IAC3C,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IACpF;;;;OAIG;IACH,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,QAAQ,CAAC,EAAE,CACV,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAC9E,gBAAgB,CAAC;IACtB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,CAAC;IACxD,wFAAwF;IACxF,YAAY,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,CAAC,KAAK,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEzB,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,iBAAiB,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;CASlH;AA4BD;;;;;;GAMG;AACH,wBAAuB,qBAAqB,CAAC,MAAM,EAAE,KAAK,SAAS,OAAO,EACzE,OAAO,EAAE,4BAA4B,CAAC,MAAM,EAAE,KAAK,CAAC,GAClD,cAAc,CAAC,MAAM,CAAC,CAyExB"}
|
|
@@ -15,7 +15,11 @@ export class CredentialFailoverError extends Error {
|
|
|
15
15
|
function isAvailable(slot, now) {
|
|
16
16
|
if (slot.blockReason === "auth_error" || slot.blockReason === "account_disabled")
|
|
17
17
|
return false;
|
|
18
|
-
|
|
18
|
+
if (slot.blockedUntil !== undefined && slot.blockedUntil > now)
|
|
19
|
+
return false;
|
|
20
|
+
// A live lease marks the one caller admitted to run the half-open probe.
|
|
21
|
+
// listSlots excludes that lease for later callers; the holder must remain runnable.
|
|
22
|
+
return true;
|
|
19
23
|
}
|
|
20
24
|
function soonestRetryAt(slots, now) {
|
|
21
25
|
const deadlines = slots
|
|
@@ -75,29 +79,43 @@ export async function* runCredentialFailover(options) {
|
|
|
75
79
|
committedOutput ||= options.isCommittedOutput(event);
|
|
76
80
|
yield event;
|
|
77
81
|
}
|
|
82
|
+
await options.onSuccess?.(slot);
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
catch (error) {
|
|
81
86
|
if (attemptStream)
|
|
82
87
|
await settle(attemptStream);
|
|
83
88
|
const failureCount = (slot.failureCount ?? 0) + (retriesBySlot.get(slot.name) ?? 0);
|
|
84
|
-
const action = classify(error, {
|
|
89
|
+
const action = classify(error, {
|
|
90
|
+
failureCount,
|
|
91
|
+
});
|
|
85
92
|
lastOriginal = error;
|
|
86
93
|
if (action.kind === "retry_same" && !committedOutput) {
|
|
87
94
|
const used = (retriesBySlot.get(slot.name) ?? 0) + 1;
|
|
88
95
|
retriesBySlot.set(slot.name, used);
|
|
89
96
|
if (used < action.maxAttempts)
|
|
90
97
|
continue;
|
|
91
|
-
throw new CredentialFailoverError(action, error, {
|
|
98
|
+
throw new CredentialFailoverError(action, error, {
|
|
99
|
+
suppressTurnRetry: false,
|
|
100
|
+
});
|
|
92
101
|
}
|
|
93
102
|
if (action.kind !== "failover") {
|
|
94
|
-
throw new CredentialFailoverError(action, error, {
|
|
103
|
+
throw new CredentialFailoverError(action, error, {
|
|
104
|
+
suppressTurnRetry: committedOutput,
|
|
105
|
+
});
|
|
95
106
|
}
|
|
96
107
|
await options.persistBlock(slot, action.block);
|
|
97
108
|
attempted.add(slot.name);
|
|
98
|
-
const failure = new CredentialFailoverError(action, error, {
|
|
109
|
+
const failure = new CredentialFailoverError(action, error, {
|
|
110
|
+
suppressTurnRetry: committedOutput,
|
|
111
|
+
});
|
|
99
112
|
lastError = failure;
|
|
100
|
-
await options.onRotate?.({
|
|
113
|
+
await options.onRotate?.({
|
|
114
|
+
slot,
|
|
115
|
+
block: action.block,
|
|
116
|
+
attempt: attempted.size,
|
|
117
|
+
committedOutput,
|
|
118
|
+
});
|
|
101
119
|
if (committedOutput)
|
|
102
120
|
throw failure;
|
|
103
121
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"failover.js","sourceRoot":"","sources":["../../../src/core/credential-pool/failover.ts"],"names":[],"mappings":"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","sourcesContent":["import { TURN_RETRY_SUPPRESSION_PREFIX } from \"@earendil-works/pi-ai/auth/pool/failover\";\nimport { type CredentialAction, type CredentialBlock, classifyCredentialFailure } from \"./classify.ts\";\n\nexport { TURN_RETRY_SUPPRESSION_PREFIX };\n\nexport type RunSlot = {\n\tname: string;\n\tblockedUntil?: number;\n\tblockReason?: string;\n\tfailureCount?: number;\n};\n\nexport type CredentialFailoverEvent<TSlot extends RunSlot> = {\n\tslot: TSlot;\n\tblock: CredentialBlock;\n\tattempt: number;\n\tcommittedOutput: boolean;\n};\n\nexport type RunCredentialFailoverOptions<TEvent, TSlot extends RunSlot> = {\n\t/** Re-read before every distinct-credential attempt so a newly added slot participates. */\n\tlistSlots: () => Promise<readonly TSlot[]> | readonly TSlot[];\n\tselect: (slots: readonly TSlot[]) => TSlot;\n\trunAttempt: (slot: TSlot) => AsyncIterable<TEvent> | Promise<AsyncIterable<TEvent>>;\n\t/**\n\t * REQUIRED and default-DENY by contract: return false only for event types\n\t * explicitly known to be pre-commit bookkeeping. Any event this predicate\n\t * does not recognize must count as committed output.\n\t */\n\tisCommittedOutput: (event: TEvent) => boolean;\n\tclassify?: (error: unknown, context: { failureCount: number }) => CredentialAction;\n\terrorFromEvent?: (event: TEvent) => unknown | undefined;\n\t/** Persisted BEFORE a replacement slot is selected so a crash never forgets a block. */\n\tpersistBlock: (slot: TSlot, block: CredentialBlock) => void | Promise<void>;\n\tonRotate?: (event: CredentialFailoverEvent<TSlot>) => void | Promise<void>;\n\tnow?: () => number;\n};\n\nexport class CredentialFailoverError extends Error {\n\treadonly action: CredentialAction;\n\treadonly original: unknown;\n\treadonly suppressTurnRetry: boolean;\n\treadonly retryAt: number | undefined;\n\n\tconstructor(action: CredentialAction, original: unknown, options: { suppressTurnRetry: boolean; retryAt?: number }) {\n\t\tconst detail = original instanceof Error ? original.message : String(original);\n\t\tsuper(`${options.suppressTurnRetry ? TURN_RETRY_SUPPRESSION_PREFIX : \"\"}${detail}`, { cause: original });\n\t\tthis.name = \"CredentialFailoverError\";\n\t\tthis.action = action;\n\t\tthis.original = original;\n\t\tthis.suppressTurnRetry = options.suppressTurnRetry;\n\t\tthis.retryAt = options.retryAt;\n\t}\n}\n\nfunction isAvailable(slot: RunSlot, now: number): boolean {\n\tif (slot.blockReason === \"auth_error\" || slot.blockReason === \"account_disabled\") return false;\n\treturn slot.blockedUntil === undefined || slot.blockedUntil <= now;\n}\n\nfunction soonestRetryAt(slots: readonly RunSlot[], now: number): number | undefined {\n\tconst deadlines = slots\n\t\t.map((slot) => slot.blockedUntil)\n\t\t.filter((value): value is number => value !== undefined && value > now);\n\treturn deadlines.length === 0 ? undefined : Math.min(...deadlines);\n}\n\nasync function settle<TEvent>(stream: AsyncIterable<TEvent>): Promise<void> {\n\t// A failed attempt must fully release its transport before a replacement\n\t// starts, or two live streams can interleave provider-side effects.\n\tconst iterator = stream[Symbol.asyncIterator]();\n\ttry {\n\t\tawait iterator.return?.(undefined);\n\t} catch {\n\t\t// Settling a dead stream must never mask the original failure.\n\t}\n}\n\n/**\n * Generic in-lane credential failover. Runs at most one failover attempt per\n * slot per request, retries provider-scoped faults on the SAME slot up to the\n * classifier's bound without blocking it, and rotates only while no committed\n * output has reached the caller - afterwards the failure is rethrown with the\n * `senpi:no-turn-retry:` marker so the turn is never replayed.\n */\nexport async function* runCredentialFailover<TEvent, TSlot extends RunSlot>(\n\toptions: RunCredentialFailoverOptions<TEvent, TSlot>,\n): AsyncGenerator<TEvent> {\n\tconst now = options.now ?? Date.now;\n\tconst classify = options.classify ?? classifyCredentialFailure;\n\tconst attempted = new Set<string>();\n\tconst retriesBySlot = new Map<string, number>();\n\tlet lastError: CredentialFailoverError | undefined;\n\tlet lastOriginal: unknown;\n\n\twhile (true) {\n\t\tconst slots = await options.listSlots();\n\t\tconst candidates = slots.filter((slot) => !attempted.has(slot.name) && isAvailable(slot, now()));\n\t\tif (candidates.length === 0) {\n\t\t\tconst retryAt = soonestRetryAt(slots, now());\n\t\t\tthrow lastError !== undefined && lastOriginal !== undefined\n\t\t\t\t? new CredentialFailoverError(lastError.action, lastOriginal, {\n\t\t\t\t\t\tsuppressTurnRetry: lastError.suppressTurnRetry,\n\t\t\t\t\t\t...(retryAt === undefined ? {} : { retryAt }),\n\t\t\t\t\t})\n\t\t\t\t: new CredentialFailoverError({ kind: \"fail_request\" }, new Error(\"No credential slots available\"), {\n\t\t\t\t\t\tsuppressTurnRetry: false,\n\t\t\t\t\t\t...(retryAt === undefined ? {} : { retryAt }),\n\t\t\t\t\t});\n\t\t}\n\t\tconst slot = options.select(candidates);\n\t\tlet committedOutput = false;\n\t\tlet attemptStream: AsyncIterable<TEvent> | undefined;\n\t\ttry {\n\t\t\tattemptStream = await options.runAttempt(slot);\n\t\t\tfor await (const event of attemptStream) {\n\t\t\t\tconst failure = options.errorFromEvent?.(event);\n\t\t\t\tif (failure !== undefined) throw failure;\n\t\t\t\tcommittedOutput ||= options.isCommittedOutput(event);\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\treturn;\n\t\t} catch (error) {\n\t\t\tif (attemptStream) await settle(attemptStream);\n\t\t\tconst failureCount = (slot.failureCount ?? 0) + (retriesBySlot.get(slot.name) ?? 0);\n\t\t\tconst action = classify(error, { failureCount });\n\t\t\tlastOriginal = error;\n\n\t\t\tif (action.kind === \"retry_same\" && !committedOutput) {\n\t\t\t\tconst used = (retriesBySlot.get(slot.name) ?? 0) + 1;\n\t\t\t\tretriesBySlot.set(slot.name, used);\n\t\t\t\tif (used < action.maxAttempts) continue;\n\t\t\t\tthrow new CredentialFailoverError(action, error, { suppressTurnRetry: false });\n\t\t\t}\n\t\t\tif (action.kind !== \"failover\") {\n\t\t\t\tthrow new CredentialFailoverError(action, error, { suppressTurnRetry: committedOutput });\n\t\t\t}\n\n\t\t\tawait options.persistBlock(slot, action.block);\n\t\t\tattempted.add(slot.name);\n\t\t\tconst failure = new CredentialFailoverError(action, error, { suppressTurnRetry: committedOutput });\n\t\t\tlastError = failure;\n\t\t\tawait options.onRotate?.({ slot, block: action.block, attempt: attempted.size, committedOutput });\n\t\t\tif (committedOutput) throw failure;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"failover.js","sourceRoot":"","sources":["../../../src/core/credential-pool/failover.ts"],"names":[],"mappings":"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","sourcesContent":["import { TURN_RETRY_SUPPRESSION_PREFIX } from \"@earendil-works/pi-ai/auth/pool/failover\";\nimport { type CredentialAction, type CredentialBlock, classifyCredentialFailure } from \"./classify.ts\";\n\nexport { TURN_RETRY_SUPPRESSION_PREFIX };\n\nexport type RunSlot = {\n\tname: string;\n\tblockedUntil?: number;\n\tblockReason?: string;\n\tfailureCount?: number;\n\tlease?: { id: string; expiresAt: number };\n\tpinned?: boolean;\n};\n\nexport type CredentialFailoverEvent<TSlot extends RunSlot> = {\n\tslot: TSlot;\n\tblock: CredentialBlock;\n\tattempt: number;\n\tcommittedOutput: boolean;\n};\n\nexport type RunCredentialFailoverOptions<TEvent, TSlot extends RunSlot> = {\n\t/** Re-read before every distinct-credential attempt so a newly added slot participates. */\n\tlistSlots: () => Promise<readonly TSlot[]> | readonly TSlot[];\n\tselect: (slots: readonly TSlot[]) => TSlot;\n\trunAttempt: (slot: TSlot) => AsyncIterable<TEvent> | Promise<AsyncIterable<TEvent>>;\n\t/**\n\t * REQUIRED and default-DENY by contract: return false only for event types\n\t * explicitly known to be pre-commit bookkeeping. Any event this predicate\n\t * does not recognize must count as committed output.\n\t */\n\tisCommittedOutput: (event: TEvent) => boolean;\n\tclassify?: (\n\t\terror: unknown,\n\t\tcontext: { failureCount: number; cooldownBaseMs?: number; cooldownCapMs?: number },\n\t) => CredentialAction;\n\terrorFromEvent?: (event: TEvent) => unknown | undefined;\n\t/** Persisted BEFORE a replacement slot is selected so a crash never forgets a block. */\n\tpersistBlock: (slot: TSlot, block: CredentialBlock) => void | Promise<void>;\n\tonRotate?: (event: CredentialFailoverEvent<TSlot>) => void | Promise<void>;\n\tonSuccess?: (slot: TSlot) => void | Promise<void>;\n\tnow?: () => number;\n};\n\nexport class CredentialFailoverError extends Error {\n\treadonly action: CredentialAction;\n\treadonly original: unknown;\n\treadonly suppressTurnRetry: boolean;\n\treadonly retryAt: number | undefined;\n\n\tconstructor(action: CredentialAction, original: unknown, options: { suppressTurnRetry: boolean; retryAt?: number }) {\n\t\tconst detail = original instanceof Error ? original.message : String(original);\n\t\tsuper(`${options.suppressTurnRetry ? TURN_RETRY_SUPPRESSION_PREFIX : \"\"}${detail}`, { cause: original });\n\t\tthis.name = \"CredentialFailoverError\";\n\t\tthis.action = action;\n\t\tthis.original = original;\n\t\tthis.suppressTurnRetry = options.suppressTurnRetry;\n\t\tthis.retryAt = options.retryAt;\n\t}\n}\n\nfunction isAvailable(slot: RunSlot, now: number): boolean {\n\tif (slot.blockReason === \"auth_error\" || slot.blockReason === \"account_disabled\") return false;\n\tif (slot.blockedUntil !== undefined && slot.blockedUntil > now) return false;\n\t// A live lease marks the one caller admitted to run the half-open probe.\n\t// listSlots excludes that lease for later callers; the holder must remain runnable.\n\treturn true;\n}\n\nfunction soonestRetryAt(slots: readonly RunSlot[], now: number): number | undefined {\n\tconst deadlines = slots\n\t\t.map((slot) => slot.blockedUntil)\n\t\t.filter((value): value is number => value !== undefined && value > now);\n\treturn deadlines.length === 0 ? undefined : Math.min(...deadlines);\n}\n\nasync function settle<TEvent>(stream: AsyncIterable<TEvent>): Promise<void> {\n\t// A failed attempt must fully release its transport before a replacement\n\t// starts, or two live streams can interleave provider-side effects.\n\tconst iterator = stream[Symbol.asyncIterator]();\n\ttry {\n\t\tawait iterator.return?.(undefined);\n\t} catch {\n\t\t// Settling a dead stream must never mask the original failure.\n\t}\n}\n\n/**\n * Generic in-lane credential failover. Runs at most one failover attempt per\n * slot per request, retries provider-scoped faults on the SAME slot up to the\n * classifier's bound without blocking it, and rotates only while no committed\n * output has reached the caller - afterwards the failure is rethrown with the\n * `senpi:no-turn-retry:` marker so the turn is never replayed.\n */\nexport async function* runCredentialFailover<TEvent, TSlot extends RunSlot>(\n\toptions: RunCredentialFailoverOptions<TEvent, TSlot>,\n): AsyncGenerator<TEvent> {\n\tconst now = options.now ?? Date.now;\n\tconst classify = options.classify ?? classifyCredentialFailure;\n\tconst attempted = new Set<string>();\n\tconst retriesBySlot = new Map<string, number>();\n\tlet lastError: CredentialFailoverError | undefined;\n\tlet lastOriginal: unknown;\n\n\twhile (true) {\n\t\tconst slots = await options.listSlots();\n\t\tconst candidates = slots.filter((slot) => !attempted.has(slot.name) && isAvailable(slot, now()));\n\t\tif (candidates.length === 0) {\n\t\t\tconst retryAt = soonestRetryAt(slots, now());\n\t\t\tthrow lastError !== undefined && lastOriginal !== undefined\n\t\t\t\t? new CredentialFailoverError(lastError.action, lastOriginal, {\n\t\t\t\t\t\tsuppressTurnRetry: lastError.suppressTurnRetry,\n\t\t\t\t\t\t...(retryAt === undefined ? {} : { retryAt }),\n\t\t\t\t\t})\n\t\t\t\t: new CredentialFailoverError({ kind: \"fail_request\" }, new Error(\"No credential slots available\"), {\n\t\t\t\t\t\tsuppressTurnRetry: false,\n\t\t\t\t\t\t...(retryAt === undefined ? {} : { retryAt }),\n\t\t\t\t\t});\n\t\t}\n\t\tconst slot = options.select(candidates);\n\t\tlet committedOutput = false;\n\t\tlet attemptStream: AsyncIterable<TEvent> | undefined;\n\t\ttry {\n\t\t\tattemptStream = await options.runAttempt(slot);\n\t\t\tfor await (const event of attemptStream) {\n\t\t\t\tconst failure = options.errorFromEvent?.(event);\n\t\t\t\tif (failure !== undefined) throw failure;\n\t\t\t\tcommittedOutput ||= options.isCommittedOutput(event);\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\tawait options.onSuccess?.(slot);\n\t\t\treturn;\n\t\t} catch (error) {\n\t\t\tif (attemptStream) await settle(attemptStream);\n\t\t\tconst failureCount = (slot.failureCount ?? 0) + (retriesBySlot.get(slot.name) ?? 0);\n\t\t\tconst action = classify(error, {\n\t\t\t\tfailureCount,\n\t\t\t});\n\t\t\tlastOriginal = error;\n\n\t\t\tif (action.kind === \"retry_same\" && !committedOutput) {\n\t\t\t\tconst used = (retriesBySlot.get(slot.name) ?? 0) + 1;\n\t\t\t\tretriesBySlot.set(slot.name, used);\n\t\t\t\tif (used < action.maxAttempts) continue;\n\t\t\t\tthrow new CredentialFailoverError(action, error, {\n\t\t\t\t\tsuppressTurnRetry: false,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (action.kind !== \"failover\") {\n\t\t\t\tthrow new CredentialFailoverError(action, error, {\n\t\t\t\t\tsuppressTurnRetry: committedOutput,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tawait options.persistBlock(slot, action.block);\n\t\t\tattempted.add(slot.name);\n\t\t\tconst failure = new CredentialFailoverError(action, error, {\n\t\t\t\tsuppressTurnRetry: committedOutput,\n\t\t\t});\n\t\t\tlastError = failure;\n\t\t\tawait options.onRotate?.({\n\t\t\t\tslot,\n\t\t\t\tblock: action.block,\n\t\t\t\tattempt: attempted.size,\n\t\t\t\tcommittedOutput,\n\t\t\t});\n\t\t\tif (committedOutput) throw failure;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessageEvent, Credential } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
|
|
3
3
|
import { type RunSlot } from "./failover.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { type CredentialSlotRepository } from "./state-store.ts";
|
|
5
5
|
/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
|
|
6
6
|
export declare const sha256SlotHasher: SlotHasher;
|
|
7
7
|
export type RotationLane = "stored" | "env";
|
|
@@ -16,6 +16,15 @@ export type RotationSources = {
|
|
|
16
16
|
credential: Credential | undefined;
|
|
17
17
|
env: (name: string) => string | undefined;
|
|
18
18
|
repository: CredentialSlotRepository;
|
|
19
|
+
policy?: {
|
|
20
|
+
affinity?: boolean;
|
|
21
|
+
cooldownBaseMs?: number;
|
|
22
|
+
cooldownCapMs?: number;
|
|
23
|
+
slots?: Record<string, {
|
|
24
|
+
env?: string;
|
|
25
|
+
value?: string;
|
|
26
|
+
}>;
|
|
27
|
+
};
|
|
19
28
|
now?: () => number;
|
|
20
29
|
};
|
|
21
30
|
/**
|
|
@@ -25,7 +34,9 @@ export type RotationSources = {
|
|
|
25
34
|
* persisted health applies only while its HMAC revision still matches the
|
|
26
35
|
* current env value, so rotating a key in place clears its own stale block.
|
|
27
36
|
*/
|
|
28
|
-
export declare function listRotationSlots(sources: RotationSources
|
|
37
|
+
export declare function listRotationSlots(sources: RotationSources, options?: {
|
|
38
|
+
acquireLeases?: boolean;
|
|
39
|
+
}): Promise<RotationSlot[]>;
|
|
29
40
|
export type CredentialRotationOptions = {
|
|
30
41
|
sources: RotationSources;
|
|
31
42
|
/** Stable session key keeps a session on its slot; absent, each request distributes. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rotation-stream.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;
|
|
1
|
+
{"version":3,"file":"rotation-stream.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,wCAAwC,CAAC;AAK1F,OAAO,EAAE,KAAK,OAAO,EAAyB,MAAM,eAAe,CAAC;AACpE,OAAO,EAAwB,KAAK,wBAAwB,EAA4B,MAAM,kBAAkB,CAAC;AAEjH,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,EAAE,UAAsF,CAAC;AAEtH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE5C,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG;IACpC,IAAI,EAAE,YAAY,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;IACnC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,CAAC,EAAE;QACR,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACzD,CAAC;IACF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB,CAAC;AAaF;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,OAAO,CAAC,YAAY,EAAE,CAAC,CA2DzB;AAiED,MAAM,MAAM,yBAAyB,GAAG;IACvC,OAAO,EAAE,eAAe,CAAC;IACzB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,UAAU,EAAE,CACX,IAAI,EAAE,YAAY,KACd,aAAa,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC,CAAC;CAC1F,CAAC;AAQF;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,yBAAyB,GAChC,cAAc,CAAC,qBAAqB,CAAC,CAmDvC"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
|
|
3
3
|
import { listSlots as listCredentialSlots } from "@earendil-works/pi-ai/auth/pool/slots";
|
|
4
|
+
import { resolveConfigValue } from "../resolve-config-value.js";
|
|
4
5
|
import { classifyCredentialFailure } from "./classify.js";
|
|
5
6
|
import { discoverEnvSlots } from "./env-slots.js";
|
|
6
7
|
import { runCredentialFailover } from "./failover.js";
|
|
8
|
+
import { acquireHalfOpenLease } from "./state-store.js";
|
|
7
9
|
/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
|
|
8
10
|
export const sha256SlotHasher = (input) => createHash("sha256").update(input).digest().readBigUInt64BE(0);
|
|
9
11
|
function overlayState(slot, state) {
|
|
@@ -14,6 +16,7 @@ function overlayState(slot, state) {
|
|
|
14
16
|
...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),
|
|
15
17
|
...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),
|
|
16
18
|
...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),
|
|
19
|
+
...(state.lease === undefined ? {} : { lease: state.lease }),
|
|
17
20
|
};
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
@@ -23,26 +26,87 @@ function overlayState(slot, state) {
|
|
|
23
26
|
* persisted health applies only while its HMAC revision still matches the
|
|
24
27
|
* current env value, so rotating a key in place clears its own stale block.
|
|
25
28
|
*/
|
|
26
|
-
export async function listRotationSlots(sources) {
|
|
29
|
+
export async function listRotationSlots(sources, options = {}) {
|
|
30
|
+
const acquireLeases = options.acquireLeases !== false;
|
|
27
31
|
const { providerId, credential, env, repository } = sources;
|
|
32
|
+
const policySlots = Object.entries(sources.policy?.slots ?? {}).flatMap(([name, ref]) => {
|
|
33
|
+
const envVarName = ref.env ?? `models.json:${name}`;
|
|
34
|
+
const key = ref.env !== undefined ? env(ref.env) : ref.value !== undefined ? resolveConfigValue(ref.value, {}) : undefined;
|
|
35
|
+
if (!key)
|
|
36
|
+
return [];
|
|
37
|
+
return [{ name, envVarName, key, source: "env" }];
|
|
38
|
+
});
|
|
28
39
|
if (credential) {
|
|
29
40
|
const state = await repository.listSlots(providerId, "stored");
|
|
30
|
-
|
|
41
|
+
const slots = [];
|
|
42
|
+
for (const slot of listCredentialSlots(credential)) {
|
|
43
|
+
const current = state[slot.name];
|
|
44
|
+
if (acquireLeases &&
|
|
45
|
+
current?.blockedUntil !== undefined &&
|
|
46
|
+
current.blockedUntil <= (sources.now ?? Date.now)()) {
|
|
47
|
+
const lease = await acquireHalfOpenLease(repository, providerId, "stored", slot.name, {
|
|
48
|
+
now: (sources.now ?? Date.now)(),
|
|
49
|
+
});
|
|
50
|
+
if (!lease)
|
|
51
|
+
continue;
|
|
52
|
+
const leased = await repository.listSlots(providerId, "stored");
|
|
53
|
+
slots.push(overlayState({
|
|
54
|
+
name: slot.name,
|
|
55
|
+
lane: "stored",
|
|
56
|
+
pinned: credential.pinned === slot.name,
|
|
57
|
+
}, leased[slot.name]));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
slots.push(overlayState({
|
|
61
|
+
name: slot.name,
|
|
62
|
+
lane: "stored",
|
|
63
|
+
pinned: credential.pinned === slot.name,
|
|
64
|
+
}, current));
|
|
65
|
+
}
|
|
66
|
+
if (policySlots.length === 0)
|
|
67
|
+
return slots;
|
|
68
|
+
const namedSources = {
|
|
69
|
+
...sources,
|
|
70
|
+
credential: undefined,
|
|
71
|
+
policy: { ...sources.policy, slots: {} },
|
|
72
|
+
};
|
|
73
|
+
const namedSlots = await listEnvRotationSlots(namedSources, policySlots, acquireLeases);
|
|
74
|
+
return [...slots, ...namedSlots];
|
|
31
75
|
}
|
|
32
|
-
const envSlots = discoverEnvSlots(providerId, env);
|
|
76
|
+
const envSlots = [...discoverEnvSlots(providerId, env), ...policySlots];
|
|
77
|
+
return listEnvRotationSlots(sources, envSlots, acquireLeases);
|
|
78
|
+
}
|
|
79
|
+
async function listEnvRotationSlots(sources, envSlots, acquireLeases = true) {
|
|
33
80
|
if (envSlots.length === 0)
|
|
34
81
|
return [];
|
|
82
|
+
const { providerId, repository } = sources;
|
|
35
83
|
const state = await repository.listSlots(providerId, "env");
|
|
36
84
|
const slots = [];
|
|
37
85
|
for (const slot of envSlots) {
|
|
38
86
|
const persisted = state[slot.name];
|
|
39
87
|
const revision = await repository.envCredentialRevision(slot.envVarName, slot.key);
|
|
40
|
-
|
|
41
|
-
|
|
88
|
+
let applicable = persisted?.credentialRevision === revision ? persisted : undefined;
|
|
89
|
+
if (acquireLeases &&
|
|
90
|
+
applicable?.blockedUntil !== undefined &&
|
|
91
|
+
applicable.blockedUntil <= (sources.now ?? Date.now)()) {
|
|
92
|
+
const lease = await acquireHalfOpenLease(repository, providerId, "env", slot.name, {
|
|
93
|
+
now: (sources.now ?? Date.now)(),
|
|
94
|
+
});
|
|
95
|
+
if (!lease)
|
|
96
|
+
continue;
|
|
97
|
+
const leased = await repository.listSlots(providerId, "env");
|
|
98
|
+
applicable = leased[slot.name];
|
|
99
|
+
}
|
|
100
|
+
slots.push(overlayState({
|
|
101
|
+
name: slot.name,
|
|
102
|
+
lane: "env",
|
|
103
|
+
envKey: slot.key,
|
|
104
|
+
envVarName: slot.envVarName,
|
|
105
|
+
}, applicable));
|
|
42
106
|
}
|
|
43
107
|
return slots;
|
|
44
108
|
}
|
|
45
|
-
function blockPatch(block, current, now, credentialRevision) {
|
|
109
|
+
function blockPatch(block, current, now, credentialRevision, policy) {
|
|
46
110
|
const failureCount = (current?.failureCount ?? 0) + 1;
|
|
47
111
|
const base = {
|
|
48
112
|
failureCount,
|
|
@@ -50,7 +114,11 @@ function blockPatch(block, current, now, credentialRevision) {
|
|
|
50
114
|
...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),
|
|
51
115
|
};
|
|
52
116
|
if (block.reason === "rate_limit") {
|
|
53
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
blockedUntil: now + Math.min(policy?.cooldownCapMs ?? block.cooldownMs, block.cooldownMs),
|
|
120
|
+
blockReason: "rate_limit",
|
|
121
|
+
};
|
|
54
122
|
}
|
|
55
123
|
return { ...base, blockReason: block.reason };
|
|
56
124
|
}
|
|
@@ -70,11 +138,15 @@ export function streamWithCredentialRotation(options) {
|
|
|
70
138
|
const { sources, runAttempt } = options;
|
|
71
139
|
const hasher = options.hasher ?? sha256SlotHasher;
|
|
72
140
|
const affinityKey = options.affinityKey ?? randomUUID();
|
|
141
|
+
const useAffinity = sources.policy?.affinity !== false;
|
|
73
142
|
const now = sources.now ?? Date.now;
|
|
74
143
|
return runCredentialFailover({
|
|
75
144
|
listSlots: () => listRotationSlots(sources),
|
|
76
145
|
select: (candidates) => {
|
|
77
|
-
const
|
|
146
|
+
const pinned = candidates.find((candidate) => candidate.pinned === true);
|
|
147
|
+
if (pinned)
|
|
148
|
+
return pinned;
|
|
149
|
+
const ordered = useAffinity ? rendezvousOrder(affinityKey, candidates, hasher) : candidates;
|
|
78
150
|
const winner = ordered[0];
|
|
79
151
|
if (!winner)
|
|
80
152
|
throw new Error("credential rotation selected from an empty candidate set");
|
|
@@ -83,12 +155,27 @@ export function streamWithCredentialRotation(options) {
|
|
|
83
155
|
runAttempt,
|
|
84
156
|
isCommittedOutput: (event) => event.type !== "start",
|
|
85
157
|
errorFromEvent,
|
|
86
|
-
classify: classifyCredentialFailure,
|
|
158
|
+
classify: (error, context) => classifyCredentialFailure(error, {
|
|
159
|
+
...context,
|
|
160
|
+
cooldownBaseMs: sources.policy?.cooldownBaseMs,
|
|
161
|
+
cooldownCapMs: sources.policy?.cooldownCapMs,
|
|
162
|
+
}),
|
|
163
|
+
onSuccess: async (slot) => {
|
|
164
|
+
await sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) => current
|
|
165
|
+
? {
|
|
166
|
+
...current,
|
|
167
|
+
lastSuccessAt: now(),
|
|
168
|
+
lease: undefined,
|
|
169
|
+
blockedUntil: undefined,
|
|
170
|
+
blockReason: undefined,
|
|
171
|
+
}
|
|
172
|
+
: undefined);
|
|
173
|
+
},
|
|
87
174
|
persistBlock: async (slot, block) => {
|
|
88
175
|
const revision = slot.lane === "env" && slot.envVarName !== undefined && slot.envKey !== undefined
|
|
89
176
|
? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)
|
|
90
177
|
: undefined;
|
|
91
|
-
await sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) => blockPatch(block, current, now(), revision));
|
|
178
|
+
await sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) => blockPatch(block, current, now(), revision, sources.policy));
|
|
92
179
|
},
|
|
93
180
|
now,
|
|
94
181
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rotation-stream.js","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport type { AssistantMessageEvent, Credential } from \"@earendil-works/pi-ai\";\nimport { rendezvousOrder, type SlotHasher } from \"@earendil-works/pi-ai/auth/pool/select\";\nimport { listSlots as listCredentialSlots } from \"@earendil-works/pi-ai/auth/pool/slots\";\nimport { type CredentialBlock, classifyCredentialFailure } from \"./classify.ts\";\nimport { discoverEnvSlots } from \"./env-slots.ts\";\nimport { type RunSlot, runCredentialFailover } from \"./failover.ts\";\nimport type { CredentialSlotRepository, CredentialSlotState } from \"./state-store.ts\";\n\n/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */\nexport const sha256SlotHasher: SlotHasher = (input) => createHash(\"sha256\").update(input).digest().readBigUInt64BE(0);\n\nexport type RotationLane = \"stored\" | \"env\";\n\nexport type RotationSlot = RunSlot & {\n\tlane: RotationLane;\n\t/** Env-lane key material for the attempt; never serialized or persisted. */\n\tenvKey?: string;\n\tenvVarName?: string;\n};\n\nexport type RotationSources = {\n\tproviderId: string;\n\tcredential: Credential | undefined;\n\tenv: (name: string) => string | undefined;\n\trepository: CredentialSlotRepository;\n\tnow?: () => number;\n};\n\nfunction overlayState(slot: RotationSlot, state: CredentialSlotState | undefined): RotationSlot {\n\tif (!state) return slot;\n\treturn {\n\t\t...slot,\n\t\t...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),\n\t\t...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),\n\t\t...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),\n\t};\n}\n\n/**\n * Lists the provider's rotation slots with sidecar health overlaid. Stored\n * credentials own the lane when present; env slots participate only when\n * nothing is stored, preserving today's resolution precedence. An env slot's\n * persisted health applies only while its HMAC revision still matches the\n * current env value, so rotating a key in place clears its own stale block.\n */\nexport async function listRotationSlots(sources: RotationSources): Promise<RotationSlot[]> {\n\tconst { providerId, credential, env, repository } = sources;\n\tif (credential) {\n\t\tconst state = await repository.listSlots(providerId, \"stored\");\n\t\treturn listCredentialSlots(credential).map((slot) =>\n\t\t\toverlayState({ name: slot.name, lane: \"stored\" }, state[slot.name]),\n\t\t);\n\t}\n\tconst envSlots = discoverEnvSlots(providerId, env);\n\tif (envSlots.length === 0) return [];\n\tconst state = await repository.listSlots(providerId, \"env\");\n\tconst slots: RotationSlot[] = [];\n\tfor (const slot of envSlots) {\n\t\tconst persisted = state[slot.name];\n\t\tconst revision = await repository.envCredentialRevision(slot.envVarName, slot.key);\n\t\tconst applicable = persisted?.credentialRevision === revision ? persisted : undefined;\n\t\tslots.push(\n\t\t\toverlayState({ name: slot.name, lane: \"env\", envKey: slot.key, envVarName: slot.envVarName }, applicable),\n\t\t);\n\t}\n\treturn slots;\n}\n\nfunction blockPatch(\n\tblock: CredentialBlock,\n\tcurrent: CredentialSlotState | undefined,\n\tnow: number,\n\tcredentialRevision?: string,\n): Omit<CredentialSlotState, \"stateVersion\"> {\n\tconst failureCount = (current?.failureCount ?? 0) + 1;\n\tconst base = {\n\t\tfailureCount,\n\t\t...(credentialRevision === undefined ? {} : { credentialRevision }),\n\t\t...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),\n\t};\n\tif (block.reason === \"rate_limit\") {\n\t\treturn { ...base, blockedUntil: now + block.cooldownMs, blockReason: \"rate_limit\" };\n\t}\n\treturn { ...base, blockReason: block.reason };\n}\n\nexport type CredentialRotationOptions = {\n\tsources: RotationSources;\n\t/** Stable session key keeps a session on its slot; absent, each request distributes. */\n\taffinityKey?: string;\n\thasher?: SlotHasher;\n\trunAttempt: (\n\t\tslot: RotationSlot,\n\t) => AsyncIterable<AssistantMessageEvent> | Promise<AsyncIterable<AssistantMessageEvent>>;\n};\n\nfunction errorFromEvent(event: AssistantMessageEvent): unknown {\n\tif (event.type !== \"error\") return undefined;\n\tconst message = event.error.errorMessage ?? \"provider stream error\";\n\treturn new Error(message);\n}\n\n/**\n * In-lane credential rotation for one provider request. Selection follows the\n * HRW order for the affinity key; only the `start` bookkeeping event counts as\n * pre-commit, so any delta bars silent rotation (default-DENY) and failures\n * after output carry the turn-retry suppression marker.\n */\nexport function streamWithCredentialRotation(\n\toptions: CredentialRotationOptions,\n): AsyncGenerator<AssistantMessageEvent> {\n\tconst { sources, runAttempt } = options;\n\tconst hasher = options.hasher ?? sha256SlotHasher;\n\tconst affinityKey = options.affinityKey ?? randomUUID();\n\tconst now = sources.now ?? Date.now;\n\n\treturn runCredentialFailover<AssistantMessageEvent, RotationSlot>({\n\t\tlistSlots: () => listRotationSlots(sources),\n\t\tselect: (candidates) => {\n\t\t\tconst ordered = rendezvousOrder(affinityKey, candidates, hasher);\n\t\t\tconst winner = ordered[0];\n\t\t\tif (!winner) throw new Error(\"credential rotation selected from an empty candidate set\");\n\t\t\treturn winner;\n\t\t},\n\t\trunAttempt,\n\t\tisCommittedOutput: (event) => event.type !== \"start\",\n\t\terrorFromEvent,\n\t\tclassify: classifyCredentialFailure,\n\t\tpersistBlock: async (slot, block) => {\n\t\t\tconst revision =\n\t\t\t\tslot.lane === \"env\" && slot.envVarName !== undefined && slot.envKey !== undefined\n\t\t\t\t\t? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)\n\t\t\t\t\t: undefined;\n\t\t\tawait sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) =>\n\t\t\t\tblockPatch(block, current, now(), revision),\n\t\t\t);\n\t\t},\n\t\tnow,\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rotation-stream.js","sourceRoot":"","sources":["../../../src/core/credential-pool/rotation-stream.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport type { AssistantMessageEvent, Credential } from \"@earendil-works/pi-ai\";\nimport { rendezvousOrder, type SlotHasher } from \"@earendil-works/pi-ai/auth/pool/select\";\nimport { listSlots as listCredentialSlots, type PooledCredential } from \"@earendil-works/pi-ai/auth/pool/slots\";\nimport { resolveConfigValue } from \"../resolve-config-value.ts\";\nimport { type CredentialBlock, classifyCredentialFailure } from \"./classify.ts\";\nimport { discoverEnvSlots } from \"./env-slots.ts\";\nimport { type RunSlot, runCredentialFailover } from \"./failover.ts\";\nimport { acquireHalfOpenLease, type CredentialSlotRepository, type CredentialSlotState } from \"./state-store.ts\";\n\n/** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */\nexport const sha256SlotHasher: SlotHasher = (input) => createHash(\"sha256\").update(input).digest().readBigUInt64BE(0);\n\nexport type RotationLane = \"stored\" | \"env\";\n\nexport type RotationSlot = RunSlot & {\n\tlane: RotationLane;\n\t/** Env-lane key material for the attempt; never serialized or persisted. */\n\tenvKey?: string;\n\tenvVarName?: string;\n};\n\nexport type RotationSources = {\n\tproviderId: string;\n\tcredential: Credential | undefined;\n\tenv: (name: string) => string | undefined;\n\trepository: CredentialSlotRepository;\n\tpolicy?: {\n\t\taffinity?: boolean;\n\t\tcooldownBaseMs?: number;\n\t\tcooldownCapMs?: number;\n\t\tslots?: Record<string, { env?: string; value?: string }>;\n\t};\n\tnow?: () => number;\n};\n\nfunction overlayState(slot: RotationSlot, state: CredentialSlotState | undefined): RotationSlot {\n\tif (!state) return slot;\n\treturn {\n\t\t...slot,\n\t\t...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),\n\t\t...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),\n\t\t...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),\n\t\t...(state.lease === undefined ? {} : { lease: state.lease }),\n\t};\n}\n\n/**\n * Lists the provider's rotation slots with sidecar health overlaid. Stored\n * credentials own the lane when present; env slots participate only when\n * nothing is stored, preserving today's resolution precedence. An env slot's\n * persisted health applies only while its HMAC revision still matches the\n * current env value, so rotating a key in place clears its own stale block.\n */\nexport async function listRotationSlots(\n\tsources: RotationSources,\n\toptions: { acquireLeases?: boolean } = {},\n): Promise<RotationSlot[]> {\n\tconst acquireLeases = options.acquireLeases !== false;\n\tconst { providerId, credential, env, repository } = sources;\n\tconst policySlots = Object.entries(sources.policy?.slots ?? {}).flatMap(([name, ref]) => {\n\t\tconst envVarName = ref.env ?? `models.json:${name}`;\n\t\tconst key =\n\t\t\tref.env !== undefined ? env(ref.env) : ref.value !== undefined ? resolveConfigValue(ref.value, {}) : undefined;\n\t\tif (!key) return [];\n\t\treturn [{ name, envVarName, key, source: \"env\" as const }];\n\t});\n\tif (credential) {\n\t\tconst state = await repository.listSlots(providerId, \"stored\");\n\t\tconst slots: RotationSlot[] = [];\n\t\tfor (const slot of listCredentialSlots(credential)) {\n\t\t\tconst current = state[slot.name];\n\t\t\tif (\n\t\t\t\tacquireLeases &&\n\t\t\t\tcurrent?.blockedUntil !== undefined &&\n\t\t\t\tcurrent.blockedUntil <= (sources.now ?? Date.now)()\n\t\t\t) {\n\t\t\t\tconst lease = await acquireHalfOpenLease(repository, providerId, \"stored\", slot.name, {\n\t\t\t\t\tnow: (sources.now ?? Date.now)(),\n\t\t\t\t});\n\t\t\t\tif (!lease) continue;\n\t\t\t\tconst leased = await repository.listSlots(providerId, \"stored\");\n\t\t\t\tslots.push(\n\t\t\t\t\toverlayState(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: slot.name,\n\t\t\t\t\t\t\tlane: \"stored\",\n\t\t\t\t\t\t\tpinned: (credential as PooledCredential).pinned === slot.name,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tleased[slot.name],\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tslots.push(\n\t\t\t\toverlayState(\n\t\t\t\t\t{\n\t\t\t\t\t\tname: slot.name,\n\t\t\t\t\t\tlane: \"stored\",\n\t\t\t\t\t\tpinned: (credential as PooledCredential).pinned === slot.name,\n\t\t\t\t\t},\n\t\t\t\t\tcurrent,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tif (policySlots.length === 0) return slots;\n\t\tconst namedSources: RotationSources = {\n\t\t\t...sources,\n\t\t\tcredential: undefined,\n\t\t\tpolicy: { ...sources.policy, slots: {} },\n\t\t};\n\t\tconst namedSlots = await listEnvRotationSlots(namedSources, policySlots, acquireLeases);\n\t\treturn [...slots, ...namedSlots];\n\t}\n\tconst envSlots = [...discoverEnvSlots(providerId, env), ...policySlots];\n\treturn listEnvRotationSlots(sources, envSlots, acquireLeases);\n}\n\nasync function listEnvRotationSlots(\n\tsources: RotationSources,\n\tenvSlots: readonly { name: string; envVarName: string; key: string }[],\n\tacquireLeases = true,\n): Promise<RotationSlot[]> {\n\tif (envSlots.length === 0) return [];\n\tconst { providerId, repository } = sources;\n\tconst state = await repository.listSlots(providerId, \"env\");\n\tconst slots: RotationSlot[] = [];\n\tfor (const slot of envSlots) {\n\t\tconst persisted = state[slot.name];\n\t\tconst revision = await repository.envCredentialRevision(slot.envVarName, slot.key);\n\t\tlet applicable = persisted?.credentialRevision === revision ? persisted : undefined;\n\t\tif (\n\t\t\tacquireLeases &&\n\t\t\tapplicable?.blockedUntil !== undefined &&\n\t\t\tapplicable.blockedUntil <= (sources.now ?? Date.now)()\n\t\t) {\n\t\t\tconst lease = await acquireHalfOpenLease(repository, providerId, \"env\", slot.name, {\n\t\t\t\tnow: (sources.now ?? Date.now)(),\n\t\t\t});\n\t\t\tif (!lease) continue;\n\t\t\tconst leased = await repository.listSlots(providerId, \"env\");\n\t\t\tapplicable = leased[slot.name];\n\t\t}\n\t\tslots.push(\n\t\t\toverlayState(\n\t\t\t\t{\n\t\t\t\t\tname: slot.name,\n\t\t\t\t\tlane: \"env\",\n\t\t\t\t\tenvKey: slot.key,\n\t\t\t\t\tenvVarName: slot.envVarName,\n\t\t\t\t},\n\t\t\t\tapplicable,\n\t\t\t),\n\t\t);\n\t}\n\treturn slots;\n}\n\nfunction blockPatch(\n\tblock: CredentialBlock,\n\tcurrent: CredentialSlotState | undefined,\n\tnow: number,\n\tcredentialRevision?: string,\n\tpolicy?: { cooldownBaseMs?: number; cooldownCapMs?: number },\n): Omit<CredentialSlotState, \"stateVersion\"> {\n\tconst failureCount = (current?.failureCount ?? 0) + 1;\n\tconst base = {\n\t\tfailureCount,\n\t\t...(credentialRevision === undefined ? {} : { credentialRevision }),\n\t\t...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),\n\t};\n\tif (block.reason === \"rate_limit\") {\n\t\treturn {\n\t\t\t...base,\n\t\t\tblockedUntil: now + Math.min(policy?.cooldownCapMs ?? block.cooldownMs, block.cooldownMs),\n\t\t\tblockReason: \"rate_limit\",\n\t\t};\n\t}\n\treturn { ...base, blockReason: block.reason };\n}\n\nexport type CredentialRotationOptions = {\n\tsources: RotationSources;\n\t/** Stable session key keeps a session on its slot; absent, each request distributes. */\n\taffinityKey?: string;\n\thasher?: SlotHasher;\n\trunAttempt: (\n\t\tslot: RotationSlot,\n\t) => AsyncIterable<AssistantMessageEvent> | Promise<AsyncIterable<AssistantMessageEvent>>;\n};\n\nfunction errorFromEvent(event: AssistantMessageEvent): unknown {\n\tif (event.type !== \"error\") return undefined;\n\tconst message = event.error.errorMessage ?? \"provider stream error\";\n\treturn new Error(message);\n}\n\n/**\n * In-lane credential rotation for one provider request. Selection follows the\n * HRW order for the affinity key; only the `start` bookkeeping event counts as\n * pre-commit, so any delta bars silent rotation (default-DENY) and failures\n * after output carry the turn-retry suppression marker.\n */\nexport function streamWithCredentialRotation(\n\toptions: CredentialRotationOptions,\n): AsyncGenerator<AssistantMessageEvent> {\n\tconst { sources, runAttempt } = options;\n\tconst hasher = options.hasher ?? sha256SlotHasher;\n\tconst affinityKey = options.affinityKey ?? randomUUID();\n\tconst useAffinity = sources.policy?.affinity !== false;\n\tconst now = sources.now ?? Date.now;\n\n\treturn runCredentialFailover<AssistantMessageEvent, RotationSlot>({\n\t\tlistSlots: () => listRotationSlots(sources),\n\t\tselect: (candidates) => {\n\t\t\tconst pinned = candidates.find((candidate) => candidate.pinned === true);\n\t\t\tif (pinned) return pinned;\n\t\t\tconst ordered = useAffinity ? rendezvousOrder(affinityKey, candidates, hasher) : candidates;\n\n\t\t\tconst winner = ordered[0];\n\t\t\tif (!winner) throw new Error(\"credential rotation selected from an empty candidate set\");\n\t\t\treturn winner;\n\t\t},\n\t\trunAttempt,\n\t\tisCommittedOutput: (event) => event.type !== \"start\",\n\t\terrorFromEvent,\n\t\tclassify: (error, context) =>\n\t\t\tclassifyCredentialFailure(error, {\n\t\t\t\t...context,\n\t\t\t\tcooldownBaseMs: sources.policy?.cooldownBaseMs,\n\t\t\t\tcooldownCapMs: sources.policy?.cooldownCapMs,\n\t\t\t}),\n\t\tonSuccess: async (slot) => {\n\t\t\tawait sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) =>\n\t\t\t\tcurrent\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t...current,\n\t\t\t\t\t\t\tlastSuccessAt: now(),\n\t\t\t\t\t\t\tlease: undefined,\n\t\t\t\t\t\t\tblockedUntil: undefined,\n\t\t\t\t\t\t\tblockReason: undefined,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t);\n\t\t},\n\t\tpersistBlock: async (slot, block) => {\n\t\t\tconst revision =\n\t\t\t\tslot.lane === \"env\" && slot.envVarName !== undefined && slot.envKey !== undefined\n\t\t\t\t\t? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)\n\t\t\t\t\t: undefined;\n\t\t\tawait sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) =>\n\t\t\t\tblockPatch(block, current, now(), revision, sources.policy),\n\t\t\t);\n\t\t},\n\t\tnow,\n\t});\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "../../types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Provider-neutral `/account` command over the generic credential pool. The
|
|
4
|
+
* existing `/claude-account` and Cursor account commands stay untouched; this
|
|
5
|
+
* command is the one surface that works for every provider. Output carries
|
|
6
|
+
* names and health only, never key material.
|
|
7
|
+
*/
|
|
8
|
+
export default function accountExtension(pi: ExtensionAPI): void;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/account/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAA2B,MAAM,gBAAgB,CAAC;AAwB5E;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAsC/D"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getCredentialAccounts, pinCredentialAccount, removeCredentialAccount, } from "../../../credential-accounts.js";
|
|
2
|
+
function parseArgs(rawArgs) {
|
|
3
|
+
return rawArgs.trim().split(/\s+/).filter(Boolean);
|
|
4
|
+
}
|
|
5
|
+
function usage(ctx) {
|
|
6
|
+
ctx.ui.notify("Usage: /account <provider> [list | pin <name> | unpin | remove <name>]", "error");
|
|
7
|
+
}
|
|
8
|
+
function statusOf(account) {
|
|
9
|
+
const states = [account.name, account.source, account.blocked ? "blocked" : "available"];
|
|
10
|
+
if (account.pinned)
|
|
11
|
+
states.push("pinned");
|
|
12
|
+
return states.join(" | ");
|
|
13
|
+
}
|
|
14
|
+
async function showAccounts(ctx, provider) {
|
|
15
|
+
const accounts = await getCredentialAccounts(ctx.modelRegistry.authStorage, provider);
|
|
16
|
+
const lines = [`Credential accounts for ${provider}:`];
|
|
17
|
+
if (accounts.length === 0)
|
|
18
|
+
lines.push(" (none)");
|
|
19
|
+
for (const account of accounts)
|
|
20
|
+
lines.push(` ${statusOf(account)}`);
|
|
21
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Provider-neutral `/account` command over the generic credential pool. The
|
|
25
|
+
* existing `/claude-account` and Cursor account commands stay untouched; this
|
|
26
|
+
* command is the one surface that works for every provider. Output carries
|
|
27
|
+
* names and health only, never key material.
|
|
28
|
+
*/
|
|
29
|
+
export default function accountExtension(pi) {
|
|
30
|
+
pi.registerCommand("account", {
|
|
31
|
+
description: "List and manage credential accounts for any provider.",
|
|
32
|
+
argumentHint: "<provider> [list | pin <name> | unpin | remove <name>]",
|
|
33
|
+
handler: async (rawArgs, ctx) => {
|
|
34
|
+
const args = parseArgs(rawArgs);
|
|
35
|
+
const provider = args[0];
|
|
36
|
+
if (provider === undefined) {
|
|
37
|
+
usage(ctx);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const action = args[1] ?? "list";
|
|
41
|
+
try {
|
|
42
|
+
if (action === "list") {
|
|
43
|
+
await showAccounts(ctx, provider);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (action === "pin" && args[2] !== undefined) {
|
|
47
|
+
await pinCredentialAccount(ctx.modelRegistry.authStorage, provider, args[2]);
|
|
48
|
+
ctx.ui.notify(`Pinned ${provider} account '${args[2]}'.`, "info");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (action === "unpin") {
|
|
52
|
+
await pinCredentialAccount(ctx.modelRegistry.authStorage, provider, null);
|
|
53
|
+
ctx.ui.notify(`Unpinned ${provider} account.`, "info");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (action === "remove" && args[2] !== undefined) {
|
|
57
|
+
await removeCredentialAccount(ctx.modelRegistry.authStorage, provider, args[2]);
|
|
58
|
+
ctx.ui.notify(`Removed ${provider} account '${args[2]}'.`, "info");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
usage(ctx);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/account/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype CredentialAccountSummary,\n\tgetCredentialAccounts,\n\tpinCredentialAccount,\n\tremoveCredentialAccount,\n} from \"../../../credential-accounts.ts\";\nimport type { ExtensionAPI, ExtensionCommandContext } from \"../../types.ts\";\n\nfunction parseArgs(rawArgs: string): string[] {\n\treturn rawArgs.trim().split(/\\s+/).filter(Boolean);\n}\n\nfunction usage(ctx: ExtensionCommandContext): void {\n\tctx.ui.notify(\"Usage: /account <provider> [list | pin <name> | unpin | remove <name>]\", \"error\");\n}\n\nfunction statusOf(account: CredentialAccountSummary): string {\n\tconst states = [account.name, account.source, account.blocked ? \"blocked\" : \"available\"];\n\tif (account.pinned) states.push(\"pinned\");\n\treturn states.join(\" | \");\n}\n\nasync function showAccounts(ctx: ExtensionCommandContext, provider: string): Promise<void> {\n\tconst accounts = await getCredentialAccounts(ctx.modelRegistry.authStorage, provider);\n\tconst lines = [`Credential accounts for ${provider}:`];\n\tif (accounts.length === 0) lines.push(\" (none)\");\n\tfor (const account of accounts) lines.push(` ${statusOf(account)}`);\n\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n}\n\n/**\n * Provider-neutral `/account` command over the generic credential pool. The\n * existing `/claude-account` and Cursor account commands stay untouched; this\n * command is the one surface that works for every provider. Output carries\n * names and health only, never key material.\n */\nexport default function accountExtension(pi: ExtensionAPI): void {\n\tpi.registerCommand(\"account\", {\n\t\tdescription: \"List and manage credential accounts for any provider.\",\n\t\targumentHint: \"<provider> [list | pin <name> | unpin | remove <name>]\",\n\t\thandler: async (rawArgs, ctx) => {\n\t\t\tconst args = parseArgs(rawArgs);\n\t\t\tconst provider = args[0];\n\t\t\tif (provider === undefined) {\n\t\t\t\tusage(ctx);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst action = args[1] ?? \"list\";\n\t\t\ttry {\n\t\t\t\tif (action === \"list\") {\n\t\t\t\t\tawait showAccounts(ctx, provider);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (action === \"pin\" && args[2] !== undefined) {\n\t\t\t\t\tawait pinCredentialAccount(ctx.modelRegistry.authStorage, provider, args[2]);\n\t\t\t\t\tctx.ui.notify(`Pinned ${provider} account '${args[2]}'.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (action === \"unpin\") {\n\t\t\t\t\tawait pinCredentialAccount(ctx.modelRegistry.authStorage, provider, null);\n\t\t\t\t\tctx.ui.notify(`Unpinned ${provider} account.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (action === \"remove\" && args[2] !== undefined) {\n\t\t\t\t\tawait removeCredentialAccount(ctx.modelRegistry.authStorage, provider, args[2]);\n\t\t\t\t\tctx.ui.notify(`Removed ${provider} account '${args[2]}'.`, \"info\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tusage(ctx);\n\t\t\t} catch (error) {\n\t\t\t\tctx.ui.notify(error instanceof Error ? error.message : String(error), \"error\");\n\t\t\t}\n\t\t},\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/goal/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AAgCrE,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/goal/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,gBAAgB,CAAC;AAgCrE,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8X5D"}
|
|
@@ -246,6 +246,13 @@ export default function goalExtension(pi) {
|
|
|
246
246
|
if (!isResumeOfStoppedGoal(ctx, sessionStartReason, goal)) {
|
|
247
247
|
return false;
|
|
248
248
|
}
|
|
249
|
+
// RPC switch_session cannot service a nested interactive UI request until the
|
|
250
|
+
// switch response completes. Keep the goal stopped and let the user resume it
|
|
251
|
+
// explicitly after the new session finishes binding.
|
|
252
|
+
if (ctx.mode === "rpc") {
|
|
253
|
+
ctx.ui.notify(`Goal remains ${goal.status} after session resume. Resume it explicitly after the session finishes loading.`, "info");
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
249
256
|
const choice = await ctx.ui.select(`Resume ${goal.status} goal?\nGoal: ${goal.objective}`, [
|
|
250
257
|
RESUME_GOAL_CHOICE,
|
|
251
258
|
LEAVE_GOAL_STOPPED_CHOICE,
|