@code-yeongyu/senpi 2026.11.3-1 → 2026.11.3-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/bundle/chunks/anthropic-messages-LJAKRKMX.js +11 -0
  3. package/dist/bundle/chunks/{args-O3WS4WCO.js → args-GQTXDYS3.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-YLBBQXI7.js → azure-openai-responses-HDEKTPMZ.js} +1 -1
  5. package/dist/bundle/chunks/chatgpt-subscription.js +10 -2
  6. package/dist/bundle/chunks/{chunk-ED7VJGL4.js → chunk-2PGSRQBF.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-4BF4FLT6.js → chunk-3LDASCJN.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-PIAY636U.js → chunk-3QPE75AR.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-FQZZGSYH.js → chunk-4ZGQUOBV.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-S2M5D5GS.js → chunk-5BMIET5O.js} +2 -2
  11. package/dist/bundle/chunks/{chunk-XLLQJALL.js → chunk-67M7HKXO.js} +1 -1
  12. package/dist/bundle/chunks/chunk-6ISOXWH2.js +2 -0
  13. package/dist/bundle/chunks/{chunk-7KLEFHSU.js → chunk-6NPK2O3C.js} +2 -2
  14. package/dist/bundle/chunks/{chunk-BNIZP4OS.js → chunk-7C4W5HMU.js} +1 -1
  15. package/dist/bundle/chunks/chunk-7KIHKE2T.js +2 -0
  16. package/dist/bundle/chunks/{chunk-WCRNJYHO.js → chunk-A4KOQUHT.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-TOOB6OLG.js → chunk-A5PZGQCP.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-TBARMJ75.js → chunk-AVP6DDBF.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-FLM6GZPT.js → chunk-B3GTID4P.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-2XBKE2BZ.js → chunk-BIWFWGUA.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-6LDUM5CL.js → chunk-DCI4QOUX.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-2YFPDNNP.js → chunk-DNZKKG4V.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-W2V7RIBF.js → chunk-E7SA75PP.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-YWD54RHU.js → chunk-EBZ6IP4V.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-J2OBN7TI.js → chunk-G5XUGK4B.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-PX7FF5LE.js → chunk-GKP34H3C.js} +2 -2
  27. package/dist/bundle/chunks/{chunk-PP52OWBB.js → chunk-GLTOBXZY.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-ZNLCKKF6.js → chunk-IJGXAACA.js} +117 -35
  29. package/dist/bundle/chunks/{chunk-LGO25HPL.js → chunk-JCTODLUP.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-3ESZE3DG.js → chunk-KHBCZ6TR.js} +1 -1
  31. package/dist/bundle/chunks/chunk-KJR3XXMI.js +2 -0
  32. package/dist/bundle/chunks/{chunk-FJ2UCBOA.js → chunk-KY4V3TSO.js} +2 -2
  33. package/dist/bundle/chunks/{chunk-4SENPCEA.js → chunk-MCXOJHN2.js} +1 -1
  34. package/dist/bundle/chunks/{chunk-DPBPO5OU.js → chunk-NKI7L5UM.js} +3 -3
  35. package/dist/bundle/chunks/{chunk-CJWSQ6CK.js → chunk-OD7RT7G7.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-F4OHTL2K.js → chunk-OOU3BRUY.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-LPHUMR6R.js → chunk-OT6ALXSJ.js} +1 -1
  38. package/dist/bundle/chunks/{chunk-XGBRBCIQ.js → chunk-Q5CKFBQT.js} +1 -1
  39. package/dist/bundle/chunks/{chunk-F7GBSQRV.js → chunk-QPYDQ4QK.js} +1 -1
  40. package/dist/bundle/chunks/{chunk-MOOFLKLI.js → chunk-QSMT5KVJ.js} +1 -1
  41. package/dist/bundle/chunks/{chunk-SAZ6E74O.js → chunk-RUOTFZQR.js} +1 -1
  42. package/dist/bundle/chunks/{chunk-5DZD623T.js → chunk-RWF6IJQL.js} +2 -2
  43. package/dist/bundle/chunks/{chunk-HBCNDC53.js → chunk-S5Y7NEHB.js} +1 -1
  44. package/dist/bundle/chunks/{chunk-DAPBNRN5.js → chunk-SCBTASM4.js} +1 -1
  45. package/dist/bundle/chunks/chunk-SJWTZ3Z2.js +2 -0
  46. package/dist/bundle/chunks/{chunk-ISZ4EWIS.js → chunk-SODMQEHO.js} +1 -1
  47. package/dist/bundle/chunks/{chunk-WEGTCUO3.js → chunk-SQAQEITV.js} +1 -1
  48. package/dist/bundle/chunks/{chunk-UFVZTQKN.js → chunk-TQQG5L4D.js} +1 -1
  49. package/dist/bundle/chunks/{chunk-VPDQ6MOL.js → chunk-U6A4X3FV.js} +1 -1
  50. package/dist/bundle/chunks/{chunk-ACIFXWJ6.js → chunk-UHUM55TR.js} +1 -1
  51. package/dist/bundle/chunks/{chunk-L5OFGHDA.js → chunk-USKWGPA6.js} +1 -1
  52. package/dist/bundle/chunks/{chunk-7ORS33CE.js → chunk-VDDJADPV.js} +1 -1
  53. package/dist/bundle/chunks/{chunk-R65P6LAT.js → chunk-VIHXG53S.js} +1 -1
  54. package/dist/bundle/chunks/{chunk-SGS76663.js → chunk-VPVD5G32.js} +1 -1
  55. package/dist/bundle/chunks/chunk-WO6I5PRF.js +3 -0
  56. package/dist/bundle/chunks/{chunk-ETCYVVVO.js → chunk-WVEIJ437.js} +1 -1
  57. package/dist/bundle/chunks/{chunk-7565XQAU.js → chunk-XKFG4N3Z.js} +1 -1
  58. package/dist/bundle/chunks/{chunk-57MRFI6E.js → chunk-XMWPCJU6.js} +1 -1
  59. package/dist/bundle/chunks/{chunk-GDPWQJDQ.js → chunk-XPSXUKSD.js} +1 -1
  60. package/dist/bundle/chunks/{chunk-FNBEICAA.js → chunk-YI6KRJDC.js} +1 -1
  61. package/dist/bundle/chunks/{chunk-UURGXV5B.js → chunk-YP47G4YP.js} +1 -1
  62. package/dist/bundle/chunks/{chunk-6BSMXZKQ.js → chunk-YRYDCKUI.js} +1 -1
  63. package/dist/bundle/chunks/{chunk-QTQ3ES2S.js → chunk-YTKZSGMQ.js} +1 -1
  64. package/dist/bundle/chunks/{chunk-V36CXIIB.js → chunk-YXL3PNTL.js} +1 -1
  65. package/dist/bundle/chunks/chunk-Z7HTHT6K.js +2 -0
  66. package/dist/bundle/chunks/{chunk-27XEDYQA.js → chunk-ZTPG27EJ.js} +1 -1
  67. package/dist/bundle/chunks/{cli-main-XM5FWY3Z.js → cli-main-RN7YB427.js} +1 -1
  68. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-FC7QLRU2.js → cloudflare-workers-ai-system-one-N5KFNKK4.js} +1 -1
  69. package/dist/bundle/chunks/credential-accounts-BJX55MTT.js +2 -0
  70. package/dist/bundle/chunks/devin.js +10 -2
  71. package/dist/bundle/chunks/{dispatch-UY6LBJJE.js → dispatch-M7UAPGVY.js} +1 -1
  72. package/dist/bundle/chunks/{google-generative-ai-7PQ4QRHJ.js → google-generative-ai-2TZSCSIS.js} +1 -1
  73. package/dist/bundle/chunks/{google-vertex-DY7EO4EJ.js → google-vertex-7NJJOI7V.js} +1 -1
  74. package/dist/bundle/chunks/{health-write-45CX64T2.js → health-write-YUSPVH7U.js} +1 -1
  75. package/dist/bundle/chunks/{help-fast-path-3EDGRKQM.js → help-fast-path-DWCA77QP.js} +1 -1
  76. package/dist/bundle/chunks/host-command-KJXUTSQ7.js +2 -0
  77. package/dist/bundle/chunks/{host-lifecycle-TGG7KGQH.js → host-lifecycle-QHIJ4G2K.js} +1 -1
  78. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  79. package/dist/bundle/chunks/{host-session-command-OJFARE5Q.js → host-session-command-IPH3WXSG.js} +1 -1
  80. package/dist/bundle/chunks/{host-socket-request-F2R5P34M.js → host-socket-request-XPXJV3CS.js} +1 -1
  81. package/dist/bundle/chunks/interactive-mode-XUXMDYLA.js +2 -0
  82. package/dist/bundle/chunks/{list-tips-3D3A6GFK.js → list-tips-XJY7EL5S.js} +1 -1
  83. package/dist/bundle/chunks/{login-health-YOPTYRTU.js → login-health-226NO44U.js} +1 -1
  84. package/dist/bundle/chunks/main-P7PKMJUG.js +2 -0
  85. package/dist/bundle/chunks/{mistral-conversations-6F2RAIB2.js → mistral-conversations-QMHMHSUF.js} +1 -1
  86. package/dist/bundle/chunks/{multi-session-host-N2RQ7JSZ.js → multi-session-host-ZQY4AN46.js} +1 -1
  87. package/dist/bundle/chunks/{native-loader-NMDTMHMA.js → native-loader-A6DNMGCK.js} +1 -1
  88. package/dist/bundle/chunks/openai-chatgpt.js +11 -3
  89. package/dist/bundle/chunks/{openai-codex-responses-YTWLG4OL.js → openai-codex-responses-GUAHKQE2.js} +1 -1
  90. package/dist/bundle/chunks/{openai-completions-TIFDYHZL.js → openai-completions-UPRTFM54.js} +1 -1
  91. package/dist/bundle/chunks/{openai-images-OAEMY4RG.js → openai-images-IKWNS6VB.js} +1 -1
  92. package/dist/bundle/chunks/{openai-responses-L35NIG5Y.js → openai-responses-AXN5NUHH.js} +1 -1
  93. package/dist/bundle/chunks/openrouter.js +10 -2
  94. package/dist/bundle/chunks/{package-manager-cli-JPNA27E5.js → package-manager-cli-PP7L2Q5M.js} +1 -1
  95. package/dist/bundle/chunks/radius.js +10 -2
  96. package/dist/bundle/chunks/{rg-engine-JDUYBM6L.js → rg-engine-VRSXT6HG.js} +1 -1
  97. package/dist/bundle/chunks/{role-45Y7PQFC.js → role-L5J7ZPCF.js} +1 -1
  98. package/dist/bundle/chunks/rotation-stream-6INZXQFC.js +2 -0
  99. package/dist/bundle/chunks/rpc-mode-W4UATRAT.js +2 -0
  100. package/dist/bundle/chunks/{rpc-monitor-source-5TUR27NY.js → rpc-monitor-source-RMMUPBMS.js} +1 -1
  101. package/dist/bundle/chunks/{run-attached-tui-TI6MELEI.js → run-attached-tui-RLXFGCEP.js} +1 -1
  102. package/dist/bundle/chunks/{schedule-command-OZ5ESMIT.js → schedule-command-ZNFEJNTT.js} +1 -1
  103. package/dist/bundle/chunks/{session-control-endpoint-5YKBYJOO.js → session-control-endpoint-TWTONJWF.js} +1 -1
  104. package/dist/bundle/chunks/{session-picker-CMRV72VA.js → session-picker-KRUVCHOV.js} +1 -1
  105. package/dist/bundle/chunks/session-slot-NJOZCOHQ.js +2 -0
  106. package/dist/bundle/chunks/session-worker.js +218 -135
  107. package/dist/bundle/chunks/{state-store-Q47AYMRI.js → state-store-ZAFZ4JOM.js} +1 -1
  108. package/dist/bundle/chunks/{typesafe-system-one-5JYDJK6S.js → typesafe-system-one-CICIRAYN.js} +1 -1
  109. package/dist/bundle/cli.js +2 -2
  110. package/dist/bundle/host-supervisor.js +1 -1
  111. package/dist/bundle/index.js +1 -1
  112. package/dist/bundle/rpc-entry.js +1 -1
  113. package/dist/bundle/runtime-manifest.json +1 -1
  114. package/dist/core/agent-session.js +5 -3
  115. package/dist/core/auth-storage.d.ts +19 -0
  116. package/dist/core/auth-storage.js +99 -24
  117. package/dist/core/auth-store-load-error.d.ts +29 -0
  118. package/dist/core/auth-store-load-error.js +74 -0
  119. package/dist/core/brand.d.ts +2 -0
  120. package/dist/core/brand.js +1 -0
  121. package/dist/core/credential-pool/failover.d.ts +8 -0
  122. package/dist/core/credential-pool/failover.js +18 -5
  123. package/dist/core/credential-pool/rotation-stream.d.ts +1 -1
  124. package/dist/core/credential-pool/rotation-stream.js +2 -7
  125. package/dist/core/credential-pool/session-slot.d.ts +20 -0
  126. package/dist/core/credential-pool/session-slot.js +26 -0
  127. package/dist/core/credential-recovery-revision.d.ts +10 -0
  128. package/dist/core/credential-recovery-revision.js +19 -0
  129. package/dist/core/execution/executor-body.d.ts +22 -0
  130. package/dist/core/execution/executor-body.js +73 -0
  131. package/dist/core/execution/executor-freeze.d.ts +17 -0
  132. package/dist/core/execution/executor-freeze.js +53 -0
  133. package/dist/core/execution/executor-pipeline.d.ts +9 -0
  134. package/dist/core/execution/executor-pipeline.js +25 -0
  135. package/dist/core/execution/executor-types.d.ts +82 -0
  136. package/dist/core/execution/executor-types.js +2 -0
  137. package/dist/core/execution/executor.d.ts +8 -0
  138. package/dist/core/execution/executor.js +216 -0
  139. package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.d.ts +15 -0
  140. package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.js +18 -0
  141. package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +9 -1
  142. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
  143. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +7 -1
  144. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +19 -2
  145. package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -2
  146. package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +25 -1
  147. package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.d.ts +9 -0
  148. package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.js +59 -0
  149. package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +7 -0
  150. package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +5 -3
  151. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -0
  152. package/dist/core/extensions/builtin/btw/index.js +3 -1
  153. package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -1
  154. package/dist/core/extensions/builtin/compaction/index.js +1 -1
  155. package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +3 -1
  156. package/dist/core/extensions/builtin/compaction/openai-remote.js +1 -1
  157. package/dist/core/extensions/builtin/compaction/speculative.js +3 -2
  158. package/dist/core/extensions/builtin/cursor-cli-oauth/account-command.js +3 -1
  159. package/dist/core/extensions/builtin/cursor-cli-oauth/native-bootstrap.js +3 -1
  160. package/dist/core/extensions/builtin/look-at/runner.js +3 -1
  161. package/dist/core/extensions/builtin/mcp/auth/callback.js +3 -4
  162. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +2 -1
  163. package/dist/core/model-registry.d.ts +10 -2
  164. package/dist/core/model-registry.js +12 -4
  165. package/dist/core/model-runtime.d.ts +8 -0
  166. package/dist/core/model-runtime.js +38 -0
  167. package/dist/core/retry-fallback/controller-types.d.ts +7 -0
  168. package/dist/core/retry-fallback/controller.d.ts +2 -0
  169. package/dist/core/retry-fallback/controller.js +13 -4
  170. package/dist/core/retry-fallback/credential-recovery.d.ts +18 -0
  171. package/dist/core/retry-fallback/credential-recovery.js +32 -0
  172. package/dist/core/retry-fallback/settings.d.ts +8 -1
  173. package/dist/core/retry-fallback/settings.js +12 -6
  174. package/dist/core/session-log.js +1 -1
  175. package/dist/core/settings-manager.js +4 -1
  176. package/dist/modes/rpc/connection-handler.js +3 -1
  177. package/dist/modes/rpc/effect-commands.d.ts +1 -0
  178. package/dist/modes/rpc/loopless-execute-grant.d.ts +20 -0
  179. package/dist/modes/rpc/loopless-execute-grant.js +26 -0
  180. package/dist/modes/rpc/loopless-runtime-settings.d.ts +30 -0
  181. package/dist/modes/rpc/loopless-runtime-settings.js +69 -0
  182. package/dist/modes/rpc/loopless-tool-call.d.ts +12 -0
  183. package/dist/modes/rpc/loopless-tool-call.js +22 -0
  184. package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -1
  185. package/dist/modes/rpc/rpc-session-state-types.d.ts +10 -0
  186. package/dist/modes/rpc/rpc-types.d.ts +1 -1
  187. package/dist/modes/rpc/runtime-factory-loopless.d.ts +57 -0
  188. package/dist/modes/rpc/runtime-factory-loopless.js +131 -0
  189. package/docs/development.md +1 -1
  190. package/docs/rpc.md +2 -0
  191. package/package.json +6 -6
  192. package/dist/bundle/chunks/anthropic-messages-AFWQB3KN.js +0 -10
  193. package/dist/bundle/chunks/chunk-3WJ2DE2G.js +0 -3
  194. package/dist/bundle/chunks/chunk-ESQBKTNG.js +0 -2
  195. package/dist/bundle/chunks/chunk-MOH5HOHX.js +0 -2
  196. package/dist/bundle/chunks/credential-accounts-YZYSKA7X.js +0 -2
  197. package/dist/bundle/chunks/host-command-PZ4VYQIW.js +0 -2
  198. package/dist/bundle/chunks/interactive-mode-7JSJVVIS.js +0 -2
  199. package/dist/bundle/chunks/main-3YGJ2O4I.js +0 -2
  200. package/dist/bundle/chunks/rotation-stream-WXWJKJZN.js +0 -2
  201. package/dist/bundle/chunks/rpc-mode-Z7MN2ZWA.js +0 -2
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { findEnvKeys, getEnvApiKey, readByProviderId } from "@earendil-works/pi-ai";
6
6
  import { slotKind, unusedSlotKinds } from "@earendil-works/pi-ai/auth/pool/kinds";
