@code-yeongyu/senpi 2026.8.26 → 2026.8.27

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 (231) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +3 -3
  3. package/dist/cli/args.d.ts +3 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +14 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +10 -1
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/bash-executor.d.ts.map +1 -1
  11. package/dist/core/bash-executor.js +67 -29
  12. package/dist/core/bash-executor.js.map +1 -1
  13. package/dist/core/credential-pool/classify.d.ts +42 -0
  14. package/dist/core/credential-pool/classify.d.ts.map +1 -0
  15. package/dist/core/credential-pool/classify.js +75 -0
  16. package/dist/core/credential-pool/classify.js.map +1 -0
  17. package/dist/core/credential-pool/env-slots.d.ts +22 -0
  18. package/dist/core/credential-pool/env-slots.d.ts.map +1 -0
  19. package/dist/core/credential-pool/env-slots.js +37 -0
  20. package/dist/core/credential-pool/env-slots.js.map +1 -0
  21. package/dist/core/credential-pool/failover.d.ts +54 -0
  22. package/dist/core/credential-pool/failover.d.ts.map +1 -0
  23. package/dist/core/credential-pool/failover.js +106 -0
  24. package/dist/core/credential-pool/failover.js.map +1 -0
  25. package/dist/core/credential-pool/rotation-stream.d.ts +43 -0
  26. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -0
  27. package/dist/core/credential-pool/rotation-stream.js +96 -0
  28. package/dist/core/credential-pool/rotation-stream.js.map +1 -0
  29. package/dist/core/credential-pool/state-store.d.ts +89 -0
  30. package/dist/core/credential-pool/state-store.d.ts.map +1 -0
  31. package/dist/core/credential-pool/state-store.js +166 -0
  32. package/dist/core/credential-pool/state-store.js.map +1 -0
  33. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/compaction/index.js +67 -0
  35. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  36. package/dist/core/extensions/builtin/compaction/log.d.ts +1 -1
  37. package/dist/core/extensions/builtin/compaction/log.d.ts.map +1 -1
  38. package/dist/core/extensions/builtin/compaction/log.js +1 -0
  39. package/dist/core/extensions/builtin/compaction/log.js.map +1 -1
  40. package/dist/core/extensions/builtin/config-reload/index.js +14 -22
  41. package/dist/core/extensions/builtin/config-reload/index.js.map +1 -1
  42. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
  43. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
  44. package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
  45. package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
  46. package/dist/core/extensions/builtin/goal/continuation.d.ts +8 -1
  47. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  48. package/dist/core/extensions/builtin/goal/continuation.js +15 -0
  49. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  50. package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js +2 -2
  51. package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js.map +1 -1
  52. package/dist/core/extensions/builtin/goal/format.d.ts +6 -1
  53. package/dist/core/extensions/builtin/goal/format.d.ts.map +1 -1
  54. package/dist/core/extensions/builtin/goal/format.js +6 -0
  55. package/dist/core/extensions/builtin/goal/format.js.map +1 -1
  56. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
  57. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +5 -2
  58. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
  59. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  60. package/dist/core/extensions/builtin/goal/monitor-continuation.js +31 -7
  61. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  62. package/dist/core/extensions/builtin/goal/persistence.js +2 -0
  63. package/dist/core/extensions/builtin/goal/persistence.js.map +1 -1
  64. package/dist/core/extensions/builtin/goal/renderers.d.ts +14 -0
  65. package/dist/core/extensions/builtin/goal/renderers.d.ts.map +1 -0
  66. package/dist/core/extensions/builtin/goal/renderers.js +153 -0
  67. package/dist/core/extensions/builtin/goal/renderers.js.map +1 -0
  68. package/dist/core/extensions/builtin/goal/store.d.ts +6 -2
  69. package/dist/core/extensions/builtin/goal/store.d.ts.map +1 -1
  70. package/dist/core/extensions/builtin/goal/store.js +8 -2
  71. package/dist/core/extensions/builtin/goal/store.js.map +1 -1
  72. package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
  73. package/dist/core/extensions/builtin/goal/tool-registration.js +16 -6
  74. package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
  75. package/dist/core/extensions/builtin/goal/types.d.ts +1 -0
  76. package/dist/core/extensions/builtin/goal/types.d.ts.map +1 -1
  77. package/dist/core/extensions/builtin/goal/types.js.map +1 -1
  78. package/dist/core/extensions/builtin/todotools/index.js +1 -1
  79. package/dist/core/extensions/builtin/todotools/index.js.map +1 -1
  80. package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts +1 -1
  81. package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts.map +1 -1
  82. package/dist/core/extensions/builtin/todotools/native-todo-mirror.js +1 -1
  83. package/dist/core/extensions/builtin/todotools/native-todo-mirror.js.map +1 -1
  84. package/dist/core/hidden-stdout-log.d.ts +10 -0
  85. package/dist/core/hidden-stdout-log.d.ts.map +1 -1
  86. package/dist/core/hidden-stdout-log.js +36 -5
  87. package/dist/core/hidden-stdout-log.js.map +1 -1
  88. package/dist/core/model-config-schema.d.ts +85 -12
  89. package/dist/core/model-config-schema.d.ts.map +1 -1
  90. package/dist/core/model-config-schema.js +25 -0
  91. package/dist/core/model-config-schema.js.map +1 -1
  92. package/dist/core/model-runtime.d.ts +34 -1
  93. package/dist/core/model-runtime.d.ts.map +1 -1
  94. package/dist/core/model-runtime.js +98 -5
  95. package/dist/core/model-runtime.js.map +1 -1
  96. package/dist/core/tools/output-accumulator.d.ts +2 -0
  97. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  98. package/dist/core/tools/output-accumulator.js +32 -7
  99. package/dist/core/tools/output-accumulator.js.map +1 -1
  100. package/dist/index.d.ts +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +1 -1
  103. package/dist/index.js.map +1 -1
  104. package/dist/main.d.ts.map +1 -1
  105. package/dist/main.js +26 -2
  106. package/dist/main.js.map +1 -1
  107. package/dist/modes/index.d.ts +1 -0
  108. package/dist/modes/index.d.ts.map +1 -1
  109. package/dist/modes/index.js +1 -0
  110. package/dist/modes/index.js.map +1 -1
  111. package/dist/modes/interactive/components/tool-execution-fallback.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/tool-execution-fallback.js +82 -1
  113. package/dist/modes/interactive/components/tool-execution-fallback.js.map +1 -1
  114. package/dist/modes/interactive/interactive-host-runtime.d.ts +29 -0
  115. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -0
  116. package/dist/modes/interactive/interactive-host-runtime.js +244 -0
  117. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -0
  118. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  119. package/dist/modes/interactive/interactive-mode.js +46 -8
  120. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  121. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  122. package/dist/modes/rpc/connection-handler.js +7 -1
  123. package/dist/modes/rpc/connection-handler.js.map +1 -1
  124. package/dist/modes/rpc/host-ensure.d.ts +41 -0
  125. package/dist/modes/rpc/host-ensure.d.ts.map +1 -0
  126. package/dist/modes/rpc/host-ensure.js +245 -0
  127. package/dist/modes/rpc/host-ensure.js.map +1 -0
  128. package/dist/modes/rpc/host-lifecycle.d.ts +57 -0
  129. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -0
  130. package/dist/modes/rpc/host-lifecycle.js +491 -0
  131. package/dist/modes/rpc/host-lifecycle.js.map +1 -0
  132. package/dist/modes/rpc/host-watchdog.d.ts +33 -0
  133. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -0
  134. package/dist/modes/rpc/host-watchdog.js +142 -0
  135. package/dist/modes/rpc/host-watchdog.js.map +1 -0
  136. package/dist/modes/rpc/multi-session-host.d.ts +7 -1
  137. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  138. package/dist/modes/rpc/multi-session-host.js +206 -32
  139. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  140. package/dist/modes/rpc/rpc-client.d.ts +25 -0
  141. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  142. package/dist/modes/rpc/rpc-client.js +83 -10
  143. package/dist/modes/rpc/rpc-client.js.map +1 -1
  144. package/dist/modes/rpc/rpc-mode.d.ts +1 -1
  145. package/dist/modes/rpc/rpc-mode.js +1 -1
  146. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  147. package/dist/modes/rpc/rpc-types.d.ts +2 -1
  148. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  149. package/dist/modes/rpc/rpc-types.js.map +1 -1
  150. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  151. package/dist/modes/rpc/session-command-router.js +12 -2
  152. package/dist/modes/rpc/session-command-router.js.map +1 -1
  153. package/dist/modes/rpc/session-event-writer.d.ts +13 -2
  154. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  155. package/dist/modes/rpc/session-event-writer.js +65 -25
  156. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  157. package/docs/rpc.md +53 -1
  158. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +30 -0
  159. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +711 -1
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -10
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +9 -3
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +6 -1
  164. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  165. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts.map +1 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js +46 -14
  168. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +11 -0
  170. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +30 -16
  172. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts +16 -0
  174. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts.map +1 -0
  175. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js +75 -0
  176. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js.map +1 -0
  177. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts +45 -0
  178. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts.map +1 -0
  179. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js +81 -0
  180. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js.map +1 -0
  181. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts +31 -0
  182. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts.map +1 -0
  183. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js +65 -0
  184. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js.map +1 -0
  185. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts +12 -0
  186. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js +37 -0
  188. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts +6 -0
  190. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +32 -6
  192. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js.map +1 -1
  193. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts +6 -0
  194. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  195. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +6 -1
  196. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +75 -0
  198. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  199. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +75 -0
  200. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  201. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  202. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  203. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  204. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  205. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  206. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  207. package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax-cn.json +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax.json +1 -1
  209. package/node_modules/@earendil-works/pi-ai/dist/providers/data/mistral.json +1 -1
  210. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  211. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  212. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  213. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  215. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  216. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  217. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  218. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  219. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +4 -0
  220. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  221. package/node_modules/@earendil-works/pi-ai/package.json +5 -5
  222. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  223. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  224. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +5 -0
  225. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  226. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +90 -1
  227. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  228. package/node_modules/@earendil-works/pi-tui/dist/tui.js +1 -1
  229. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
  230. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  231. package/package.json +7 -7
