@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.
Files changed (181) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session-services.d.ts.map +1 -1
  7. package/dist/core/agent-session-services.js +2 -0
  8. package/dist/core/agent-session-services.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +5 -0
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +1 -0
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/auth-storage.d.ts +1 -0
  14. package/dist/core/auth-storage.d.ts.map +1 -1
  15. package/dist/core/auth-storage.js +3 -0
  16. package/dist/core/auth-storage.js.map +1 -1
  17. package/dist/core/credential-accounts.d.ts +31 -0
  18. package/dist/core/credential-accounts.d.ts.map +1 -0
  19. package/dist/core/credential-accounts.js +135 -0
  20. package/dist/core/credential-accounts.js.map +1 -0
  21. package/dist/core/credential-pool/classify.d.ts +6 -1
  22. package/dist/core/credential-pool/classify.d.ts.map +1 -1
  23. package/dist/core/credential-pool/classify.js +22 -5
  24. package/dist/core/credential-pool/classify.js.map +1 -1
  25. package/dist/core/credential-pool/failover.d.ts +8 -0
  26. package/dist/core/credential-pool/failover.d.ts.map +1 -1
  27. package/dist/core/credential-pool/failover.js +24 -6
  28. package/dist/core/credential-pool/failover.js.map +1 -1
  29. package/dist/core/credential-pool/rotation-stream.d.ts +13 -2
  30. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
  31. package/dist/core/credential-pool/rotation-stream.js +97 -10
  32. package/dist/core/credential-pool/rotation-stream.js.map +1 -1
  33. package/dist/core/extensions/builtin/account/index.d.ts +9 -0
  34. package/dist/core/extensions/builtin/account/index.d.ts.map +1 -0
  35. package/dist/core/extensions/builtin/account/index.js +69 -0
  36. package/dist/core/extensions/builtin/account/index.js.map +1 -0
  37. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/goal/index.js +7 -0
  39. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  40. package/dist/core/extensions/builtin/imagegen/index.d.ts.map +1 -1
  41. package/dist/core/extensions/builtin/imagegen/index.js +10 -3
  42. package/dist/core/extensions/builtin/imagegen/index.js.map +1 -1
  43. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/index.js +4 -0
  45. package/dist/core/extensions/builtin/index.js.map +1 -1
  46. package/dist/core/extensions/builtin/service-tier.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/service-tier.js +13 -0
  48. package/dist/core/extensions/builtin/service-tier.js.map +1 -1
  49. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  50. package/dist/core/extensions/builtin/terminal/extension.js +4 -2
  51. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  52. package/dist/core/model-config-schema.d.ts +4 -4
  53. package/dist/core/model-runtime.d.ts +2 -0
  54. package/dist/core/model-runtime.d.ts.map +1 -1
  55. package/dist/core/model-runtime.js +74 -37
  56. package/dist/core/model-runtime.js.map +1 -1
  57. package/dist/core/retry-fallback/chains.d.ts +23 -1
  58. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  59. package/dist/core/retry-fallback/chains.js +59 -3
  60. package/dist/core/retry-fallback/chains.js.map +1 -1
  61. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  62. package/dist/core/retry-fallback/controller.js +9 -2
  63. package/dist/core/retry-fallback/controller.js.map +1 -1
  64. package/dist/core/retry-fallback/settings.d.ts.map +1 -1
  65. package/dist/core/retry-fallback/settings.js +9 -0
  66. package/dist/core/retry-fallback/settings.js.map +1 -1
  67. package/dist/core/retry-fallback/validate.js +1 -1
  68. package/dist/core/retry-fallback/validate.js.map +1 -1
  69. package/dist/core/sdk.js +1 -1
  70. package/dist/core/sdk.js.map +1 -1
  71. package/dist/core/session-manager.d.ts +9 -0
  72. package/dist/core/session-manager.d.ts.map +1 -1
  73. package/dist/core/session-manager.js +31 -0
  74. package/dist/core/session-manager.js.map +1 -1
  75. package/dist/main.d.ts +7 -0
  76. package/dist/main.d.ts.map +1 -1
  77. package/dist/main.js +22 -2
  78. package/dist/main.js.map +1 -1
  79. package/dist/modes/app-server/server/account.d.ts.map +1 -1
  80. package/dist/modes/app-server/server/account.js +5 -5
  81. package/dist/modes/app-server/server/account.js.map +1 -1
  82. package/dist/modes/interactive/components/footer.d.ts +11 -3
  83. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/footer.js +31 -1
  85. package/dist/modes/interactive/components/footer.js.map +1 -1
  86. package/dist/modes/interactive/grok/chrome.d.ts +4 -4
  87. package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
  88. package/dist/modes/interactive/grok/chrome.js.map +1 -1
  89. package/dist/modes/interactive/grok/footer.d.ts +3 -3
  90. package/dist/modes/interactive/grok/footer.d.ts.map +1 -1
  91. package/dist/modes/interactive/grok/footer.js.map +1 -1
  92. package/dist/modes/interactive/interactive-host-runtime.d.ts +16 -1
  93. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  94. package/dist/modes/interactive/interactive-host-runtime.js +321 -36
  95. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  96. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  97. package/dist/modes/interactive/interactive-mode.js +295 -250
  98. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  99. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  100. package/dist/modes/rpc/connection-handler.js +144 -29
  101. package/dist/modes/rpc/connection-handler.js.map +1 -1
  102. package/dist/modes/rpc/multi-session-host.js +14 -4
  103. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  104. package/dist/modes/rpc/rpc-client.d.ts +90 -8
  105. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  106. package/dist/modes/rpc/rpc-client.js +118 -23
  107. package/dist/modes/rpc/rpc-client.js.map +1 -1
  108. package/dist/modes/rpc/rpc-mode.d.ts +3 -1
  109. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  110. package/dist/modes/rpc/rpc-mode.js +3 -1
  111. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  112. package/dist/modes/rpc/rpc-types.d.ts +164 -2
  113. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  114. package/dist/modes/rpc/rpc-types.js.map +1 -1
  115. package/dist/modes/rpc/session-command-router.d.ts +22 -0
  116. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  117. package/dist/modes/rpc/session-command-router.js +153 -14
  118. package/dist/modes/rpc/session-command-router.js.map +1 -1
  119. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  120. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  121. package/dist/modes/rpc/session-event-writer.js +4 -0
  122. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  123. package/dist/modes/rpc/session-registry.d.ts +16 -0
  124. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  125. package/dist/modes/rpc/session-registry.js +40 -3
  126. package/dist/modes/rpc/session-registry.js.map +1 -1
  127. package/docs/providers.md +48 -0
  128. package/docs/rpc.md +43 -4
  129. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +26 -0
  130. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  131. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +13 -1
  132. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +15 -1
  133. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +14 -1
  134. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.d.ts.map +1 -1
  135. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js +20 -4
  136. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js.map +1 -1
  137. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts +1 -1
  138. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts.map +1 -1
  139. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.js.map +1 -1
  140. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts +21 -0
  141. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts.map +1 -0
  142. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js +27 -0
  143. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js.map +1 -0
  144. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts +24 -0
  145. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts.map +1 -1
  146. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.js.map +1 -1
  147. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.d.ts.map +1 -1
  148. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js +7 -2
  149. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js.map +1 -1
  150. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  151. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts +2 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts.map +1 -1
  153. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js +2 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js.map +1 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts +3 -2
  156. package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts.map +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/auth/types.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/providers/data/alibaba-token-plan.json +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  162. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  163. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  164. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  165. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  168. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  170. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  172. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  178. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  179. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  180. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  181. 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;CACtB,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,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,gBAAgB,CAAC;IACnF,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,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;AAyBD;;;;;;GAMG;AACH,wBAAuB,qBAAqB,CAAC,MAAM,EAAE,KAAK,SAAS,OAAO,EACzE,OAAO,EAAE,4BAA4B,CAAC,MAAM,EAAE,KAAK,CAAC,GAClD,cAAc,CAAC,MAAM,CAAC,CA2DxB"}
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
- return slot.blockedUntil === undefined || slot.blockedUntil <= now;
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, { failureCount });
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, { suppressTurnRetry: false });
98
+ throw new CredentialFailoverError(action, error, {
99
+ suppressTurnRetry: false,
100
+ });
92
101
  }