7
+ import { stampLogin } from "@earendil-works/pi-ai/auth/pool/login-id";
7
8
  import { appendLoginSlot, listSlots, projectSlot, removeSlot, repairManagedSentinelSlots, upsertSlot, } from "@earendil-works/pi-ai/auth/pool/slots";
8
9
  import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
9
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
@@ -16,6 +17,7 @@ import { stripBom } from "../utils/text.js";
16
17
  import { signOutLegacyClaudeLogins } from "./anthropic-legacy-sign-out.js";
17
18
  import { resolveReadOnlyCredential, resolveStoredCredential, resolveStoredSlot, validateReadOnlyAuthData, } from "./auth-credential-values.js";
18
19
  import { migrateLegacyProviderKeys } from "./auth-provider-key-migration.js";
20
+ import { describeAuthStoreLoadFailure, failureAfterRead, parseAuthDocument, } from "./auth-store-load-error.js";
19
21
  import { ANTHROPIC_SUBSCRIPTION_PROVIDER_ID } from "./extensions/builtin/anthropic-subscription/provider-id.js";
20
22
  import { signInAgainMessage, signInRequiredNames } from "./extensions/builtin/anthropic-subscription/sign-in-again.js";
21
23
  import { lockWithLossHandling } from "./file-lock.js";