@@ -0,0 +1,43 @@
1
+ import type { AssistantMessageEvent, Credential } from "@earendil-works/pi-ai";
2
+ import { type SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
3
+ import { type RunSlot } from "./failover.ts";
4
+ import type { CredentialSlotRepository } from "./state-store.ts";
5
+ /** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
6
+ export declare const sha256SlotHasher: SlotHasher;
7
+ export type RotationLane = "stored" | "env";
8
+ export type RotationSlot = RunSlot & {
9
+ lane: RotationLane;
10
+ /** Env-lane key material for the attempt; never serialized or persisted. */
11
+ envKey?: string;
12
+ envVarName?: string;
13
+ };
14
+ export type RotationSources = {
15
+ providerId: string;
16
+ credential: Credential | undefined;
17
+ env: (name: string) => string | undefined;
18
+ repository: CredentialSlotRepository;
19
+ now?: () => number;
20
+ };
21
+ /**
22
+ * Lists the provider's rotation slots with sidecar health overlaid. Stored
23
+ * credentials own the lane when present; env slots participate only when
24
+ * nothing is stored, preserving today's resolution precedence. An env slot's
25
+ * persisted health applies only while its HMAC revision still matches the
26
+ * current env value, so rotating a key in place clears its own stale block.
27
+ */
28
+ export declare function listRotationSlots(sources: RotationSources): Promise<RotationSlot[]>;
29
+ export type CredentialRotationOptions = {
30
+ sources: RotationSources;
31
+ /** Stable session key keeps a session on its slot; absent, each request distributes. */
32
+ affinityKey?: string;
33
+ hasher?: SlotHasher;
34
+ runAttempt: (slot: RotationSlot) => AsyncIterable<AssistantMessageEvent> | Promise<AsyncIterable<AssistantMessageEvent>>;
35
+ };
36
+ /**
37
+ * In-lane credential rotation for one provider request. Selection follows the
38
+ * HRW order for the affinity key; only the `start` bookkeeping event counts as
39
+ * pre-commit, so any delta bars silent rotation (default-DENY) and failures
40
+ * after output carry the turn-retry suppression marker.
41
+ */
42
+ export declare function streamWithCredentialRotation(options: CredentialRotationOptions): AsyncGenerator<AssistantMessageEvent>;
43
+ //# sourceMappingURL=rotation-stream.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,96 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
3
+ import { listSlots as listCredentialSlots } from "@earendil-works/pi-ai/auth/pool/slots";
4
+ import { classifyCredentialFailure } from "./classify.js";
5
+ import { discoverEnvSlots } from "./env-slots.js";
6
+ import { runCredentialFailover } from "./failover.js";
7
+ /** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
8
+ export const sha256SlotHasher = (input) => createHash("sha256").update(input).digest().readBigUInt64BE(0);
9
+ function overlayState(slot, state) {
10
+ if (!state)
11
+ return slot;
12
+ return {
13
+ ...slot,
14
+ ...(state.blockedUntil === undefined ? {} : { blockedUntil: state.blockedUntil }),
15
+ ...(state.blockReason === undefined ? {} : { blockReason: state.blockReason }),
16
+ ...(state.failureCount === undefined ? {} : { failureCount: state.failureCount }),
17
+ };
18
+ }
19
+ /**
20
+ * Lists the provider's rotation slots with sidecar health overlaid. Stored
21
+ * credentials own the lane when present; env slots participate only when
22
+ * nothing is stored, preserving today's resolution precedence. An env slot's
23
+ * persisted health applies only while its HMAC revision still matches the
24
+ * current env value, so rotating a key in place clears its own stale block.
25
+ */
26
+ export async function listRotationSlots(sources) {
27
+ const { providerId, credential, env, repository } = sources;
28
+ if (credential) {
29
+ const state = await repository.listSlots(providerId, "stored");
30
+ return listCredentialSlots(credential).map((slot) => overlayState({ name: slot.name, lane: "stored" }, state[slot.name]));
31
+ }
32
+ const envSlots = discoverEnvSlots(providerId, env);
33
+ if (envSlots.length === 0)
34
+ return [];
35
+ const state = await repository.listSlots(providerId, "env");
36
+ const slots = [];
37
+ for (const slot of envSlots) {
38
+ const persisted = state[slot.name];
39
+ 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));
42
+ }
43
+ return slots;
44
+ }
45
+ function blockPatch(block, current, now, credentialRevision) {
46
+ const failureCount = (current?.failureCount ?? 0) + 1;
47
+ const base = {
48
+ failureCount,
49
+ ...(credentialRevision === undefined ? {} : { credentialRevision }),
50
+ ...(current?.lastSuccessAt === undefined ? {} : { lastSuccessAt: current.lastSuccessAt }),
51
+ };
52
+ if (block.reason === "rate_limit") {
53
+ return { ...base, blockedUntil: now + block.cooldownMs, blockReason: "rate_limit" };
54
+ }
55
+ return { ...base, blockReason: block.reason };
56
+ }
57
+ function errorFromEvent(event) {
58
+ if (event.type !== "error")
59
+ return undefined;
60
+ const message = event.error.errorMessage ?? "provider stream error";
61
+ return new Error(message);
62
+ }
63
+ /**
64
+ * In-lane credential rotation for one provider request. Selection follows the
65
+ * HRW order for the affinity key; only the `start` bookkeeping event counts as
66
+ * pre-commit, so any delta bars silent rotation (default-DENY) and failures
67
+ * after output carry the turn-retry suppression marker.
68
+ */
69
+ export function streamWithCredentialRotation(options) {
70
+ const { sources, runAttempt } = options;
71
+ const hasher = options.hasher ?? sha256SlotHasher;
72
+ const affinityKey = options.affinityKey ?? randomUUID();
73
+ const now = sources.now ?? Date.now;
74
+ return runCredentialFailover({
75
+ listSlots: () => listRotationSlots(sources),
76
+ select: (candidates) => {
77
+ const ordered = rendezvousOrder(affinityKey, candidates, hasher);
78
+ const winner = ordered[0];
79
+ if (!winner)
80
+ throw new Error("credential rotation selected from an empty candidate set");
81
+ return winner;
82
+ },
83
+ runAttempt,
84
+ isCommittedOutput: (event) => event.type !== "start",
85
+ errorFromEvent,
86
+ classify: classifyCredentialFailure,
87
+ persistBlock: async (slot, block) => {
88
+ const revision = slot.lane === "env" && slot.envVarName !== undefined && slot.envKey !== undefined
89
+ ? await sources.repository.envCredentialRevision(slot.envVarName, slot.envKey)
90
+ : undefined;
91
+ await sources.repository.mutateSlotState(sources.providerId, slot.lane, slot.name, (current) => blockPatch(block, current, now(), revision));
92
+ },
93
+ now,
94
+ });
95
+ }
96
+ //# sourceMappingURL=rotation-stream.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ export declare const CREDENTIAL_POOL_STATE_FILENAME = "credential-pool-state.json";
3
+ declare const slotStateSchema: z.ZodObject<{
4
+ stateVersion: z.ZodNumber;
5
+ blockedUntil: z.ZodOptional<z.ZodNumber>;
6
+ blockReason: z.ZodOptional<z.ZodEnum<{
7
+ auth_error: "auth_error";
8
+ account_disabled: "account_disabled";
9
+ rate_limit: "rate_limit";
10
+ }>>;
11
+ failureCount: z.ZodOptional<z.ZodNumber>;
12
+ lastSuccessAt: z.ZodOptional<z.ZodNumber>;
13
+ credentialRevision: z.ZodOptional<z.ZodString>;
14
+ lease: z.ZodOptional<z.ZodObject<{
15
+ id: z.ZodString;
16
+ expiresAt: z.ZodNumber;
17
+ }, z.core.$strict>>;
18
+ }, z.core.$strict>;
19
+ export type CredentialSlotState = Readonly<z.infer<typeof slotStateSchema>>;
20
+ declare const documentSchema: z.ZodObject<{
21
+ schemaVersion: z.ZodLiteral<1>;
22
+ installationKey: z.ZodString;
23
+ providers: z.ZodRecord<z.ZodString, z.ZodObject<{
24
+ lanes: z.ZodRecord<z.ZodString, z.ZodObject<{
25
+ slots: z.ZodRecord<z.ZodString, z.ZodObject<{
26
+ stateVersion: z.ZodNumber;
27
+ blockedUntil: z.ZodOptional<z.ZodNumber>;
28
+ blockReason: z.ZodOptional<z.ZodEnum<{
29
+ auth_error: "auth_error";
30
+ account_disabled: "account_disabled";
31
+ rate_limit: "rate_limit";
32
+ }>>;
33
+ failureCount: z.ZodOptional<z.ZodNumber>;
34
+ lastSuccessAt: z.ZodOptional<z.ZodNumber>;
35
+ credentialRevision: z.ZodOptional<z.ZodString>;
36
+ lease: z.ZodOptional<z.ZodObject<{
37
+ id: z.ZodString;
38
+ expiresAt: z.ZodNumber;
39
+ }, z.core.$strict>>;
40
+ }, z.core.$strict>>;
41
+ }, z.core.$strict>>;
42
+ }, z.core.$strict>>;
43
+ }, z.core.$strict>;
44
+ export type CredentialPoolStateDocument = z.infer<typeof documentSchema>;
45
+ export declare function credentialPoolStatePath(agentDir?: string): string;
46
+ export type SlotHealth = "ready" | "blocked" | "half_open";
47
+ /** Deadlines are absolute so a restart never re-enables a cooling credential early. */
48
+ export declare function slotHealth(state: CredentialSlotState | undefined, now: number): SlotHealth;
49
+ /**
50
+ * File-locked health sidecar for credential pools. Never stores credential
51
+ * material: env slots are keyed by an installation-local HMAC revision so a
52
+ * rotated env value clears its own stale block without persisting a raw key
53
+ * hash anywhere.
54
+ */
55
+ export declare class CredentialSlotRepository {
56
+ private readonly path;
57
+ constructor(path?: string);
58
+ private ensureFile;
59
+ private withDocument;
60
+ installationKey(): Promise<string>;
61
+ /**
62
+ * Derives the persistence key for an env slot's health. HMAC over the
63
+ * installation key means the revision is useless outside this installation
64
+ * and never equals a raw hash of the key material.
65
+ */
66
+ envCredentialRevision(envVarName: string, envValue: string): Promise<string>;
67
+ listSlots(providerId: string, laneId: string): Promise<Record<string, CredentialSlotState>>;
68
+ /**
69
+ * Atomic read-modify-write for one slot's health. `stateVersion` increments
70
+ * on every write so concurrent mutators can detect lost updates; returning
71
+ * undefined removes the slot state entirely.
72
+ */
73
+ mutateSlotState(providerId: string, laneId: string, slotId: string, fn: (current: CredentialSlotState | undefined) => Omit<CredentialSlotState, "stateVersion"> | undefined): Promise<CredentialSlotState | undefined>;
74
+ }
75
+ export type HalfOpenLease = {
76
+ leaseId: string;
77
+ expiresAt: number;
78
+ };
79
+ /**
80
+ * Transitions an expired cooldown atomically to half_open: exactly one caller
81
+ * wins the probe lease; everyone else keeps treating the slot as unavailable
82
+ * until the lease expires or the probe settles the block.
83
+ */
84
+ export declare function acquireHalfOpenLease(repository: CredentialSlotRepository, providerId: string, laneId: string, slotId: string, options?: {
85
+ now?: number;
86
+ leaseTtlMs?: number;
87
+ }): Promise<HalfOpenLease | undefined>;
88
+ export {};
89
+ //# sourceMappingURL=state-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/state-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,8BAA8B,+BAA+B,CAAC;AAS3E,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;kBAQnB,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC;AAE5E,QAAA,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;kBAclB,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEzE,wBAAgB,uBAAuB,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEhF;AAsBD,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAE3D,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,CAS1F;AAED;;;;;GAKG;AACH,qBAAa,wBAAwB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,GAAE,MAAkC;IAIpD,OAAO,CAAC,UAAU;YAMJ,YAAY;IAepB,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAIxC;;;;OAIG;IACG,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK5E,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAMjG;;;;OAIG;IACG,eAAe,CACpB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,KAAK,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC,GAAG,SAAS,GACrG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;CAwB3C;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;GAIG;AACH,wBAAsB,oBAAoB,CACzC,UAAU,EAAE,wBAAwB,EACpC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAapC"}
@@ -0,0 +1,166 @@
1
+ import { createHmac, randomBytes, randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ import { z } from "zod";
6
+ import { getAgentDir } from "../../config.js";
7
+ import { FILE_STORAGE_LOCK_OPTIONS } from "../lockfile-policy.js";
8
+ export const CREDENTIAL_POOL_STATE_FILENAME = "credential-pool-state.json";
9
+ const HEX_256_BIT = /^[0-9a-f]{64}$/;
10
+ const leaseSchema = z.strictObject({
11
+ id: z.string().min(1).max(128),
12
+ expiresAt: z.number().int().positive(),
13
+ });
14
+ const slotStateSchema = z.strictObject({
15
+ stateVersion: z.number().int().nonnegative(),
16
+ blockedUntil: z.number().int().positive().optional(),
17
+ blockReason: z.enum(["auth_error", "rate_limit", "account_disabled"]).optional(),
18
+ failureCount: z.number().int().nonnegative().optional(),
19
+ lastSuccessAt: z.number().int().positive().optional(),
20
+ credentialRevision: z.string().regex(HEX_256_BIT).optional(),
21
+ lease: leaseSchema.optional(),
22
+ });
23
+ const documentSchema = z.strictObject({
24
+ schemaVersion: z.literal(1),
25
+ installationKey: z.string().regex(HEX_256_BIT),
26
+ providers: z.record(z.string(), z.strictObject({
27
+ lanes: z.record(z.string(), z.strictObject({
28
+ slots: z.record(z.string(), slotStateSchema),
29
+ })),
30
+ })),
31
+ });
32
+ export function credentialPoolStatePath(agentDir = getAgentDir()) {
33
+ return join(agentDir, CREDENTIAL_POOL_STATE_FILENAME);
34
+ }
35
+ const FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
36
+ function freshDocument() {
37
+ return { schemaVersion: 1, installationKey: randomBytes(32).toString("hex"), providers: {} };
38
+ }
39
+ /**
40
+ * The sidecar holds only health, so an unreadable or invalid document resets to
41
+ * a fresh one instead of failing auth resolution; the worst outcome of a reset
42
+ * is a cleared cooldown, never lost credential material.
43
+ */
44
+ function parseDocument(content) {
45
+ try {
46
+ const parsed = documentSchema.safeParse(JSON.parse(content));
47
+ return parsed.success ? parsed.data : freshDocument();
48
+ }
49
+ catch {
50
+ return freshDocument();
51
+ }
52
+ }
53
+ /** Deadlines are absolute so a restart never re-enables a cooling credential early. */
54
+ export function slotHealth(state, now) {
55
+ if (!state)
56
+ return "ready";
57
+ // Auth and billing blocks have no expiry; only credential replacement clears them.
58
+ if (state.blockReason === "auth_error" || state.blockReason === "account_disabled")
59
+ return "blocked";
60
+ if (state.blockedUntil !== undefined && state.blockedUntil > now)
61
+ return "blocked";
62
+ if (state.blockedUntil !== undefined && state.lease !== undefined && state.lease.expiresAt > now) {
63
+ return "half_open";
64
+ }
65
+ return "ready";
66
+ }
67
+ /**
68
+ * File-locked health sidecar for credential pools. Never stores credential
69
+ * material: env slots are keyed by an installation-local HMAC revision so a
70
+ * rotated env value clears its own stale block without persisting a raw key
71
+ * hash anywhere.
72
+ */
73
+ export class CredentialSlotRepository {
74
+ constructor(path = credentialPoolStatePath()) {
75
+ this.path = path;
76
+ }
77
+ ensureFile() {
78
+ if (existsSync(this.path))
79
+ return;
80
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
81
+ writeFileSync(this.path, JSON.stringify(freshDocument(), null, 2), FILE_WRITE_OPTIONS);
82
+ }
83
+ async withDocument(fn) {
84
+ this.ensureFile();
85
+ const release = await lockfile.lock(this.path, FILE_STORAGE_LOCK_OPTIONS);
86
+ try {
87
+ const document = parseDocument(readFileSync(this.path, "utf-8"));
88
+ const { result, next } = fn(document);
89
+ if (next)
90
+ writeFileSync(this.path, JSON.stringify(next, null, 2), FILE_WRITE_OPTIONS);
91
+ return result;
92
+ }
93
+ finally {
94
+ await release();
95
+ }
96
+ }
97
+ async installationKey() {
98
+ return this.withDocument((document) => ({ result: document.installationKey, next: document }));
99
+ }
100
+ /**
101
+ * Derives the persistence key for an env slot's health. HMAC over the
102
+ * installation key means the revision is useless outside this installation
103
+ * and never equals a raw hash of the key material.
104
+ */
105
+ async envCredentialRevision(envVarName, envValue) {
106
+ const key = await this.installationKey();
107
+ return createHmac("sha256", key).update(`${envVarName}\0${envValue}`).digest("hex");
108
+ }
109
+ async listSlots(providerId, laneId) {
110
+ return this.withDocument((document) => ({
111
+ result: { ...document.providers[providerId]?.lanes[laneId]?.slots },
112
+ }));
113
+ }
114
+ /**
115
+ * Atomic read-modify-write for one slot's health. `stateVersion` increments
116
+ * on every write so concurrent mutators can detect lost updates; returning
117
+ * undefined removes the slot state entirely.
118
+ */
119
+ async mutateSlotState(providerId, laneId, slotId, fn) {
120
+ return this.withDocument((document) => {
121
+ const provider = document.providers[providerId] ?? { lanes: {} };
122
+ const lane = provider.lanes[laneId] ?? { slots: {} };
123
+ const current = lane.slots[slotId];
124
+ const mutated = fn(current);
125
+ const slots = { ...lane.slots };
126
+ let result;
127
+ if (mutated === undefined) {
128
+ delete slots[slotId];
129
+ }
130
+ else {
131
+ result = { ...mutated, stateVersion: (current?.stateVersion ?? 0) + 1 };
132
+ slots[slotId] = result;
133
+ }
134
+ const next = {
135
+ ...document,
136
+ providers: {
137
+ ...document.providers,
138
+ [providerId]: { lanes: { ...provider.lanes, [laneId]: { slots } } },
139
+ },
140
+ };
141
+ return { result, next };
142
+ });
143
+ }
144
+ }
145
+ /**
146
+ * Transitions an expired cooldown atomically to half_open: exactly one caller
147
+ * wins the probe lease; everyone else keeps treating the slot as unavailable
148
+ * until the lease expires or the probe settles the block.
149
+ */
150
+ export async function acquireHalfOpenLease(repository, providerId, laneId, slotId, options = {}) {
151
+ const now = options.now ?? Date.now();
152
+ const leaseTtlMs = options.leaseTtlMs ?? 30_000;
153
+ let lease;
154
+ await repository.mutateSlotState(providerId, laneId, slotId, (current) => {
155
+ if (!current)
156
+ return current;
157
+ const expired = current.blockedUntil !== undefined && current.blockedUntil <= now;
158
+ const leaseLive = current.lease !== undefined && current.lease.expiresAt > now;
159
+ if (!expired || leaseLive || current.blockReason === "auth_error")
160
+ return current;
161
+ lease = { leaseId: randomUUID(), expiresAt: now + leaseTtlMs };
162
+ return { ...current, lease: { id: lease.leaseId, expiresAt: lease.expiresAt } };
163
+ });
164
+ return lease;
165
+ }
166
+ //# sourceMappingURL=state-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.js","sourceRoot":"","sources":["../../../src/core/credential-pool/state-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,QAAQ,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AAElE,MAAM,CAAC,MAAM,8BAA8B,GAAG,4BAA4B,CAAC;AAE3E,MAAM,WAAW,GAAG,gBAAgB,CAAC;AAErC,MAAM,WAAW,GAAG,CAAC,CAAC,YAAY,CAAC;IAClC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC;AAEH,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IACtC,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC5C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACpD,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,kBAAkB,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChF,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IACvD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACrD,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE;IAC5D,KAAK,EAAE,WAAW,CAAC,QAAQ,EAAE;CAC7B,CAAC,CAAC;AAIH,MAAM,cAAc,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC;IAC9C,SAAS,EAAE,CAAC,CAAC,MAAM,CAClB,CAAC,CAAC,MAAM,EAAE,EACV,CAAC,CAAC,YAAY,CAAC;QACd,KAAK,EAAE,CAAC,CAAC,MAAM,CACd,CAAC,CAAC,MAAM,EAAE,EACV,CAAC,CAAC,YAAY,CAAC;YACd,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC;SAC5C,CAAC,CACF;KACD,CAAC,CACF;CACD,CAAC,CAAC;AAIH,MAAM,UAAU,uBAAuB,CAAC,WAAmB,WAAW,EAAE;IACvE,OAAO,IAAI,CAAC,QAAQ,EAAE,8BAA8B,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,kBAAkB,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAW,CAAC;AAEvE,SAAS,aAAa;IACrB,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AAC9F,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,OAAe;IACrC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7D,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,aAAa,EAAE,CAAC;IACxB,CAAC;AACF,CAAC;AAID,uFAAuF;AACvF,MAAM,UAAU,UAAU,CAAC,KAAsC,EAAE,GAAW;IAC7E,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IAC3B,mFAAmF;IACnF,IAAI,KAAK,CAAC,WAAW,KAAK,YAAY,IAAI,KAAK,CAAC,WAAW,KAAK,kBAAkB;QAAE,OAAO,SAAS,CAAC;IACrG,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,IAAI,KAAK,CAAC,YAAY,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IACnF,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;QAClG,OAAO,WAAW,CAAC;IACpB,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,OAAO,wBAAwB;IAGpC,YAAY,OAAe,uBAAuB,EAAE;QACnD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAClB,CAAC;IAEO,UAAU;QACjB,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QAClC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACxF,CAAC;IAEO,KAAK,CAAC,YAAY,CACzB,EAAgG;QAEhG,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,yBAAyB,CAAC,CAAC;QAC1E,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;YACjE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,IAAI;gBAAE,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;YACtF,OAAO,MAAM,CAAC;QACf,CAAC;gBAAS,CAAC;YACV,MAAM,OAAO,EAAE,CAAC;QACjB,CAAC;IACF,CAAC;IAED,KAAK,CAAC,eAAe;QACpB,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IAChG,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,qBAAqB,CAAC,UAAkB,EAAE,QAAgB;QAC/D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QACzC,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,GAAG,UAAU,KAAK,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrF,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,UAAkB,EAAE,MAAc;QACjD,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YACvC,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE;SACnE,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe,CACpB,UAAkB,EAClB,MAAc,EACd,MAAc,EACd,EAAuG;QAEvG,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,QAAQ,EAAE,EAAE;YACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YACjE,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YACrD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACnC,MAAM,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;YAC5B,MAAM,KAAK,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAChC,IAAI,MAAuC,CAAC;YAC5C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC3B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACP,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,CAAC,OAAO,EAAE,YAAY,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;YACxB,CAAC;YACD,MAAM,IAAI,GAAgC;gBACzC,GAAG,QAAQ;gBACX,SAAS,EAAE;oBACV,GAAG,QAAQ,CAAC,SAAS;oBACrB,CAAC,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;iBACnE;aACD,CAAC;YACF,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;IACJ,CAAC;CACD;AAID;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,UAAoC,EACpC,UAAkB,EAClB,MAAc,EACd,MAAc,EACd,UAAiD,EAAE;IAEnD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC;IAChD,IAAI,KAAgC,CAAC;IACrC,MAAM,UAAU,CAAC,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;QACxE,IAAI,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC;QAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,OAAO,CAAC,YAAY,IAAI,GAAG,CAAC;QAClF,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC;QAC/E,IAAI,CAAC,OAAO,IAAI,SAAS,IAAI,OAAO,CAAC,WAAW,KAAK,YAAY;YAAE,OAAO,OAAO,CAAC;QAClF,KAAK,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,GAAG,GAAG,UAAU,EAAE,CAAC;QAC/D,OAAO,EAAE,GAAG,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;IACjF,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["import { createHmac, randomBytes, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport { z } from \"zod\";\nimport { getAgentDir } from \"../../config.ts\";\nimport { FILE_STORAGE_LOCK_OPTIONS } from \"../lockfile-policy.ts\";\n\nexport const CREDENTIAL_POOL_STATE_FILENAME = \"credential-pool-state.json\";\n\nconst HEX_256_BIT = /^[0-9a-f]{64}$/;\n\nconst leaseSchema = z.strictObject({\n\tid: z.string().min(1).max(128),\n\texpiresAt: z.number().int().positive(),\n});\n\nconst slotStateSchema = z.strictObject({\n\tstateVersion: z.number().int().nonnegative(),\n\tblockedUntil: z.number().int().positive().optional(),\n\tblockReason: z.enum([\"auth_error\", \"rate_limit\", \"account_disabled\"]).optional(),\n\tfailureCount: z.number().int().nonnegative().optional(),\n\tlastSuccessAt: z.number().int().positive().optional(),\n\tcredentialRevision: z.string().regex(HEX_256_BIT).optional(),\n\tlease: leaseSchema.optional(),\n});\n\nexport type CredentialSlotState = Readonly<z.infer<typeof slotStateSchema>>;\n\nconst documentSchema = z.strictObject({\n\tschemaVersion: z.literal(1),\n\tinstallationKey: z.string().regex(HEX_256_BIT),\n\tproviders: z.record(\n\t\tz.string(),\n\t\tz.strictObject({\n\t\t\tlanes: z.record(\n\t\t\t\tz.string(),\n\t\t\t\tz.strictObject({\n\t\t\t\t\tslots: z.record(z.string(), slotStateSchema),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t),\n});\n\nexport type CredentialPoolStateDocument = z.infer<typeof documentSchema>;\n\nexport function credentialPoolStatePath(agentDir: string = getAgentDir()): string {\n\treturn join(agentDir, CREDENTIAL_POOL_STATE_FILENAME);\n}\n\nconst FILE_WRITE_OPTIONS = { encoding: \"utf-8\", mode: 0o600 } as const;\n\nfunction freshDocument(): CredentialPoolStateDocument {\n\treturn { schemaVersion: 1, installationKey: randomBytes(32).toString(\"hex\"), providers: {} };\n}\n\n/**\n * The sidecar holds only health, so an unreadable or invalid document resets to\n * a fresh one instead of failing auth resolution; the worst outcome of a reset\n * is a cleared cooldown, never lost credential material.\n */\nfunction parseDocument(content: string): CredentialPoolStateDocument {\n\ttry {\n\t\tconst parsed = documentSchema.safeParse(JSON.parse(content));\n\t\treturn parsed.success ? parsed.data : freshDocument();\n\t} catch {\n\t\treturn freshDocument();\n\t}\n}\n\nexport type SlotHealth = \"ready\" | \"blocked\" | \"half_open\";\n\n/** Deadlines are absolute so a restart never re-enables a cooling credential early. */\nexport function slotHealth(state: CredentialSlotState | undefined, now: number): SlotHealth {\n\tif (!state) return \"ready\";\n\t// Auth and billing blocks have no expiry; only credential replacement clears them.\n\tif (state.blockReason === \"auth_error\" || state.blockReason === \"account_disabled\") return \"blocked\";\n\tif (state.blockedUntil !== undefined && state.blockedUntil > now) return \"blocked\";\n\tif (state.blockedUntil !== undefined && state.lease !== undefined && state.lease.expiresAt > now) {\n\t\treturn \"half_open\";\n\t}\n\treturn \"ready\";\n}\n\n/**\n * File-locked health sidecar for credential pools. Never stores credential\n * material: env slots are keyed by an installation-local HMAC revision so a\n * rotated env value clears its own stale block without persisting a raw key\n * hash anywhere.\n */\nexport class CredentialSlotRepository {\n\tprivate readonly path: string;\n\n\tconstructor(path: string = credentialPoolStatePath()) {\n\t\tthis.path = path;\n\t}\n\n\tprivate ensureFile(): void {\n\t\tif (existsSync(this.path)) return;\n\t\tmkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });\n\t\twriteFileSync(this.path, JSON.stringify(freshDocument(), null, 2), FILE_WRITE_OPTIONS);\n\t}\n\n\tprivate async withDocument<T>(\n\t\tfn: (document: CredentialPoolStateDocument) => { result: T; next?: CredentialPoolStateDocument },\n\t): Promise<T> {\n\t\tthis.ensureFile();\n\t\tconst release = await lockfile.lock(this.path, FILE_STORAGE_LOCK_OPTIONS);\n\t\ttry {\n\t\t\tconst document = parseDocument(readFileSync(this.path, \"utf-8\"));\n\t\t\tconst { result, next } = fn(document);\n\t\t\tif (next) writeFileSync(this.path, JSON.stringify(next, null, 2), FILE_WRITE_OPTIONS);\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tawait release();\n\t\t}\n\t}\n\n\tasync installationKey(): Promise<string> {\n\t\treturn this.withDocument((document) => ({ result: document.installationKey, next: document }));\n\t}\n\n\t/**\n\t * Derives the persistence key for an env slot's health. HMAC over the\n\t * installation key means the revision is useless outside this installation\n\t * and never equals a raw hash of the key material.\n\t */\n\tasync envCredentialRevision(envVarName: string, envValue: string): Promise<string> {\n\t\tconst key = await this.installationKey();\n\t\treturn createHmac(\"sha256\", key).update(`${envVarName}\\0${envValue}`).digest(\"hex\");\n\t}\n\n\tasync listSlots(providerId: string, laneId: string): Promise<Record<string, CredentialSlotState>> {\n\t\treturn this.withDocument((document) => ({\n\t\t\tresult: { ...document.providers[providerId]?.lanes[laneId]?.slots },\n\t\t}));\n\t}\n\n\t/**\n\t * Atomic read-modify-write for one slot's health. `stateVersion` increments\n\t * on every write so concurrent mutators can detect lost updates; returning\n\t * undefined removes the slot state entirely.\n\t */\n\tasync mutateSlotState(\n\t\tproviderId: string,\n\t\tlaneId: string,\n\t\tslotId: string,\n\t\tfn: (current: CredentialSlotState | undefined) => Omit<CredentialSlotState, \"stateVersion\"> | undefined,\n\t): Promise<CredentialSlotState | undefined> {\n\t\treturn this.withDocument((document) => {\n\t\t\tconst provider = document.providers[providerId] ?? { lanes: {} };\n\t\t\tconst lane = provider.lanes[laneId] ?? { slots: {} };\n\t\t\tconst current = lane.slots[slotId];\n\t\t\tconst mutated = fn(current);\n\t\t\tconst slots = { ...lane.slots };\n\t\t\tlet result: CredentialSlotState | undefined;\n\t\t\tif (mutated === undefined) {\n\t\t\t\tdelete slots[slotId];\n\t\t\t} else {\n\t\t\t\tresult = { ...mutated, stateVersion: (current?.stateVersion ?? 0) + 1 };\n\t\t\t\tslots[slotId] = result;\n\t\t\t}\n\t\t\tconst next: CredentialPoolStateDocument = {\n\t\t\t\t...document,\n\t\t\t\tproviders: {\n\t\t\t\t\t...document.providers,\n\t\t\t\t\t[providerId]: { lanes: { ...provider.lanes, [laneId]: { slots } } },\n\t\t\t\t},\n\t\t\t};\n\t\t\treturn { result, next };\n\t\t});\n\t}\n}\n\nexport type HalfOpenLease = { leaseId: string; expiresAt: number };\n\n/**\n * Transitions an expired cooldown atomically to half_open: exactly one caller\n * wins the probe lease; everyone else keeps treating the slot as unavailable\n * until the lease expires or the probe settles the block.\n */\nexport async function acquireHalfOpenLease(\n\trepository: CredentialSlotRepository,\n\tproviderId: string,\n\tlaneId: string,\n\tslotId: string,\n\toptions: { now?: number; leaseTtlMs?: number } = {},\n): Promise<HalfOpenLease | undefined> {\n\tconst now = options.now ?? Date.now();\n\tconst leaseTtlMs = options.leaseTtlMs ?? 30_000;\n\tlet lease: HalfOpenLease | undefined;\n\tawait repository.mutateSlotState(providerId, laneId, slotId, (current) => {\n\t\tif (!current) return current;\n\t\tconst expired = current.blockedUntil !== undefined && current.blockedUntil <= now;\n\t\tconst leaseLive = current.lease !== undefined && current.lease.expiresAt > now;\n\t\tif (!expired || leaseLive || current.blockReason === \"auth_error\") return current;\n\t\tlease = { leaseId: randomUUID(), expiresAt: now + leaseTtlMs };\n\t\treturn { ...current, lease: { id: lease.leaseId, expiresAt: lease.expiresAt } };\n\t});\n\treturn lease;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEX,YAAY,EAIZ,MAAM,gBAAgB,CAAC;AA4BxB,OAAO,EAEN,KAAK,kCAAkC,EAIvC,MAAM,oBAAoB,CAAC;AAgD5B,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnG;AAuFD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAC1C,EAAE,EAAE,YAAY,EAChB,4BAA4B,GAAE,kCAAuC,GACnE,IAAI,CA+xBN"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/compaction/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEX,YAAY,EAIZ,MAAM,gBAAgB,CAAC;AA4BxB,OAAO,EAEN,KAAK,kCAAkC,EAIvC,MAAM,oBAAoB,CAAC;AAgD5B,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnG;AAuFD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAC1C,EAAE,EAAE,YAAY,EAChB,4BAA4B,GAAE,kCAAuC,GACnE,IAAI,CA+1BN"}
@@ -136,6 +136,12 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
136
136
  }
