@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
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAgJrB"}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAgLrB"}
@@ -26,6 +26,8 @@ export async function executeBashWithOperations(command, cwd, operations, option
26
26
  const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
27
27
  let tempFilePath;
28
28
  let tempFileStream;
29
+ let tempFileError;
30
+ let tempFileErrorListener;
29
31
  let totalBytes = 0;
30
32
  const compactOutputChunks = () => {
31
33
  if (outputChunkStart < 64) {
@@ -42,6 +44,10 @@ export async function executeBashWithOperations(command, cwd, operations, option
42
44
  const id = randomBytes(8).toString("hex");
43
45
  tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
44
46
  tempFileStream = createWriteStream(tempFilePath);
47
+ tempFileErrorListener = (error) => {
48
+ tempFileError ??= error;
49
+ };
50
+ tempFileStream.on("error", tempFileErrorListener);
45
51
  for (let i = outputChunkStart; i < outputChunks.length; i++) {
46
52
  const chunk = outputChunks[i];
47
53
  if (chunk !== undefined) {
@@ -55,17 +61,38 @@ export async function executeBashWithOperations(command, cwd, operations, option
55
61
  if (!stream) {
56
62
  return;
57
63
  }
64
+ if (tempFileError) {
65
+ if (tempFileErrorListener)
66
+ stream.off("error", tempFileErrorListener);
67
+ stream.destroy();
68
+ throw tempFileError;
69
+ }
58
70
  await new Promise((resolve, reject) => {
71
+ let settled = false;
72
+ const cleanup = () => {
73
+ stream.off("error", onError);
74
+ if (tempFileErrorListener)
75
+ stream.off("error", tempFileErrorListener);
76
+ stream.off("close", onClose);
77
+ };
78
+ const settle = (error) => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ cleanup();
83
+ if (error)
84
+ reject(error);
85
+ else
86
+ resolve();
87
+ };
59
88
  const onError = (error) => {
60
- stream.off("finish", onFinish);
61
- reject(error);
89
+ tempFileError ??= error;
62
90
  };
63
- const onFinish = () => {
64
- stream.off("error", onError);
65
- resolve();
91
+ const onClose = () => {
92
+ settle(tempFileError);
66
93
  };
67
- stream.once("error", onError);
68
- stream.once("finish", onFinish);
94
+ stream.on("error", onError);
95
+ stream.once("close", onClose);
69
96
  stream.end();
70
97
  });
71
98
  };
@@ -103,36 +130,37 @@ export async function executeBashWithOperations(command, cwd, operations, option
103
130
  onData(Buffer.from(finalText, "utf-8"));
104
131
  }
105
132
  };
133
+ const prepareFinalOutput = async () => {
134
+ try {
135
+ finishDecoder();
136
+ const fullOutput = outputText();
137
+ const truncationResult = truncateTail(fullOutput);
138
+ if (truncationResult.truncated) {
139
+ ensureTempFile();
140
+ }
141
+ return { fullOutput, truncationResult };
142
+ }
143
+ catch (error) {
144
+ try {
145
+ await closeTempFileStream();
146
+ }
147
+ catch (closeError) {
148
+ throw new AggregateError([error, closeError], "Bash output finalization and spill cleanup failed");
149
+ }
150
+ throw error;
151
+ }
152
+ };
153
+ let result;
106
154
  try {
107
- const result = await operations.exec(command, cwd, {
155
+ result = await operations.exec(command, cwd, {
108
156
  onData,
109
157
  signal: options?.signal,
110
158
  });
111
- finishDecoder();
112
- const fullOutput = outputText();
113
- const truncationResult = truncateTail(fullOutput);
114
- if (truncationResult.truncated) {
115
- ensureTempFile();
116
- }
117
- await closeTempFileStream();
118
- const cancelled = options?.signal?.aborted ?? false;
119
- return {
120
- output: truncationResult.truncated ? truncationResult.content : fullOutput,
121
- exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
122
- cancelled,
123
- truncated: truncationResult.truncated,
124
- fullOutputPath: tempFilePath,
125
- };
126
159
  }
127
160
  catch (err) {
128
161
  // Check if it was an abort
129
162
  if (options?.signal?.aborted) {
130
- finishDecoder();
131
- const fullOutput = outputText();
132
- const truncationResult = truncateTail(fullOutput);
133
- if (truncationResult.truncated) {
134
- ensureTempFile();
135
- }
163
+ const { fullOutput, truncationResult } = await prepareFinalOutput();
136
164
  await closeTempFileStream();
137
165
  return {
138
166
  output: truncationResult.truncated ? truncationResult.content : fullOutput,
@@ -145,5 +173,15 @@ export async function executeBashWithOperations(command, cwd, operations, option
145
173
  await closeTempFileStream();
146
174
  throw err;
147
175
  }
176
+ const { fullOutput, truncationResult } = await prepareFinalOutput();
177
+ await closeTempFileStream();
178
+ const cancelled = options?.signal?.aborted ?? false;
179
+ return {
180
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
181
+ exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
182
+ cancelled,
183
+ truncated: truncationResult.truncated,
184
+ fullOutputPath: tempFilePath,
185
+ };
148
186
  }
149
187
  //# sourceMappingURL=bash-executor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputChunkStart = 0;\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst compactOutputChunks = () => {\n\t\tif (outputChunkStart < 64) {\n\t\t\treturn;\n\t\t}\n\t\toutputChunks.splice(0, outputChunkStart);\n\t\toutputChunkStart = 0;\n\t};\n\n\tconst outputText = () => outputChunks.slice(outputChunkStart).join(\"\");\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\tfor (let i = outputChunkStart; i < outputChunks.length; i++) {\n\t\t\tconst chunk = outputChunks[i];\n\t\t\tif (chunk !== undefined) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeTempFileStream = async () => {\n\t\tconst stream = tempFileStream;\n\t\ttempFileStream = undefined;\n\t\tif (!stream) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\tstream.off(\"finish\", onFinish);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onFinish = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tstream.once(\"error\", onError);\n\t\t\tstream.once(\"finish\", onFinish);\n\t\t\tstream.end();\n\t\t});\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunkStart < outputChunks.length - 1) {\n\t\t\tconst removed = outputChunks[outputChunkStart];\n\t\t\tif (removed === undefined) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toutputBytes -= removed.length;\n\t\t\toutputChunkStart++;\n\t\t}\n\t\tcompactOutputChunks();\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst finishDecoder = () => {\n\t\tconst finalText = sanitizeBinaryOutput(stripAnsi(decoder.decode())).replace(/\\r/g, \"\");\n\t\tif (finalText.length > 0) {\n\t\t\tonData(Buffer.from(finalText, \"utf-8\"));\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tfinishDecoder();\n\t\tconst fullOutput = outputText();\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFileStream();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tfinishDecoder();\n\t\t\tconst fullOutput = outputText();\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFileStream();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFileStream();\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputChunkStart = 0;\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet tempFileError: Error | undefined;\n\tlet tempFileErrorListener: ((error: Error) => void) | undefined;\n\tlet totalBytes = 0;\n\n\tconst compactOutputChunks = () => {\n\t\tif (outputChunkStart < 64) {\n\t\t\treturn;\n\t\t}\n\t\toutputChunks.splice(0, outputChunkStart);\n\t\toutputChunkStart = 0;\n\t};\n\n\tconst outputText = () => outputChunks.slice(outputChunkStart).join(\"\");\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\ttempFileErrorListener = (error) => {\n\t\t\ttempFileError ??= error;\n\t\t};\n\t\ttempFileStream.on(\"error\", tempFileErrorListener);\n\t\tfor (let i = outputChunkStart; i < outputChunks.length; i++) {\n\t\t\tconst chunk = outputChunks[i];\n\t\t\tif (chunk !== undefined) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeTempFileStream = async () => {\n\t\tconst stream = tempFileStream;\n\t\ttempFileStream = undefined;\n\t\tif (!stream) {\n\t\t\treturn;\n\t\t}\n\t\tif (tempFileError) {\n\t\t\tif (tempFileErrorListener) stream.off(\"error\", tempFileErrorListener);\n\t\t\tstream.destroy();\n\t\t\tthrow tempFileError;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false;\n\t\t\tconst cleanup = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tif (tempFileErrorListener) stream.off(\"error\", tempFileErrorListener);\n\t\t\t\tstream.off(\"close\", onClose);\n\t\t\t};\n\t\t\tconst settle = (error?: Error) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\tif (error) reject(error);\n\t\t\t\telse resolve();\n\t\t\t};\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\ttempFileError ??= error;\n\t\t\t};\n\t\t\tconst onClose = () => {\n\t\t\t\tsettle(tempFileError);\n\t\t\t};\n\t\t\tstream.on(\"error\", onError);\n\t\t\tstream.once(\"close\", onClose);\n\t\t\tstream.end();\n\t\t});\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunkStart < outputChunks.length - 1) {\n\t\t\tconst removed = outputChunks[outputChunkStart];\n\t\t\tif (removed === undefined) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toutputBytes -= removed.length;\n\t\t\toutputChunkStart++;\n\t\t}\n\t\tcompactOutputChunks();\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst finishDecoder = () => {\n\t\tconst finalText = sanitizeBinaryOutput(stripAnsi(decoder.decode())).replace(/\\r/g, \"\");\n\t\tif (finalText.length > 0) {\n\t\t\tonData(Buffer.from(finalText, \"utf-8\"));\n\t\t}\n\t};\n\n\tconst prepareFinalOutput = async () => {\n\t\ttry {\n\t\t\tfinishDecoder();\n\t\t\tconst fullOutput = outputText();\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\treturn { fullOutput, truncationResult };\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tawait closeTempFileStream();\n\t\t\t} catch (closeError) {\n\t\t\t\tthrow new AggregateError([error, closeError], \"Bash output finalization and spill cleanup failed\");\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tlet result: Awaited<ReturnType<BashOperations[\"exec\"]>>;\n\ttry {\n\t\tresult = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst { fullOutput, truncationResult } = await prepareFinalOutput();\n\t\t\tawait closeTempFileStream();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFileStream();\n\n\t\tthrow err;\n\t}\n\n\tconst { fullOutput, truncationResult } = await prepareFinalOutput();\n\tawait closeTempFileStream();\n\tconst cancelled = options?.signal?.aborted ?? false;\n\n\treturn {\n\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\tcancelled,\n\t\ttruncated: truncationResult.truncated,\n\t\tfullOutputPath: tempFilePath,\n\t};\n}\n"]}
@@ -0,0 +1,42 @@
1
+ export declare const COOLDOWN_BASE_MS = 60000;
2
+ export declare const COOLDOWN_CAP_MS: number;
3
+ export declare const RETRY_SAME_MAX_ATTEMPTS = 2;
4
+ export type CredentialBlock = {
5
+ reason: "auth_error";
6
+ } | {
7
+ reason: "account_disabled";
8
+ } | {
9
+ reason: "rate_limit";
10
+ cooldownMs: number;
11
+ retryAfterWasCapped: boolean;
12
+ };
13
+ export type CredentialAction = {
14
+ kind: "failover";
15
+ block: CredentialBlock;
16
+ } | {
17
+ kind: "retry_same";
18
+ maxAttempts: typeof RETRY_SAME_MAX_ATTEMPTS;
19
+ } | {
20
+ kind: "fail_request";
21
+ };
22
+ /**
23
+ * Per-slot exponential cooldown with the server hint as a FLOOR, never an
24
+ * override: a hint can only lengthen the wait the failure count already earned,
25
+ * and everything caps at 48 hours with the capping recorded.
26
+ */
27
+ export declare function rateLimitCooldown(failureCount: number, serverHintMs?: number): {
28
+ cooldownMs: number;
29
+ retryAfterWasCapped: boolean;
30
+ };
31
+ /**
32
+ * Maps a provider failure onto the credential-pool action space. Credential-
33
+ * scoped failures fail over (permanently for auth/billing, cooldown for rate
34
+ * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot
35
+ * without blocking it, because blocking a healthy credential for a provider
36
+ * outage only destroys prompt-cache locality; everything else fails the
37
+ * request so the model fallback chain above keeps owning it.
38
+ */
39
+ export declare function classifyCredentialFailure(error: unknown, context?: {
40
+ failureCount?: number;
41
+ }): CredentialAction;
42
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/classify.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AACtD,eAAO,MAAM,eAAe,QAAoB,CAAC;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,MAAM,MAAM,eAAe,GACxB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GACzB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,OAAO,uBAAuB,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACnB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAItD;AAuBD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CAgCnH"}
@@ -0,0 +1,75 @@
1
+ import { DEFAULT_SLOT_BLOCK_MS, MAX_SLOT_BLOCK_MS } from "@earendil-works/pi-ai/auth/pool/failover";
2
+ import { normalizeProviderError } from "@earendil-works/pi-ai/utils/error-body";
3
+ import { getOverflowPatterns } from "@earendil-works/pi-ai/utils/overflow";
4
+ import { extract429RetryAfterMs } from "@earendil-works/pi-ai/utils/retry-hint";
5
+ export const COOLDOWN_BASE_MS = DEFAULT_SLOT_BLOCK_MS;
6
+ export const COOLDOWN_CAP_MS = MAX_SLOT_BLOCK_MS;
7
+ export const RETRY_SAME_MAX_ATTEMPTS = 2;
8
+ /**
9
+ * Per-slot exponential cooldown with the server hint as a FLOOR, never an
10
+ * override: a hint can only lengthen the wait the failure count already earned,
11
+ * and everything caps at 48 hours with the capping recorded.
12
+ */
13
+ export function rateLimitCooldown(failureCount, serverHintMs) {
14
+ const backoff = Math.min(COOLDOWN_CAP_MS, COOLDOWN_BASE_MS * 2 ** Math.max(0, failureCount));
15
+ const floored = serverHintMs === undefined ? backoff : Math.max(backoff, serverHintMs);
16
+ return { cooldownMs: Math.min(COOLDOWN_CAP_MS, floored), retryAfterWasCapped: floored > COOLDOWN_CAP_MS };
17
+ }
18
+ const INVALID_KEY_TEXT = /invalid[ _-]?(?:api[ _-]?)?key|authentication[_ ]?error|invalid x-api-key|unauthorized/i;
19
+ const ACCOUNT_SCOPED_403_TEXT = /account|credential|token|api[ _-]?key|organization|subscription/i;
20
+ const RATE_LIMIT_TEXT = /rate[ _-]?limit|too many requests|resource_exhausted/i;
21
+ const BILLING_TEXT = /billing|credits?[ _-]?(?:required|exhausted|balance)|insufficient[ _-]?(?:funds|quota|credit)|payment[ _-]?required|quota[ _-]?exhausted/i;
22
+ const OVERLOAD_TEXT = /overloaded/i;
23
+ const NETWORK_TEXT = /econnreset|econnrefused|etimedout|enotfound|socket hang up|fetch failed|network error|request timed out/i;
24
+ const FAIL_FAST_TEXT = /context[ _-]?(?:length|window)|maximum context|invalid[ _-]?model|model[ _-]?not[ _-]?found|malformed[ _-]?stream|premature[ _-]?(?:close|stream)/i;
25
+ const ABORT_TEXT = /\baborted?\b/i;
26
+ function isAbort(error, message) {
27
+ if (error instanceof Error && error.name === "AbortError")
28
+ return true;
29
+ return ABORT_TEXT.test(message);
30
+ }
31
+ function isOverflowText(text) {
32
+ return getOverflowPatterns().some((pattern) => pattern.test(text));
33
+ }
34
+ /**
35
+ * Maps a provider failure onto the credential-pool action space. Credential-
36
+ * scoped failures fail over (permanently for auth/billing, cooldown for rate
37
+ * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot
38
+ * without blocking it, because blocking a healthy credential for a provider
39
+ * outage only destroys prompt-cache locality; everything else fails the
40
+ * request so the model fallback chain above keeps owning it.
41
+ */
42
+ export function classifyCredentialFailure(error, context = {}) {
43
+ const normalized = normalizeProviderError(error);
44
+ const text = normalized.messageCarriesBody ? normalized.message : `${normalized.message} ${normalized.body ?? ""}`;
45
+ const status = normalized.status;
46
+ const failureCount = context.failureCount ?? 0;
47
+ if (isAbort(error, text))
48
+ return { kind: "fail_request" };
49
+ if (status === 401 || INVALID_KEY_TEXT.test(text)) {
50
+ return { kind: "failover", block: { reason: "auth_error" } };
51
+ }
52
+ if (status === 403) {
53
+ return ACCOUNT_SCOPED_403_TEXT.test(text)
54
+ ? { kind: "failover", block: { reason: "auth_error" } }
55
+ : { kind: "fail_request" };
56
+ }
57
+ if (status === 402 || BILLING_TEXT.test(text)) {
58
+ return { kind: "failover", block: { reason: "account_disabled" } };
59
+ }
60
+ if (status === 429 || RATE_LIMIT_TEXT.test(text)) {
61
+ const hint = extract429RetryAfterMs({ status: status ?? 429, bodyText: text });
62
+ return { kind: "failover", block: { reason: "rate_limit", ...rateLimitCooldown(failureCount, hint) } };
63
+ }
64
+ if (isOverflowText(text) || status === 400 || status === 404 || FAIL_FAST_TEXT.test(text)) {
65
+ return { kind: "fail_request" };
66
+ }
67
+ if (status === 529 || OVERLOAD_TEXT.test(text) || (status !== undefined && status >= 500 && status < 600)) {
68
+ return { kind: "retry_same", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };
69
+ }
70
+ if (status === 408 || NETWORK_TEXT.test(text)) {
71
+ return { kind: "retry_same", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };
72
+ }
73
+ return { kind: "fail_request" };
74
+ }
75
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/core/credential-pool/classify.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_SLOT_BLOCK_MS, MAX_SLOT_BLOCK_MS } from \"@earendil-works/pi-ai/auth/pool/failover\";\nimport { normalizeProviderError } from \"@earendil-works/pi-ai/utils/error-body\";\nimport { getOverflowPatterns } from \"@earendil-works/pi-ai/utils/overflow\";\nimport { extract429RetryAfterMs } from \"@earendil-works/pi-ai/utils/retry-hint\";\n\nexport const COOLDOWN_BASE_MS = DEFAULT_SLOT_BLOCK_MS;\nexport const COOLDOWN_CAP_MS = MAX_SLOT_BLOCK_MS;\nexport const RETRY_SAME_MAX_ATTEMPTS = 2;\n\nexport type CredentialBlock =\n\t| { reason: \"auth_error\" }\n\t| { reason: \"account_disabled\" }\n\t| { reason: \"rate_limit\"; cooldownMs: number; retryAfterWasCapped: boolean };\n\nexport type CredentialAction =\n\t| { kind: \"failover\"; block: CredentialBlock }\n\t| { kind: \"retry_same\"; maxAttempts: typeof RETRY_SAME_MAX_ATTEMPTS }\n\t| { kind: \"fail_request\" };\n\n/**\n * Per-slot exponential cooldown with the server hint as a FLOOR, never an\n * override: a hint can only lengthen the wait the failure count already earned,\n * and everything caps at 48 hours with the capping recorded.\n */\nexport function rateLimitCooldown(\n\tfailureCount: number,\n\tserverHintMs?: number,\n): { cooldownMs: number; retryAfterWasCapped: boolean } {\n\tconst backoff = Math.min(COOLDOWN_CAP_MS, COOLDOWN_BASE_MS * 2 ** Math.max(0, failureCount));\n\tconst floored = serverHintMs === undefined ? backoff : Math.max(backoff, serverHintMs);\n\treturn { cooldownMs: Math.min(COOLDOWN_CAP_MS, floored), retryAfterWasCapped: floored > COOLDOWN_CAP_MS };\n}\n\nconst INVALID_KEY_TEXT = /invalid[ _-]?(?:api[ _-]?)?key|authentication[_ ]?error|invalid x-api-key|unauthorized/i;\nconst ACCOUNT_SCOPED_403_TEXT = /account|credential|token|api[ _-]?key|organization|subscription/i;\nconst RATE_LIMIT_TEXT = /rate[ _-]?limit|too many requests|resource_exhausted/i;\nconst BILLING_TEXT =\n\t/billing|credits?[ _-]?(?:required|exhausted|balance)|insufficient[ _-]?(?:funds|quota|credit)|payment[ _-]?required|quota[ _-]?exhausted/i;\nconst OVERLOAD_TEXT = /overloaded/i;\nconst NETWORK_TEXT =\n\t/econnreset|econnrefused|etimedout|enotfound|socket hang up|fetch failed|network error|request timed out/i;\nconst FAIL_FAST_TEXT =\n\t/context[ _-]?(?:length|window)|maximum context|invalid[ _-]?model|model[ _-]?not[ _-]?found|malformed[ _-]?stream|premature[ _-]?(?:close|stream)/i;\nconst ABORT_TEXT = /\\baborted?\\b/i;\n\nfunction isAbort(error: unknown, message: string): boolean {\n\tif (error instanceof Error && error.name === \"AbortError\") return true;\n\treturn ABORT_TEXT.test(message);\n}\n\nfunction isOverflowText(text: string): boolean {\n\treturn getOverflowPatterns().some((pattern) => pattern.test(text));\n}\n\n/**\n * Maps a provider failure onto the credential-pool action space. Credential-\n * scoped failures fail over (permanently for auth/billing, cooldown for rate\n * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot\n * without blocking it, because blocking a healthy credential for a provider\n * outage only destroys prompt-cache locality; everything else fails the\n * request so the model fallback chain above keeps owning it.\n */\nexport function classifyCredentialFailure(error: unknown, context: { failureCount?: number } = {}): CredentialAction {\n\tconst normalized = normalizeProviderError(error);\n\tconst text = normalized.messageCarriesBody ? normalized.message : `${normalized.message} ${normalized.body ?? \"\"}`;\n\tconst status = normalized.status;\n\tconst failureCount = context.failureCount ?? 0;\n\n\tif (isAbort(error, text)) return { kind: \"fail_request\" };\n\tif (status === 401 || INVALID_KEY_TEXT.test(text)) {\n\t\treturn { kind: \"failover\", block: { reason: \"auth_error\" } };\n\t}\n\tif (status === 403) {\n\t\treturn ACCOUNT_SCOPED_403_TEXT.test(text)\n\t\t\t? { kind: \"failover\", block: { reason: \"auth_error\" } }\n\t\t\t: { kind: \"fail_request\" };\n\t}\n\tif (status === 402 || BILLING_TEXT.test(text)) {\n\t\treturn { kind: \"failover\", block: { reason: \"account_disabled\" } };\n\t}\n\tif (status === 429 || RATE_LIMIT_TEXT.test(text)) {\n\t\tconst hint = extract429RetryAfterMs({ status: status ?? 429, bodyText: text });\n\t\treturn { kind: \"failover\", block: { reason: \"rate_limit\", ...rateLimitCooldown(failureCount, hint) } };\n\t}\n\tif (isOverflowText(text) || status === 400 || status === 404 || FAIL_FAST_TEXT.test(text)) {\n\t\treturn { kind: \"fail_request\" };\n\t}\n\tif (status === 529 || OVERLOAD_TEXT.test(text) || (status !== undefined && status >= 500 && status < 600)) {\n\t\treturn { kind: \"retry_same\", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };\n\t}\n\tif (status === 408 || NETWORK_TEXT.test(text)) {\n\t\treturn { kind: \"retry_same\", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };\n\t}\n\treturn { kind: \"fail_request\" };\n}\n"]}
@@ -0,0 +1,22 @@
1
+ export declare const MAX_ENV_SLOT_INDEX = 16;
2
+ export type EnvCredentialSlot = {
3
+ name: string;
4
+ envVarName: string;
5
+ key: string;
6
+ source: "env";
7
+ };
8
+ /**
9
+ * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered
10
+ * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the
11
+ * canonical mapping lists several variables.
12
+ */
13
+ export declare function primaryEnvVar(providerId: string): string | undefined;
14
+ /**
15
+ * Discovers numbered env credential slots: the primary var yields slot `env`,
16
+ * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is
17
+ * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no
18
+ * slot is invented for the gap. Stored credentials keep outranking every env
19
+ * slot because resolution consults env only when nothing is stored.
20
+ */
21
+ export declare function discoverEnvSlots(providerId: string, env: (name: string) => string | undefined): EnvCredentialSlot[];
22
+ //# sourceMappingURL=env-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-slots.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/env-slots.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,MAAM,MAAM,iBAAiB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIpE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAYnH"}
@@ -0,0 +1,37 @@
1
+ import { getApiKeyEnvVars } from "@earendil-works/pi-ai";
2
+ export const MAX_ENV_SLOT_INDEX = 16;
3
+ /**
4
+ * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered
5
+ * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the
6
+ * canonical mapping lists several variables.
7
+ */
8
+ export function primaryEnvVar(providerId) {
9
+ const envVars = getApiKeyEnvVars(providerId);
10
+ if (!envVars || envVars.length === 0)
11
+ return undefined;
12
+ return envVars.find((name) => name.endsWith("_API_KEY")) ?? envVars[0];
13
+ }
14
+ /**
15
+ * Discovers numbered env credential slots: the primary var yields slot `env`,
16
+ * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is
17
+ * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no
18
+ * slot is invented for the gap. Stored credentials keep outranking every env
19
+ * slot because resolution consults env only when nothing is stored.
20
+ */
21
+ export function discoverEnvSlots(providerId, env) {
22
+ const primary = primaryEnvVar(providerId);
23
+ if (!primary)
24
+ return [];
25
+ const slots = [];
26
+ const base = env(primary);
27
+ if (base)
28
+ slots.push({ name: "env", envVarName: primary, key: base, source: "env" });
29
+ for (let index = 2; index <= MAX_ENV_SLOT_INDEX; index++) {
30
+ const envVarName = `${primary}_${index}`;
31
+ const value = env(envVarName);
32
+ if (value)
33
+ slots.push({ name: `env-${index}`, envVarName, key: value, source: "env" });
34
+ }
35
+ return slots;
36
+ }
37
+ //# sourceMappingURL=env-slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-slots.js","sourceRoot":"","sources":["../../../src/core/credential-pool/env-slots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AASrC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC/C,MAAM,OAAO,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACvD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,GAAyC;IAC7F,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1B,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,kBAAkB,EAAE,KAAK,EAAE,EAAE,CAAC;QAC1D,MAAM,UAAU,GAAG,GAAG,OAAO,IAAI,KAAK,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9B,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["import { getApiKeyEnvVars } from \"@earendil-works/pi-ai\";\n\nexport const MAX_ENV_SLOT_INDEX = 16;\n\nexport type EnvCredentialSlot = {\n\tname: string;\n\tenvVarName: string;\n\tkey: string;\n\tsource: \"env\";\n};\n\n/**\n * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered\n * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the\n * canonical mapping lists several variables.\n */\nexport function primaryEnvVar(providerId: string): string | undefined {\n\tconst envVars = getApiKeyEnvVars(providerId);\n\tif (!envVars || envVars.length === 0) return undefined;\n\treturn envVars.find((name) => name.endsWith(\"_API_KEY\")) ?? envVars[0];\n}\n\n/**\n * Discovers numbered env credential slots: the primary var yields slot `env`,\n * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is\n * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no\n * slot is invented for the gap. Stored credentials keep outranking every env\n * slot because resolution consults env only when nothing is stored.\n */\nexport function discoverEnvSlots(providerId: string, env: (name: string) => string | undefined): EnvCredentialSlot[] {\n\tconst primary = primaryEnvVar(providerId);\n\tif (!primary) return [];\n\tconst slots: EnvCredentialSlot[] = [];\n\tconst base = env(primary);\n\tif (base) slots.push({ name: \"env\", envVarName: primary, key: base, source: \"env\" });\n\tfor (let index = 2; index <= MAX_ENV_SLOT_INDEX; index++) {\n\t\tconst envVarName = `${primary}_${index}`;\n\t\tconst value = env(envVarName);\n\t\tif (value) slots.push({ name: `env-${index}`, envVarName, key: value, source: \"env\" });\n\t}\n\treturn slots;\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import { TURN_RETRY_SUPPRESSION_PREFIX } from "@earendil-works/pi-ai/auth/pool/failover";
2
+ import { type CredentialAction, type CredentialBlock } from "./classify.ts";
3
+ export { TURN_RETRY_SUPPRESSION_PREFIX };
4
+ export type RunSlot = {
5
+ name: string;
6
+ blockedUntil?: number;
7
+ blockReason?: string;
8
+ failureCount?: number;
9
+ };
10
+ export type CredentialFailoverEvent<TSlot extends RunSlot> = {
11
+ slot: TSlot;
12
+ block: CredentialBlock;
13
+ attempt: number;
14
+ committedOutput: boolean;
15
+ };
16
+ export type RunCredentialFailoverOptions<TEvent, TSlot extends RunSlot> = {
17
+ /** Re-read before every distinct-credential attempt so a newly added slot participates. */
18
+ listSlots: () => Promise<readonly TSlot[]> | readonly TSlot[];
19
+ select: (slots: readonly TSlot[]) => TSlot;
20
+ runAttempt: (slot: TSlot) => AsyncIterable<TEvent> | Promise<AsyncIterable<TEvent>>;
21
+ /**
22
+ * REQUIRED and default-DENY by contract: return false only for event types
23
+ * explicitly known to be pre-commit bookkeeping. Any event this predicate
24
+ * does not recognize must count as committed output.
25
+ */
26
+ isCommittedOutput: (event: TEvent) => boolean;
27
+ classify?: (error: unknown, context: {
28
+ failureCount: number;
29
+ }) => CredentialAction;
30
+ errorFromEvent?: (event: TEvent) => unknown | undefined;
31
+ /** Persisted BEFORE a replacement slot is selected so a crash never forgets a block. */
32
+ persistBlock: (slot: TSlot, block: CredentialBlock) => void | Promise<void>;
33
+ onRotate?: (event: CredentialFailoverEvent<TSlot>) => void | Promise<void>;
34
+ now?: () => number;
35
+ };
36
+ export declare class CredentialFailoverError extends Error {
37
+ readonly action: CredentialAction;
38
+ readonly original: unknown;
39
+ readonly suppressTurnRetry: boolean;
40
+ readonly retryAt: number | undefined;
41
+ constructor(action: CredentialAction, original: unknown, options: {
42
+ suppressTurnRetry: boolean;
43
+ retryAt?: number;
44
+ });
45
+ }
46
+ /**
47
+ * Generic in-lane credential failover. Runs at most one failover attempt per
48
+ * slot per request, retries provider-scoped faults on the SAME slot up to the
49
+ * classifier's bound without blocking it, and rotates only while no committed
50
+ * output has reached the caller - afterwards the failure is rethrown with the
51
+ * `senpi:no-turn-retry:` marker so the turn is never replayed.
52
+ */
53
+ export declare function runCredentialFailover<TEvent, TSlot extends RunSlot>(options: RunCredentialFailoverOptions<TEvent, TSlot>): AsyncGenerator<TEvent>;
54
+ //# sourceMappingURL=failover.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,106 @@
1
+ import { TURN_RETRY_SUPPRESSION_PREFIX } from "@earendil-works/pi-ai/auth/pool/failover";
2
+ import { classifyCredentialFailure } from "./classify.js";
3
+ export { TURN_RETRY_SUPPRESSION_PREFIX };
4
+ export class CredentialFailoverError extends Error {
5
+ constructor(action, original, options) {
6
+ const detail = original instanceof Error ? original.message : String(original);
7
+ super(`${options.suppressTurnRetry ? TURN_RETRY_SUPPRESSION_PREFIX : ""}${detail}`, { cause: original });
8
+ this.name = "CredentialFailoverError";
9
+ this.action = action;
10
+ this.original = original;
11
+ this.suppressTurnRetry = options.suppressTurnRetry;
12
+ this.retryAt = options.retryAt;
13
+ }
14
+ }
15
+ function isAvailable(slot, now) {
16
+ if (slot.blockReason === "auth_error" || slot.blockReason === "account_disabled")
17
+ return false;
18
+ return slot.blockedUntil === undefined || slot.blockedUntil <= now;
19
+ }
20
+ function soonestRetryAt(slots, now) {
21
+ const deadlines = slots
22
+ .map((slot) => slot.blockedUntil)
23
+ .filter((value) => value !== undefined && value > now);
24
+ return deadlines.length === 0 ? undefined : Math.min(...deadlines);
25
+ }
26
+ async function settle(stream) {
27
+ // A failed attempt must fully release its transport before a replacement
28
+ // starts, or two live streams can interleave provider-side effects.
29
+ const iterator = stream[Symbol.asyncIterator]();
30
+ try {
31
+ await iterator.return?.(undefined);
32
+ }
33
+ catch {
34
+ // Settling a dead stream must never mask the original failure.
35
+ }
36
+ }
37
+ /**
38
+ * Generic in-lane credential failover. Runs at most one failover attempt per
39
+ * slot per request, retries provider-scoped faults on the SAME slot up to the
40
+ * classifier's bound without blocking it, and rotates only while no committed
41
+ * output has reached the caller - afterwards the failure is rethrown with the
42
+ * `senpi:no-turn-retry:` marker so the turn is never replayed.
43
+ */
44
+ export async function* runCredentialFailover(options) {
45
+ const now = options.now ?? Date.now;
46
+ const classify = options.classify ?? classifyCredentialFailure;
47
+ const attempted = new Set();
48
+ const retriesBySlot = new Map();
49
+ let lastError;
50
+ let lastOriginal;
51
+ while (true) {
52
+ const slots = await options.listSlots();
53
+ const candidates = slots.filter((slot) => !attempted.has(slot.name) && isAvailable(slot, now()));
54
+ if (candidates.length === 0) {
55
+ const retryAt = soonestRetryAt(slots, now());
56
+ throw lastError !== undefined && lastOriginal !== undefined
57
+ ? new CredentialFailoverError(lastError.action, lastOriginal, {
58
+ suppressTurnRetry: lastError.suppressTurnRetry,
59
+ ...(retryAt === undefined ? {} : { retryAt }),
60
+ })
61
+ : new CredentialFailoverError({ kind: "fail_request" }, new Error("No credential slots available"), {
62
+ suppressTurnRetry: false,
63
+ ...(retryAt === undefined ? {} : { retryAt }),
64
+ });
65
+ }
66
+ const slot = options.select(candidates);
67
+ let committedOutput = false;
68
+ let attemptStream;
69
+ try {
70
+ attemptStream = await options.runAttempt(slot);
71
+ for await (const event of attemptStream) {
72
+ const failure = options.errorFromEvent?.(event);
73
+ if (failure !== undefined)
74
+ throw failure;
75
+ committedOutput ||= options.isCommittedOutput(event);
76
+ yield event;
77
+ }
78
+ return;
79
+ }
80
+ catch (error) {
81
+ if (attemptStream)
82
+ await settle(attemptStream);
83
+ const failureCount = (slot.failureCount ?? 0) + (retriesBySlot.get(slot.name) ?? 0);
84
+ const action = classify(error, { failureCount });
85
+ lastOriginal = error;
86
+ if (action.kind === "retry_same" && !committedOutput) {
87
+ const used = (retriesBySlot.get(slot.name) ?? 0) + 1;
88
+ retriesBySlot.set(slot.name, used);
89
+ if (used < action.maxAttempts)
90
+ continue;
91
+ throw new CredentialFailoverError(action, error, { suppressTurnRetry: false });
92
+ }
93
+ if (action.kind !== "failover") {
94
+ throw new CredentialFailoverError(action, error, { suppressTurnRetry: committedOutput });
95
+ }
96
+ await options.persistBlock(slot, action.block);
97
+ attempted.add(slot.name);
98
+ const failure = new CredentialFailoverError(action, error, { suppressTurnRetry: committedOutput });
99
+ lastError = failure;
100
+ await options.onRotate?.({ slot, block: action.block, attempt: attempted.size, committedOutput });
101
+ if (committedOutput)
102
+ throw failure;
103
+ }
104
+ }
105
+ }
106
+ //# sourceMappingURL=failover.js.map
@@ -0,0 +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"]}