@@ -392,7 +394,7 @@ export class AuthStorage {
392
394
  // Mutation paths keep the stored keys as-is: the key migration (and its
393
395
  // pre-write backup) belongs to the load seam, so a write can never drop a
394
396
  // legacy entry without the backup that preserves it.
395
- return repairPoisonedPoolSlots(JSON.parse(stripBom(content))).data;
397
+ return repairPoisonedPoolSlots(parseAuthDocument(content)).data;
396
398
  }
397
399
  /**
398
400
  * Reports whether the parse healed poisoned pool slots or moved legacy
@@ -406,7 +408,7 @@ export class AuthStorage {
406
408
  if (!content) {
407
409
  return { data: {}, repaired: false, migrated: false };
408
410
  }
409
- const repaired = repairPoisonedPoolSlots(JSON.parse(stripBom(content)));
411
+ const repaired = repairPoisonedPoolSlots(parseAuthDocument(content));
410
412
  const migrated = migrateLegacyProviderKeys(repaired.data);
411
413
  const moved = signOutLegacyClaudeLogins(migrated.data);
412
414
  if (moved.skipped && !this.reportedMigrationSkips.has(moved.skipped)) {
@@ -444,19 +446,37 @@ export class AuthStorage {
444
446
  this.data = data;
445
447
  this.dataLoaded = true;
446
448
  this.dataFromBusyRead = false;
449
+ this.loadError = undefined;
447
450
  this.readState.data = data;
448
451
  this.readState.loaded = true;
449
452
  this.readState.revision = revision;
450
453
  }
454
+ /** The shared read state holds the store's current contents: this instance answers from it again. */
455
+ adoptReadState(data) {
456
+ this.data = data;
457
+ this.dataLoaded = true;
458
+ this.dataFromBusyRead = false;
459
+ this.loadError = undefined;
460
+ return data;
461
+ }
462
+ /** A sync write that parsed and saved the locked store proves it loads again. */
463
+ writeLocked(fn) {
464
+ const result = this.storage.withLock(fn);
465
+ this.loadError = undefined;
466
+ return result;
467
+ }
451
468
  /**
452
469
  * Reload credentials from storage.
453
470
  */