137
137
  let idleWarmupTimer;
138
138
  let idleWarmupAttempt = 0;
139
+ // True from the `agent_end` idle trigger until the next turn starts (or the
140
+ // session shuts down). The idle-apply watcher below is fenced on it so a
141
+ // summary that lands after the user has already prompted is never applied
142
+ // out from under the turn that is starting; the warm-consume path in
143
+ // `before_agent_start` owns the job from that point on.
144
+ let sessionIdleSinceAgentEnd = false;
139
145
  function cancelIdleWarmupRetry() {
140
146
  if (idleWarmupTimer === undefined)
141
147
  return;
@@ -212,9 +218,66 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
212
218
  invalidateSpeculativeCompaction(ctx);
213
219
  startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
214
220
  armIdleWarmupRetry(ctx);
221
+ armIdleApply(ctx);
215
222
  }, idleRetry.IDLE_WARMUP_RETRY_DELAY_MS);
216
223
  });
217
224
  }
225
+ /**
226
+ * Apply the idle warm summary as soon as it finishes generating, while the
227
+ * session is still idle. Holding it warm until the next `before_agent_start`
228
+ * makes the user watch their own prompt wait behind a compaction they could
229
+ * not see coming; applying during the idle gap renders the [compaction] block
230
+ * first and lets the next message stack below it.
231
+ *
232
+ * Every guard is re-read at continuation time, because generation takes long
233
+ * enough for all of them to change: the session may no longer be idle, the
234
+ * job may have been invalidated or claimed, the context may have dropped
235
+ * below the threshold, the lane may have been handed to the SDK, or the
236
+ * breaker may have tripped. A refused apply (stale anchor/revision) silently
237
+ * keeps the warm hold, so the next prompt consumes it exactly as before.
238
+ */
239
+ function armIdleApply(ctx) {
240
+ const job = speculativeJob;
241
+ if (!job)
242
+ return;
243
+ // Never throws out of the continuation: a retired context, a refused apply,
244
+ // or a provider failure all resolve into a silent stand-down.
245
+ void job.promise
246
+ .then(async (compaction) => {
247
+ if (!compaction)
248
+ return;
249
+ if (speculativeJob !== job)
250
+ return;
251
+ if (!sessionIdleSinceAgentEnd)
252
+ return;
253
+ if (isContextRetired(ctx))
254
+ return;
255
+ if (!ctx.isIdle())
256
+ return;
257
+ if (lanePolicy.disablesSenpiCompaction(ctx))
258
+ return;
259
+ if (breaker.isTripped(state, Date.now()))
260
+ return;
261
+ if (cap.shouldRejectByCap(state).cancel)
262
+ return;
263
+ const usage = ctx.getContextUsage();
264
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
265
+ if (!usage ||
266
+ !policy.shouldTriggerCompaction(usage, contextWindow, ctx.getCompactionSettings(), state.lastYield ?? undefined)) {
267
+ return;
268
+ }
269
+ const result = await applyGeneratedCompaction(ctx, job.snapshot, () => speculativeGeneration, compaction);
270
+ if (!result.applied)
271
+ return;
272
+ // The job is consumed: clearing it here is also what stands the idle
273
+ // retry watcher down for this generation.
274
+ if (speculativeJob === job)
275
+ speculativeJob = undefined;
276
+ cancelIdleWarmupRetry();
277
+ getLogger(ctx).debug("idle_applied", { generation: job.generation, origin: "speculative" });
278
+ })
279
+ .catch(() => { });
280
+ }
218
281
  function isSameModelIdentity(left, right) {
219
282
  return (right !== undefined &&
220
283
  left.api === right.api &&
@@ -661,6 +724,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
661
724
  state = breaker.recordFailure(state, Date.now(), { route: compactEvent.reason });
662
725
  });