93
102
  if (action.kind !== "failover") {
94
- throw new CredentialFailoverError(action, error, { suppressTurnRetry: committedOutput });
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, { suppressTurnRetry: committedOutput });
109
+ const failure = new CredentialFailoverError(action, error, {
110
+ suppressTurnRetry: committedOutput,
111
+ });
99
112
  lastError = failure;
100
- await options.onRotate?.({ slot, block: action.block, attempt: attempted.size, committedOutput });
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 { CredentialSlotRepository } from "./state-store.ts";
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): Promise<RotationSlot[]>;
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;AAI1F,OAAO,EAAE,KAAK,OAAO,EAAyB,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,wBAAwB,EAAuB,MAAM,kBAAkB,CAAC;AAEtF,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,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB,CAAC;AAYF;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAqBzF;AAoBD,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,CA6BvC"}
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
- return listCredentialSlots(credential).map((slot) => overlayState({ name: slot.name, lane: "stored" }, state[slot.name]));
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
- const applicable = persisted?.credentialRevision === revision ? persisted : undefined;
41
- slots.push(overlayState({ name: slot.name, lane: "env", envKey: slot.key, envVarName: slot.envVarName }, applicable));
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 { ...base, blockedUntil: now + block.cooldownMs, blockReason: "rate_limit" };
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 ordered = rendezvousOrder(affinityKey, candidates, hasher);
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,CAoX5D"}
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,