454
471
  reload() {
455
472
  let data = {};
456
473
  let revision;
474
+ const load = { parsed: false, writeBack: false };
457
475
  try {
458
476
  this.storage.withLock((current) => {
459
477
  const parsed = this.parseStorageContent(current);
478
+ load.parsed = true;
479
+ load.writeBack = parsed.repaired || parsed.migrated;
460
480
  data = parsed.data;
461
481
  // A written repair or migration invalidates the revision read before
462
482
  // it; leaving it unset makes the next reader re-read instead of
@@ -472,9 +492,17 @@ export class AuthStorage {
472
492
  this.updateReadState(data, revision);
473
493
  return "loaded";
474
494
  }
475
- catch (error) {
476
- // Preserve the last valid in-memory snapshot.
477
- this.recordError(error instanceof Error ? error : new Error(String(error)));
495
+ catch (rawError) {
496
+ const error = failureAfterRead(rawError, load);
497
+ const recorded = this.noteLoadFailure(error);
498
+ // auth.json parsed and only a later step failed (a write-back or a lost lock): its parsed
499
+ // credentials stand, as on the async path. A failure before the parse keeps the last snapshot.
500
+ if (load.parsed) {
501
+ this.updateReadState(data, revision);
502
+ this.loadError = describeAuthStoreLoadFailure(error);
503
+ }
504
+ if (!recorded)
505
+ return "failed";
478
506
  if (!(error instanceof CredentialStoreBusyError))
479
507
  return "failed";
480
508
  if (!this.dataLoaded) {
@@ -492,6 +520,30 @@ export class AuthStorage {
492
520
  isCredentialStoreBusy() {
493
521
  return this.dataFromBusyRead;
494
522
  }
523
+ /**
524
+ * Why the store could not be loaded on its last read, or undefined once any read or write of this
525
+ * instance finds it good again (senpi#4413). While set, the in-memory credentials are not the store's
526
+ * contents; a store that cannot be read or parsed also refuses every write.
527
+ */
528
+ getLoadError() {
529
+ return this.loadError;
530
+ }
531
+ /**
532
+ * Records a failed read; returns false when it repeats the load error already reported, so a store
533
+ * that stays broken is not re-recorded on every status read that retries it.
534
+ */
535
+ noteLoadFailure(error) {
536
+ const loadError = describeAuthStoreLoadFailure(error);
537
+ const repeated = loadError !== undefined &&
538
+ this.loadError?.kind === loadError.kind &&
539
+ this.loadError.reason === loadError.reason;
540
+ if (loadError)
541
+ this.loadError = loadError;
542
+ if (repeated)
543
+ return false;
544
+ this.recordError(error);
545
+ return true;
546
+ }
495
547
  /**
496
548
  * Monotonic count of reads (sync or async) that found the store locked before it ever
497
549
  * loaded and answered with the empty placeholder. A change across an operation means
@@ -524,12 +576,12 @@ export class AuthStorage {
524
576
  * `takeReplacement`, so no credential disappears without the user being told where it went.
525
577
  */
526
578
  set(provider, credential) {
527
- return this.storage.withLock((content) => {
579
+ return this.writeLocked((content) => {
528
580
  const currentData = this.parseStorageData(content);
529
581
  const replacement = this.noteReplacement(provider, currentData[provider], credential, content);
530
- const next = appendLoginSlot(currentData[provider], credential, undefined, {
582
+ const next = stampLogin(appendLoginSlot(currentData[provider], credential, undefined, {
531
583
  accountIdentity: this.oauthAuth(provider)?.accountIdentity,
532
- });
584
+ }));
533
585
  const nextData = { ...currentData, [provider]: next };
534
586
  this.data = nextData;
535
587
  return { result: replacement, next: JSON.stringify(nextData, null, 2) };
@@ -609,7 +661,7 @@ export class AuthStorage {
609
661
  return replacement;
610
662
  }
611
663
  remove(provider) {
612
- this.storage.withLock((content) => {
664
+ this.writeLocked((content) => {
613
665
  const nextData = { ...this.parseStorageData(content) };
614
666
  delete nextData[provider];
615
667
  this.data = nextData;
@@ -619,17 +671,21 @@ export class AuthStorage {
619
671
  listSlots(provider) {
620
672
  return listSlots(readByProviderId(this.data, provider));
621
673
  }
674
+ /** Adding a new slot is a sign-in and stamps a fresh `loginId`; updating an existing slot keeps it (senpi#4099). */
622
675
  setSlot(provider, slot) {
623
- this.storage.withLock((content) => {
676
+ this.writeLocked((content) => {
624
677
  const currentData = this.parseStorageData(content);
625
- const next = upsertSlot(currentData[provider], slot);
678
+ const current = currentData[provider];
679
+ const upserted = upsertSlot(current, slot);
680
+ const adds = !listSlots(current).some((existing) => existing.name === slot.name);
681
+ const next = adds ? stampLogin(upserted) : upserted;
626
682
  const nextData = { ...currentData, [provider]: next };
627
683
  this.data = nextData;
628
684
  return { result: undefined, next: JSON.stringify(nextData, null, 2) };
629
685
  });
630
686
  }
631
687
  removeSlot(provider, name) {
632
- this.storage.withLock((content) => {
688
+ this.writeLocked((content) => {
633
689
  const currentData = this.parseStorageData(content);
634
690
  const next = removeSlot(currentData[provider], name);
635
691
  const nextData = { ...currentData };
@@ -666,16 +722,34 @@ export class AuthStorage {
666
722
  return errors;
667
723
  }
668
724
  async reloadFromStorageAsync(options) {
669
- return this.storage.withLockAsync(async (content) => {
670
- const parsed = this.parseStorageContent(content);
671
- const revision = parsed.repaired || parsed.migrated || !this.authPath ? undefined : getFileContentRevision(this.authPath);
672
- this.updateReadState(parsed.data, revision);
673
- if (parsed.migrated)
674
- this.backupAuthFile(content);
675
- return parsed.repaired || parsed.migrated
676
- ? { result: parsed.data, next: JSON.stringify(parsed.data, null, 2) }
677
- : { result: parsed.data };
678
- }, options);
725
+ try {
726
+ return await this.reloadLockedAsync(options);
727
+ }
728
+ catch (error) {
729
+ if (!options?.signal?.aborted)
730
+ this.loadError = describeAuthStoreLoadFailure(error) ?? this.loadError;
731
+ throw error;
732
+ }
733
+ }
734
+ async reloadLockedAsync(options) {
735
+ const load = { parsed: false, writeBack: false };
736
+ try {
737
+ return await this.storage.withLockAsync(async (content) => {
738
+ const parsed = this.parseStorageContent(content);
739
+ load.parsed = true;
740
+ load.writeBack = parsed.repaired || parsed.migrated;
741
+ const revision = parsed.repaired || parsed.migrated || !this.authPath ? undefined : getFileContentRevision(this.authPath);
742
+ this.updateReadState(parsed.data, revision);
743
+ if (parsed.migrated)
744
+ this.backupAuthFile(content);
745
+ return parsed.repaired || parsed.migrated
746
+ ? { result: parsed.data, next: JSON.stringify(parsed.data, null, 2) }
747
+ : { result: parsed.data };
748
+ }, options);
749
+ }
750
+ catch (error) {
751
+ throw failureAfterRead(error, load);
752
+ }
679
753
  }
680
754
  async readLatestData(options) {
681
755
  options?.signal?.throwIfAborted();
@@ -692,7 +766,7 @@ export class AuthStorage {
692
766
  }
693
767
  const revision = getFileContentRevision(this.authPath);
694
768
  if (revision !== undefined && revision === this.readState.revision)
695
- return this.readState.data;
769
+ return this.adoptReadState(this.readState.data);
696
770
  if (!this.readState.reload) {
697
771
  const controller = new AbortController();
698
772
  const reload = {
@@ -715,10 +789,11 @@ export class AuthStorage {
715
789
  reload.readers++;
716
790
  try {
717
791
  try {
718
- return await raceWithAbortSignal(reload.promise, options?.signal);
792
+ return this.adoptReadState(await raceWithAbortSignal(reload.promise, options?.signal));
719
793
  }
720
794
  catch (error) {
721
795
  options?.signal?.throwIfAborted();
796
+ this.loadError = describeAuthStoreLoadFailure(error) ?? this.loadError;
722
797
  this.noteBusyFallback(error);
723
798
  return this.readState.data;
724
799
  }
@@ -0,0 +1,29 @@
1
+ export type AuthStoreLoadErrorKind = "malformed_json" | "invalid_format" | "unreadable" | "write_back_failed";
2
+ /**
3
+ * Why an existing auth.json could not be loaded (senpi#4413). `reason` is safe to show and send to
4
+ * clients: it never carries the file's path or contents.
5
+ */
6
+ export interface AuthStoreLoadError {
7
+ readonly kind: AuthStoreLoadErrorKind;
8
+ readonly reason: string;
9
+ }
10
+ export declare class AuthStoreUnreadableError extends Error {
11
+ readonly loadError: AuthStoreLoadError;
12
+ constructor(loadError: AuthStoreLoadError);
13
+ }
14
+ /** The top-level object of an auth.json document; anything else is refused rather than read as empty. */
15
+ export declare function parseAuthDocument(content: string): Record<string, unknown>;
16
+ /**
17
+ * The load error a failed store read stands for, or undefined when the failure says nothing about
18
+ * the file: a lock held by another writer, a read the caller aborted, or a lock lost after a clean read.
19
+ */
20
+ export declare function describeAuthStoreLoadFailure(error: unknown): AuthStoreLoadError | undefined;
21
+ /**
22
+ * The error a store load surfaces: once auth.json parsed, a later failure (a write-back or a lost lock)
23
+ * becomes an AuthStoreAfterReadError so it is never reported as an unreadable file. Aborts pass through.
24
+ */
25
+ export declare function failureAfterRead(error: unknown, load: {
26
+ parsed: boolean;
27
+ writeBack: boolean;
28
+ }): unknown;
29
+ //# sourceMappingURL=auth-store-load-error.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { stripBom } from "../utils/text.js";
2
+ import { CredentialStoreBusyError } from "./lockfile-policy.js";
3
+ export class AuthStoreUnreadableError extends Error {
4
+ constructor(loadError) {
5
+ super(`Your sign-in store (auth.json) could not be read: ${loadError.reason}. Nothing was changed; repair or move auth.json in the agent directory, then sign in again.`);
6
+ this.name = "AuthStoreUnreadableError";
7
+ this.loadError = loadError;
8
+ }
9
+ }
10
+ /**
11
+ * A store load that failed after auth.json was read and parsed: saving its repaired or migrated copy
12
+ * failed (`wroteBack`), or the lock was lost once the read was done. Neither means the file is unreadable.
13
+ */
14
+ class AuthStoreAfterReadError extends Error {
15
+ constructor(cause, wroteBack) {
16
+ const detail = cause instanceof Error ? cause.message : String(cause);
17
+ super(wroteBack
18
+ ? `auth.json was read, but saving its repaired or migrated copy failed: ${detail}`
19
+ : `auth.json was read, but its lock was lost before the read finished: ${detail}`, { cause });
20
+ this.name = "AuthStoreAfterReadError";
21
+ this.wroteBack = wroteBack;
22
+ }
23
+ }
24
+ /** The top-level object of an auth.json document; anything else is refused rather than read as empty. */
25
+ export function parseAuthDocument(content) {
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(stripBom(content));
29
+ }
30
+ catch {
31
+ throw new AuthStoreUnreadableError({ kind: "malformed_json", reason: "it is not valid JSON" });
32
+ }
33
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
34
+ throw new AuthStoreUnreadableError({ kind: "invalid_format", reason: "it does not hold a JSON object" });
35
+ }
36
+ return parsed;
37
+ }
38
+ /**
39
+ * The load error a failed store read stands for, or undefined when the failure says nothing about
40
+ * the file: a lock held by another writer, a read the caller aborted, or a lock lost after a clean read.
41
+ */
42
+ export function describeAuthStoreLoadFailure(error) {
43
+ if (error instanceof AuthStoreUnreadableError)
44
+ return error.loadError;
45
+ if (error instanceof CredentialStoreBusyError)
46
+ return undefined;
47
+ if (error instanceof Error && error.name === "AbortError")
48
+ return undefined;
49
+ if (error instanceof AuthStoreAfterReadError) {
50
+ if (!error.wroteBack)
51
+ return undefined;
52
+ const code = errnoCode(error.cause);
53
+ const reason = "it was read, but its repaired or migrated copy could not be saved";
54
+ return { kind: "write_back_failed", reason: code ? `${reason} (${code})` : reason };
55
+ }
56
+ const code = errnoCode(error);
57
+ return {
58
+ kind: "unreadable",
59
+ reason: code ? `the file could not be read (${code})` : "the file could not be read",
60
+ };
61
+ }
62
+ /**
63
+ * The error a store load surfaces: once auth.json parsed, a later failure (a write-back or a lost lock)
64
+ * becomes an AuthStoreAfterReadError so it is never reported as an unreadable file. Aborts pass through.
65
+ */
66
+ export function failureAfterRead(error, load) {
67
+ if (!load.parsed || (error instanceof Error && error.name === "AbortError"))
68
+ return error;
69
+ return new AuthStoreAfterReadError(error, load.writeBack);
70
+ }
71
+ function errnoCode(error) {
72
+ return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
73
+ }
74
+ //# sourceMappingURL=auth-store-load-error.js.map
@@ -46,6 +46,8 @@ export interface BrandProfile {
46
46
  readonly userAgent: string;
47
47
  /** Product token used as the provider-side originator, when the distribution overrides it. */
48
48
  readonly originator?: string;
49
+ /** Product logo for the sign-in result pages, as a base64 image data URI. */
50
+ readonly logo?: string;
49
51
  /** Update channel of the branded product; absent means the product manages updates itself. */
50
52
  readonly update?: BrandUpdateChannel;
51
53
  readonly changelog?: BrandChangelog;
@@ -91,6 +91,7 @@ export function parseBrandProfile(raw) {
91
91
  envPrefix: (readString(source, "envPrefix") || name).toUpperCase(),
92
92
  userAgent: readString(source, "userAgent") || name,
93
93
  originator: readString(source, "originator"),
94
+ logo: readString(source, "logo"),
94
95
  update: readUpdateChannel(source),
95
96
  ...(changelog ? { changelog } : {}),
96
97
  };
@@ -82,6 +82,14 @@ export declare class CredentialFailoverError extends Error {
82
82
  });
83
83
  }
84
84
  export declare function isAvailable(slot: Pick<RunSlot, "blockReason" | "blockedUntil">, now: number): boolean;
85
+ /**
86
+ * The slots a request's next attempt may pick from: the untried, unblocked ones, else the cooling
87
+ * probe. Empty means the request is refused. Shared by the stream and by auxiliary callers that
88
+ * resolve the session's account outside it (senpi#3924), so both treat an all-blocked pool alike.
89
+ */
90
+ export declare function attemptCandidates<TSlot extends RunSlot>(slots: readonly TSlot[], attempted: ReadonlySet<string>, now: number): TSlot[];
91
+ /** The refusal a request gets when {@link attemptCandidates} is empty before any attempt ran. */
92
+ export declare function noSlotsError(slots: readonly RunSlot[], now: number): CredentialFailoverError;
85
93
  /**
86
94
  * Generic in-lane credential failover. Runs at most one failover attempt per
87
95
  * slot per request, retries provider-scoped faults on the SAME slot up to the
@@ -46,6 +46,20 @@ function coolingProbe(slots, attempted) {
46
46
  const soonest = cooling.reduce((best, slot) => (best === undefined || (slot.blockedUntil ?? 0) < (best.blockedUntil ?? 0) ? slot : best), undefined);
47
47
  return soonest === undefined ? [] : [soonest];
48
48
  }
49
+ /**
50
+ * The slots a request's next attempt may pick from: the untried, unblocked ones, else the cooling
51
+ * probe. Empty means the request is refused. Shared by the stream and by auxiliary callers that
52
+ * resolve the session's account outside it (senpi#3924), so both treat an all-blocked pool alike.
53
+ */
54
+ export function attemptCandidates(slots, attempted, now) {
55
+ const available = slots.filter((slot) => !attempted.has(slot.name) && isAvailable(slot, now));
56
+ return available.length > 0 ? available : coolingProbe(slots, attempted);
57
+ }
58
+ /** The refusal a request gets when {@link attemptCandidates} is empty before any attempt ran. */
59
+ export function noSlotsError(slots, now) {
60
+ const retryAt = soonestRetryAt(slots, now);
61
+ return new CredentialFailoverError({ kind: "fail_request" }, new Error(noSlotsMessage(slots, now)), retryAt === undefined ? {} : { retryAt });
62
+ }
49
63
  /**
50
64
  * A slot's due-back time as the reader's local clock shows it, followed by the ISO instant that the goal
51
65
  * provider-retry scan (`providerRetryAt`) and turn-end liveness read from this text (senpi#4329).
@@ -134,17 +148,16 @@ export async function* runCredentialFailover(options) {
134
148
  try {
135
149
  while (true) {
136
150
  const slots = await options.listSlots();
137
- const available = slots.filter((slot) => !attempted.has(slot.name) && isAvailable(slot, now()));
138
- const candidates = available.length > 0 ? available : coolingProbe(slots, attempted);
151
+ const candidates = attemptCandidates(slots, attempted, now());
139
152
  if (candidates.length === 0) {
140
153
  if (lastTerminalEvent !== undefined) {
141
154
  yield lastTerminalEvent;
142
155
  return;
143
156
  }
157
+ if (lastError === undefined || lastOriginal === undefined)
158
+ throw noSlotsError(slots, now());
144
159
  const retryAt = soonestRetryAt(slots, now());
145
- throw lastError !== undefined && lastOriginal !== undefined
146
- ? new CredentialFailoverError(lastError.action, lastOriginal, retryAt === undefined ? {} : { retryAt })
147
- : new CredentialFailoverError({ kind: "fail_request" }, new Error(noSlotsMessage(slots, now())), retryAt === undefined ? {} : { retryAt });
160
+ throw new CredentialFailoverError(lastError.action, lastOriginal, retryAt === undefined ? {} : { retryAt });
148
161
  }
149
162
  const slot = options.select(candidates);
150
163
  unsettled = slot;
@@ -1,5 +1,5 @@
1
1
  import type { AssistantMessageEvent } from "@earendil-works/pi-ai";
2
- import { type SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
2
+ import type { SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
3
3
  import { type RotationSlot, type RotationSources } from "./rotation-slots.ts";
4
4
  export { listRotationSlots, type RotationLane, type RotationSlot, type RotationSources } from "./rotation-slots.ts";
5
5
  /** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
@@ -1,5 +1,4 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { applyAccountOrder, rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
3
2
  import { emitProviderAccountFailover, emitProviderAccountsChanged, } from "../extensions/builtin/anthropic-subscription/account-events.js";
4
3
  import { AUTH_BLOCK_REPROBE_MS } from "./auth-reprobe.js";
5
4
  import { classifyCredentialFailure } from "./classify.js";
@@ -8,6 +7,7 @@ import { STORE_BUSY, writeHealthUnlessStoreBusy } from "./health-write.js";
8
7
  import { modelBlockKey, pruneModelBlocks, withModelBlock } from "./model-scope.js";
9
8
  import { isCommittedRotationOutput, isRotationStreamStart, rotationErrorFromEvent } from "./rotation-events.js";
10
9
  import { listRotationSlots, releaseProbeLease } from "./rotation-slots.js";
10
+ import { pickRotationSlot } from "./session-slot.js";
11
11
  export { listRotationSlots } from "./rotation-slots.js";
12
12
  /** The exact hash the claude-sdk-oauth affinity oracle uses, so pools never remap. */
13
13
  export const sha256SlotHasher = (input) => createHash("sha256").update(input).digest().readBigUInt64BE(0);
@@ -79,12 +79,7 @@ export function streamWithCredentialRotation(options) {
79
79
  return runCredentialFailover({
80
80
  listSlots: () => listRotationSlots(sources, options.modelId === undefined ? {} : { modelId: options.modelId }),
81
81
  select: (candidates) => {
82
- const pinned = candidates.find((candidate) => candidate.pinned === true);
83
- if (pinned)
84
- return pinned;
85
- const ranked = useAffinity ? rendezvousOrder(affinityKey, candidates, hasher) : candidates;
86
- const ordered = applyAccountOrder(pooled?.order, ranked);
87
- const winner = ordered[0];
82
+ const winner = pickRotationSlot(candidates, { affinityKey, useAffinity, hasher, order: pooled?.order });
88
83
  if (!winner)
89
84
  throw new Error("credential rotation selected from an empty candidate set");
90
85
  return winner;
@@ -0,0 +1,20 @@
1
+ import { type SlotHasher } from "@earendil-works/pi-ai/auth/pool/select";
2
+ import type { RotationSlot } from "./rotation-slots.ts";
3
+ export type SlotPickPolicy = {
4
+ affinityKey: string;
5
+ useAffinity: boolean;
6
+ hasher: SlotHasher;
7
+ /** The pool's user account order, applied after the pin (senpi#3591). */
8
+ order?: readonly string[];
9
+ };
10
+ /** The pin when present, else the first account of the affinity order with the user's order applied. */
11
+ export declare function pickRotationSlot<TSlot extends RotationSlot>(candidates: readonly TSlot[], policy: SlotPickPolicy): TSlot | undefined;
12
+ /**
13
+ * The account a session's next streamed request starts on, for callers that send a resolved key
14
+ * outside the rotation stream (compaction, /btw, look-at, cache keepalive). It is the stream's own
15
+ * first pick: the same candidates (unblocked accounts, else the rate-limited one due back first) and
16
+ * the same order. When the stream would refuse, this throws the stream's refusal, so an auxiliary
17
+ * request never authenticates as an account the stream would not use.
18
+ */
19
+ export declare function selectSessionSlot<TSlot extends RotationSlot>(slots: readonly TSlot[], policy: SlotPickPolicy, now: number): TSlot | undefined;
20
+ //# sourceMappingURL=session-slot.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { applyAccountOrder, rendezvousOrder } from "@earendil-works/pi-ai/auth/pool/select";
2
+ import { attemptCandidates, noSlotsError } from "./failover.js";
3
+ /** The pin when present, else the first account of the affinity order with the user's order applied. */
4
+ export function pickRotationSlot(candidates, policy) {
5
+ const pinned = candidates.find((candidate) => candidate.pinned === true);
6
+ if (pinned)
7
+ return pinned;
8
+ const ranked = policy.useAffinity ? rendezvousOrder(policy.affinityKey, candidates, policy.hasher) : candidates;
9
+ return applyAccountOrder(policy.order, ranked)[0];
10
+ }
11
+ /**
12
+ * The account a session's next streamed request starts on, for callers that send a resolved key
13
+ * outside the rotation stream (compaction, /btw, look-at, cache keepalive). It is the stream's own
14
+ * first pick: the same candidates (unblocked accounts, else the rate-limited one due back first) and
15
+ * the same order. When the stream would refuse, this throws the stream's refusal, so an auxiliary
16
+ * request never authenticates as an account the stream would not use.
17
+ */
18
+ export function selectSessionSlot(slots, policy, now) {
19
+ if (slots.length === 0)
20
+ return undefined;
21
+ const candidates = attemptCandidates(slots, new Set(), now);
22
+ if (candidates.length === 0)
23
+ throw noSlotsError(slots, now);
24
+ return pickRotationSlot(candidates, policy);
25
+ }
26
+ //# sourceMappingURL=session-slot.js.map
@@ -0,0 +1,10 @@
1
+ import type { AuthStorage } from "./auth-storage.ts";
2
+ /**
3
+ * The retry-fallback recovery revision of `provider` (senpi#4099): the `loginId` the last sign-in
4
+ * stored, or "" when none did. A token refresh keeps the id or drops it to "" and never mints one, so
5
+ * only a sign-in moves it to a new non-empty value. The
6
+ * metadata read reloads another process's write without resolving key references. An unreadable
7
+ * store answers `undefined`, which never releases anything.
8
+ */
9
+ export declare function credentialRecoveryRevision(storage: AuthStorage | undefined, provider: string): Promise<string | undefined>;
10
+ //# sourceMappingURL=credential-recovery-revision.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The retry-fallback recovery revision of `provider` (senpi#4099): the `loginId` the last sign-in
3
+ * stored, or "" when none did. A token refresh keeps the id or drops it to "" and never mints one, so
4
+ * only a sign-in moves it to a new non-empty value. The
5
+ * metadata read reloads another process's write without resolving key references. An unreadable
6
+ * store answers `undefined`, which never releases anything.
7
+ */
8
+ export async function credentialRecoveryRevision(storage, provider) {
9
+ if (!storage)
10
+ return undefined;
11
+ try {
12
+ await storage.list();
13
+ return storage.get(provider)?.loginId ?? "";
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
19
+ //# sourceMappingURL=credential-recovery-revision.js.map
@@ -0,0 +1,22 @@
1
+ import type { AgentContext, AgentTool, AgentToolCall, AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
2
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
3
+ import type { PreparationPipeline } from "./executor-types.ts";
4
+ /** What release needs from preparation; it lives in memory with the executor that prepared the call. */
5
+ export interface PreparedBody {
6
+ readonly tool: AgentTool;
7
+ readonly toolCall: AgentToolCall;
8
+ readonly args: unknown;
9
+ readonly assistantMessage: AssistantMessage;
10
+ readonly context: AgentContext;
11
+ /** The model's own name when it resolved to another tool; today's result carries a correction notice for it. */
12
+ readonly requestedName?: string;
13
+ }
14
+ export interface BodyOutcome {
15
+ readonly result: AgentToolResult<unknown>;
16
+ readonly isError: boolean;
17
+ }
18
+ export declare function errorResult(message: string): AgentToolResult<unknown>;
19
+ export declare function messageOf(error: unknown): string;
20
+ export declare function runBody(body: PreparedBody, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<unknown>): Promise<BodyOutcome>;
21
+ export declare function finalizeBody(body: PreparedBody, executed: BodyOutcome, pipeline: PreparationPipeline, signal: AbortSignal | undefined): Promise<BodyOutcome>;
22
+ //# sourceMappingURL=executor-body.d.ts.map
@@ -0,0 +1,73 @@
1
+ export function errorResult(message) {
2
+ return { content: [{ type: "text", text: message }], details: {} };
3
+ }
4
+ export function messageOf(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ const ABORTED = Symbol("aborted");
8
+ // Today's body semantics (agent-loop `executePreparedToolCall`): an abort releases the wait even when the tool never
9
+ // settles, and updates stop at settlement.
10
+ export async function runBody(body, signal, onUpdate) {
11
+ const updates = [];
12
+ let accepting = true;
13
+ try {
14
+ const execution = body.tool.execute(body.toolCall.id, body.args, signal, (partial) => {
15
+ if (accepting)
16
+ updates.push(Promise.resolve(onUpdate(partial)));
17
+ });
18
+ const aborted = signal === undefined
19
+ ? undefined
20
+ : new Promise((resolve) => {
21
+ if (signal.aborted)
22
+ resolve(ABORTED);
23
+ else
24
+ signal.addEventListener("abort", () => resolve(ABORTED), { once: true });
25
+ });
26
+ const settled = aborted ? await Promise.race([execution, aborted]) : await execution;
27
+ accepting = false;
28
+ await Promise.all(updates);
29
+ if (settled === ABORTED) {
30
+ void Promise.resolve(execution).catch(() => undefined);
31
+ return { result: errorResult("Tool execution aborted"), isError: true };
32
+ }
33
+ return { result: settled, isError: settled.isError === true };
34
+ }
35
+ catch (error) {
36
+ accepting = false;
37
+ await Promise.all(updates);
38
+ return { result: errorResult(messageOf(error)), isError: true };
39
+ }
40
+ }
41
+ // Today's after-hook merge (agent-loop `finalizeExecutedToolCall`), field by field.
42
+ export async function finalizeBody(body, executed, pipeline, signal) {
43
+ const { result, isError } = executed;
44
+ try {
45
+ const after = await pipeline.afterToolCall({
46
+ assistantMessage: body.assistantMessage,
47
+ toolCall: body.toolCall,
48
+ args: body.args,
49
+ result,
50
+ isError,
51
+ context: body.context,
52
+ }, signal);
53
+ if (!after)
54
+ return executed;
55
+ const structuredContent = after.structuredContent ?? (after.content ? undefined : result.structuredContent);
56
+ const merged = {
57
+ ...result,
58
+ content: after.content ?? result.content,
59
+ details: after.details ?? result.details,
60
+ usage: after.usage ?? result.usage,
61
+ terminate: after.terminate ?? result.terminate,
62
+ };
63
+ if (structuredContent === undefined)
64
+ delete merged.structuredContent;
65
+ else
66
+ merged.structuredContent = structuredContent;
67
+ return { result: merged, isError: after.isError ?? isError };
68
+ }
69
+ catch (error) {
70
+ return { result: errorResult(messageOf(error)), isError: true };
71
+ }
72
+ }
73
+ //# sourceMappingURL=executor-body.js.map