663
726
  pi.on("before_agent_start", async (event, ctx) => {
727
+ sessionIdleSinceAgentEnd = false;
664
728
  cancelIdleWarmupRetry();
665
729
  const message = checkpointState.attachRestorationDirective(restorationDirectiveState, recentCheckpoint(ctx), restoration.consumePendingPayload(restorationState));
666
730
  const usage = ctx.getContextUsage();
@@ -781,8 +845,10 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
781
845
  })) {
782
846
  getLogger(ctx).debug("idle_trigger", { contextWindow, tokens: usage?.tokens ?? 0 });
783
847
  idleWarmupAttempt = 0;
848
+ sessionIdleSinceAgentEnd = true;
784
849
  startSpeculativeCompaction(ctx, idle.IDLE_COMPACTION_INSTRUCTIONS);
785
850
  armIdleWarmupRetry(ctx);
851
+ armIdleApply(ctx);
786
852
  }
787
853
  });
788
854
  pi.on("message_end", async (event, ctx) => {
@@ -809,6 +875,7 @@ export default function compactionExtension(pi, remoteCompactionDependencies = {
809
875
  // warm-up watcher down here rather than leaving a timer armed against a
810
876
  // context that is about to start throwing on every read.
811
877
  pi.on("session_shutdown", () => {
878
+ sessionIdleSinceAgentEnd = false;
812
879
  cancelIdleWarmupRetry();
813
880
  idleWarmupAttempt = 0;
814
881
  speculativeJob?.controller.abort();