@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.
- package/CHANGELOG.md +27 -0
- package/dist/bundle/chunks/anthropic-messages-LJAKRKMX.js +11 -0
- package/dist/bundle/chunks/{args-O3WS4WCO.js → args-GQTXDYS3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YLBBQXI7.js → azure-openai-responses-HDEKTPMZ.js} +1 -1
- package/dist/bundle/chunks/chatgpt-subscription.js +10 -2
- package/dist/bundle/chunks/{chunk-ED7VJGL4.js → chunk-2PGSRQBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-4BF4FLT6.js → chunk-3LDASCJN.js} +1 -1
- package/dist/bundle/chunks/{chunk-PIAY636U.js → chunk-3QPE75AR.js} +1 -1
- package/dist/bundle/chunks/{chunk-FQZZGSYH.js → chunk-4ZGQUOBV.js} +1 -1
- package/dist/bundle/chunks/{chunk-S2M5D5GS.js → chunk-5BMIET5O.js} +2 -2
- package/dist/bundle/chunks/{chunk-XLLQJALL.js → chunk-67M7HKXO.js} +1 -1
- package/dist/bundle/chunks/chunk-6ISOXWH2.js +2 -0
- package/dist/bundle/chunks/{chunk-7KLEFHSU.js → chunk-6NPK2O3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-BNIZP4OS.js → chunk-7C4W5HMU.js} +1 -1
- package/dist/bundle/chunks/chunk-7KIHKE2T.js +2 -0
- package/dist/bundle/chunks/{chunk-WCRNJYHO.js → chunk-A4KOQUHT.js} +1 -1
- package/dist/bundle/chunks/{chunk-TOOB6OLG.js → chunk-A5PZGQCP.js} +1 -1
- package/dist/bundle/chunks/{chunk-TBARMJ75.js → chunk-AVP6DDBF.js} +1 -1
- package/dist/bundle/chunks/{chunk-FLM6GZPT.js → chunk-B3GTID4P.js} +1 -1
- package/dist/bundle/chunks/{chunk-2XBKE2BZ.js → chunk-BIWFWGUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-6LDUM5CL.js → chunk-DCI4QOUX.js} +1 -1
- package/dist/bundle/chunks/{chunk-2YFPDNNP.js → chunk-DNZKKG4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-W2V7RIBF.js → chunk-E7SA75PP.js} +1 -1
- package/dist/bundle/chunks/{chunk-YWD54RHU.js → chunk-EBZ6IP4V.js} +1 -1
- package/dist/bundle/chunks/{chunk-J2OBN7TI.js → chunk-G5XUGK4B.js} +1 -1
- package/dist/bundle/chunks/{chunk-PX7FF5LE.js → chunk-GKP34H3C.js} +2 -2
- package/dist/bundle/chunks/{chunk-PP52OWBB.js → chunk-GLTOBXZY.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZNLCKKF6.js → chunk-IJGXAACA.js} +117 -35
- package/dist/bundle/chunks/{chunk-LGO25HPL.js → chunk-JCTODLUP.js} +1 -1
- package/dist/bundle/chunks/{chunk-3ESZE3DG.js → chunk-KHBCZ6TR.js} +1 -1
- package/dist/bundle/chunks/chunk-KJR3XXMI.js +2 -0
- package/dist/bundle/chunks/{chunk-FJ2UCBOA.js → chunk-KY4V3TSO.js} +2 -2
- package/dist/bundle/chunks/{chunk-4SENPCEA.js → chunk-MCXOJHN2.js} +1 -1
- package/dist/bundle/chunks/{chunk-DPBPO5OU.js → chunk-NKI7L5UM.js} +3 -3
- package/dist/bundle/chunks/{chunk-CJWSQ6CK.js → chunk-OD7RT7G7.js} +1 -1
- package/dist/bundle/chunks/{chunk-F4OHTL2K.js → chunk-OOU3BRUY.js} +1 -1
- package/dist/bundle/chunks/{chunk-LPHUMR6R.js → chunk-OT6ALXSJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-XGBRBCIQ.js → chunk-Q5CKFBQT.js} +1 -1
- package/dist/bundle/chunks/{chunk-F7GBSQRV.js → chunk-QPYDQ4QK.js} +1 -1
- package/dist/bundle/chunks/{chunk-MOOFLKLI.js → chunk-QSMT5KVJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SAZ6E74O.js → chunk-RUOTFZQR.js} +1 -1
- package/dist/bundle/chunks/{chunk-5DZD623T.js → chunk-RWF6IJQL.js} +2 -2
- package/dist/bundle/chunks/{chunk-HBCNDC53.js → chunk-S5Y7NEHB.js} +1 -1
- package/dist/bundle/chunks/{chunk-DAPBNRN5.js → chunk-SCBTASM4.js} +1 -1
- package/dist/bundle/chunks/chunk-SJWTZ3Z2.js +2 -0
- package/dist/bundle/chunks/{chunk-ISZ4EWIS.js → chunk-SODMQEHO.js} +1 -1
- package/dist/bundle/chunks/{chunk-WEGTCUO3.js → chunk-SQAQEITV.js} +1 -1
- package/dist/bundle/chunks/{chunk-UFVZTQKN.js → chunk-TQQG5L4D.js} +1 -1
- package/dist/bundle/chunks/{chunk-VPDQ6MOL.js → chunk-U6A4X3FV.js} +1 -1
- package/dist/bundle/chunks/{chunk-ACIFXWJ6.js → chunk-UHUM55TR.js} +1 -1
- package/dist/bundle/chunks/{chunk-L5OFGHDA.js → chunk-USKWGPA6.js} +1 -1
- package/dist/bundle/chunks/{chunk-7ORS33CE.js → chunk-VDDJADPV.js} +1 -1
- package/dist/bundle/chunks/{chunk-R65P6LAT.js → chunk-VIHXG53S.js} +1 -1
- package/dist/bundle/chunks/{chunk-SGS76663.js → chunk-VPVD5G32.js} +1 -1
- package/dist/bundle/chunks/chunk-WO6I5PRF.js +3 -0
- package/dist/bundle/chunks/{chunk-ETCYVVVO.js → chunk-WVEIJ437.js} +1 -1
- package/dist/bundle/chunks/{chunk-7565XQAU.js → chunk-XKFG4N3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-57MRFI6E.js → chunk-XMWPCJU6.js} +1 -1
- package/dist/bundle/chunks/{chunk-GDPWQJDQ.js → chunk-XPSXUKSD.js} +1 -1
- package/dist/bundle/chunks/{chunk-FNBEICAA.js → chunk-YI6KRJDC.js} +1 -1
- package/dist/bundle/chunks/{chunk-UURGXV5B.js → chunk-YP47G4YP.js} +1 -1
- package/dist/bundle/chunks/{chunk-6BSMXZKQ.js → chunk-YRYDCKUI.js} +1 -1
- package/dist/bundle/chunks/{chunk-QTQ3ES2S.js → chunk-YTKZSGMQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-V36CXIIB.js → chunk-YXL3PNTL.js} +1 -1
- package/dist/bundle/chunks/chunk-Z7HTHT6K.js +2 -0
- package/dist/bundle/chunks/{chunk-27XEDYQA.js → chunk-ZTPG27EJ.js} +1 -1
- package/dist/bundle/chunks/{cli-main-XM5FWY3Z.js → cli-main-RN7YB427.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-FC7QLRU2.js → cloudflare-workers-ai-system-one-N5KFNKK4.js} +1 -1
- package/dist/bundle/chunks/credential-accounts-BJX55MTT.js +2 -0
- package/dist/bundle/chunks/devin.js +10 -2
- package/dist/bundle/chunks/{dispatch-UY6LBJJE.js → dispatch-M7UAPGVY.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-7PQ4QRHJ.js → google-generative-ai-2TZSCSIS.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-DY7EO4EJ.js → google-vertex-7NJJOI7V.js} +1 -1
- package/dist/bundle/chunks/{health-write-45CX64T2.js → health-write-YUSPVH7U.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-3EDGRKQM.js → help-fast-path-DWCA77QP.js} +1 -1
- package/dist/bundle/chunks/host-command-KJXUTSQ7.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-TGG7KGQH.js → host-lifecycle-QHIJ4G2K.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +1 -1
- package/dist/bundle/chunks/{host-session-command-OJFARE5Q.js → host-session-command-IPH3WXSG.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-F2R5P34M.js → host-socket-request-XPXJV3CS.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-XUXMDYLA.js +2 -0
- package/dist/bundle/chunks/{list-tips-3D3A6GFK.js → list-tips-XJY7EL5S.js} +1 -1
- package/dist/bundle/chunks/{login-health-YOPTYRTU.js → login-health-226NO44U.js} +1 -1
- package/dist/bundle/chunks/main-P7PKMJUG.js +2 -0
- package/dist/bundle/chunks/{mistral-conversations-6F2RAIB2.js → mistral-conversations-QMHMHSUF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-N2RQ7JSZ.js → multi-session-host-ZQY4AN46.js} +1 -1
- package/dist/bundle/chunks/{native-loader-NMDTMHMA.js → native-loader-A6DNMGCK.js} +1 -1
- package/dist/bundle/chunks/openai-chatgpt.js +11 -3
- package/dist/bundle/chunks/{openai-codex-responses-YTWLG4OL.js → openai-codex-responses-GUAHKQE2.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-TIFDYHZL.js → openai-completions-UPRTFM54.js} +1 -1
- package/dist/bundle/chunks/{openai-images-OAEMY4RG.js → openai-images-IKWNS6VB.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-L35NIG5Y.js → openai-responses-AXN5NUHH.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +10 -2
- package/dist/bundle/chunks/{package-manager-cli-JPNA27E5.js → package-manager-cli-PP7L2Q5M.js} +1 -1
- package/dist/bundle/chunks/radius.js +10 -2
- package/dist/bundle/chunks/{rg-engine-JDUYBM6L.js → rg-engine-VRSXT6HG.js} +1 -1
- package/dist/bundle/chunks/{role-45Y7PQFC.js → role-L5J7ZPCF.js} +1 -1
- package/dist/bundle/chunks/rotation-stream-6INZXQFC.js +2 -0
- package/dist/bundle/chunks/rpc-mode-W4UATRAT.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-5TUR27NY.js → rpc-monitor-source-RMMUPBMS.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-TI6MELEI.js → run-attached-tui-RLXFGCEP.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-OZ5ESMIT.js → schedule-command-ZNFEJNTT.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-5YKBYJOO.js → session-control-endpoint-TWTONJWF.js} +1 -1
- package/dist/bundle/chunks/{session-picker-CMRV72VA.js → session-picker-KRUVCHOV.js} +1 -1
- package/dist/bundle/chunks/session-slot-NJOZCOHQ.js +2 -0
- package/dist/bundle/chunks/session-worker.js +218 -135
- package/dist/bundle/chunks/{state-store-Q47AYMRI.js → state-store-ZAFZ4JOM.js} +1 -1
- package/dist/bundle/chunks/{typesafe-system-one-5JYDJK6S.js → typesafe-system-one-CICIRAYN.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.js +5 -3
- package/dist/core/auth-storage.d.ts +19 -0
- package/dist/core/auth-storage.js +99 -24
- package/dist/core/auth-store-load-error.d.ts +29 -0
- package/dist/core/auth-store-load-error.js +74 -0
- package/dist/core/brand.d.ts +2 -0
- package/dist/core/brand.js +1 -0
- package/dist/core/credential-pool/failover.d.ts +8 -0
- package/dist/core/credential-pool/failover.js +18 -5
- package/dist/core/credential-pool/rotation-stream.d.ts +1 -1
- package/dist/core/credential-pool/rotation-stream.js +2 -7
- package/dist/core/credential-pool/session-slot.d.ts +20 -0
- package/dist/core/credential-pool/session-slot.js +26 -0
- package/dist/core/credential-recovery-revision.d.ts +10 -0
- package/dist/core/credential-recovery-revision.js +19 -0
- package/dist/core/execution/executor-body.d.ts +22 -0
- package/dist/core/execution/executor-body.js +73 -0
- package/dist/core/execution/executor-freeze.d.ts +17 -0
- package/dist/core/execution/executor-freeze.js +53 -0
- package/dist/core/execution/executor-pipeline.d.ts +9 -0
- package/dist/core/execution/executor-pipeline.js +25 -0
- package/dist/core/execution/executor-types.d.ts +82 -0
- package/dist/core/execution/executor-types.js +2 -0
- package/dist/core/execution/executor.d.ts +8 -0
- package/dist/core/execution/executor.js +216 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.d.ts +15 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-block-log.js +18 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +9 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +7 -1
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +19 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +25 -1
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.d.ts +9 -0
- package/dist/core/extensions/builtin/anthropic-subscription/provider-diagnostic.js +59 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-registry-wiring.js +7 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +5 -3
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -0
- package/dist/core/extensions/builtin/btw/index.js +3 -1
- package/dist/core/extensions/builtin/cache-keepalive/index.js +3 -1
- package/dist/core/extensions/builtin/compaction/index.js +1 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.d.ts +3 -1
- package/dist/core/extensions/builtin/compaction/openai-remote.js +1 -1
- package/dist/core/extensions/builtin/compaction/speculative.js +3 -2
- package/dist/core/extensions/builtin/cursor-cli-oauth/account-command.js +3 -1
- package/dist/core/extensions/builtin/cursor-cli-oauth/native-bootstrap.js +3 -1
- package/dist/core/extensions/builtin/look-at/runner.js +3 -1
- package/dist/core/extensions/builtin/mcp/auth/callback.js +3 -4
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +2 -1
- package/dist/core/model-registry.d.ts +10 -2
- package/dist/core/model-registry.js +12 -4
- package/dist/core/model-runtime.d.ts +8 -0
- package/dist/core/model-runtime.js +38 -0
- package/dist/core/retry-fallback/controller-types.d.ts +7 -0
- package/dist/core/retry-fallback/controller.d.ts +2 -0
- package/dist/core/retry-fallback/controller.js +13 -4
- package/dist/core/retry-fallback/credential-recovery.d.ts +18 -0
- package/dist/core/retry-fallback/credential-recovery.js +32 -0
- package/dist/core/retry-fallback/settings.d.ts +8 -1
- package/dist/core/retry-fallback/settings.js +12 -6
- package/dist/core/session-log.js +1 -1
- package/dist/core/settings-manager.js +4 -1
- package/dist/modes/rpc/connection-handler.js +3 -1
- package/dist/modes/rpc/effect-commands.d.ts +1 -0
- package/dist/modes/rpc/loopless-execute-grant.d.ts +20 -0
- package/dist/modes/rpc/loopless-execute-grant.js +26 -0
- package/dist/modes/rpc/loopless-runtime-settings.d.ts +30 -0
- package/dist/modes/rpc/loopless-runtime-settings.js +69 -0
- package/dist/modes/rpc/loopless-tool-call.d.ts +12 -0
- package/dist/modes/rpc/loopless-tool-call.js +22 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state-types.d.ts +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/modes/rpc/runtime-factory-loopless.d.ts +57 -0
- package/dist/modes/rpc/runtime-factory-loopless.js +131 -0
- package/docs/development.md +1 -1
- package/docs/rpc.md +2 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/anthropic-messages-AFWQB3KN.js +0 -10
- package/dist/bundle/chunks/chunk-3WJ2DE2G.js +0 -3
- package/dist/bundle/chunks/chunk-ESQBKTNG.js +0 -2
- package/dist/bundle/chunks/chunk-MOH5HOHX.js +0 -2
- package/dist/bundle/chunks/credential-accounts-YZYSKA7X.js +0 -2
- package/dist/bundle/chunks/host-command-PZ4VYQIW.js +0 -2
- package/dist/bundle/chunks/interactive-mode-7JSJVVIS.js +0 -2
- package/dist/bundle/chunks/main-3YGJ2O4I.js +0 -2
- package/dist/bundle/chunks/rotation-stream-WXWJKJZN.js +0 -2
- 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(
|
|
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(
|
|
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 (
|
|
476
|
-
|
|
477
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
676
|
+
this.writeLocked((content) => {
|
|
624
677
|
const currentData = this.parseStorageData(content);
|
|
625
|
-
const
|
|
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.
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
this.
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
package/dist/core/brand.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/brand.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
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
|