@code-yeongyu/senpi 2026.10.16-2 → 2026.10.16-4
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 +48 -0
- package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-2NO5ZOYE.js} +6 -6
- package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
- package/dist/bundle/chunks/{chunk-4R4PRTF5.js → chunk-CJRE5LLR.js} +1 -1
- package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-GY5O7NIQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-JD4CV3GH.js} +2 -2
- package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
- package/dist/bundle/chunks/{chunk-SSEGIXRL.js → chunk-KUQ3K33L.js} +1 -1
- package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-KWC3MCLP.js} +1 -1
- package/dist/bundle/chunks/chunk-MLL7AGVM.js +3 -0
- package/dist/bundle/chunks/chunk-PAJKEG2L.js +8 -0
- package/dist/bundle/chunks/{chunk-DPRV3FR7.js → chunk-PWHAOWNH.js} +1 -1
- package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
- package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-XCRGEMY2.js} +144 -134
- package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-I6JF2GPS.js} +1 -1
- package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
- package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
- package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
- package/dist/bundle/chunks/{help-fast-path-SVCMHCYG.js → help-fast-path-APSHUDY6.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-46PTZGIB.js → host-session-command-BUJUJHDA.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-4Q3TIXOZ.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-XEZB2XVR.js} +6 -5
- package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-YWZHOZOY.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-QX7YSKDU.js → rotation-stream-HY3JPQCX.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-SLWXVXXE.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-2RHSWBQO.js → schedule-command-JRWZGEDU.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-E2ZSEWXW.js} +1 -1
- package/dist/bundle/chunks/session-picker-5IZY7E3G.js +2 -0
- package/dist/bundle/chunks/session-worker.js +266 -255
- package/dist/bundle/cli.js +2 -2
- 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-runtime.d.ts +15 -0
- package/dist/core/agent-session-runtime.js +53 -2
- package/dist/core/agent-session.d.ts +28 -0
- package/dist/core/agent-session.js +88 -3
- package/dist/core/context-files.d.ts +21 -0
- package/dist/core/context-files.js +175 -0
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
- package/dist/core/dynamic-prompt/intent-gate.js +4 -3
- package/dist/core/dynamic-prompt/policies.d.ts +4 -1
- package/dist/core/dynamic-prompt/policies.js +3 -2
- package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
- package/dist/core/dynamic-prompt/working-task.js +3 -2
- package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
- package/dist/core/dynamic-prompt/workstation.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +26 -3
- package/dist/core/extensions/builtin/anthropic-subscription/account-management.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-management.js +40 -0
- package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/accounts.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +10 -5
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +158 -17
- package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.d.ts +13 -0
- package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.js +89 -0
- package/dist/core/extensions/builtin/goal/index.js +18 -8
- package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
- package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
- package/dist/core/extensions/builtin/index.js +3 -0
- package/dist/core/extensions/builtin/permission-system/always-scope.d.ts +34 -0
- package/dist/core/extensions/builtin/permission-system/always-scope.js +77 -0
- package/dist/core/extensions/builtin/permission-system/index.js +3 -1
- package/dist/core/extensions/builtin/permission-system/parsers.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/parsers.js +24 -3
- package/dist/core/extensions/builtin/permission-system/prompt.js +5 -1
- package/dist/core/extensions/builtin/permission-system/service.js +1 -1
- package/dist/core/extensions/builtin/permission-system/types.d.ts +7 -0
- package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
- package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
- package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
- package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
- package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
- package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
- package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
- package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
- package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
- package/dist/core/extensions/types.d.ts +32 -1
- package/dist/core/package-manager.js +4 -32
- package/dist/core/project-trust.d.ts +8 -0
- package/dist/core/project-trust.js +12 -0
- package/dist/core/provider-api-key-auth.d.ts +9 -1
- package/dist/core/provider-api-key-auth.js +34 -0
- package/dist/core/provider-composer.d.ts +23 -0
- package/dist/core/resource-loader.d.ts +5 -7
- package/dist/core/resource-loader.js +11 -84
- package/dist/core/retry-fallback/circuit-probes.d.ts +1 -0
- package/dist/core/retry-fallback/circuit-probes.js +3 -0
- package/dist/core/retry-fallback/circuit.d.ts +5 -0
- package/dist/core/retry-fallback/circuit.js +27 -4
- package/dist/core/retry-fallback/controller.d.ts +9 -4
- package/dist/core/retry-fallback/controller.js +21 -4
- package/dist/core/settings-manager.d.ts +19 -0
- package/dist/core/settings-manager.js +27 -1
- package/dist/core/settings-shapes.d.ts +4 -0
- package/dist/core/skills-prompt.d.ts +18 -0
- package/dist/core/skills-prompt.js +131 -0
- package/dist/core/skills.d.ts +1 -9
- package/dist/core/skills.js +1 -59
- package/dist/core/trust-manager.d.ts +13 -4
- package/dist/core/trust-manager.js +52 -21
- package/dist/core/turn-deadline.d.ts +92 -0
- package/dist/core/turn-deadline.js +179 -0
- package/dist/main.js +23 -4
- package/dist/modes/interactive/interactive-mode.js +10 -3
- package/dist/modes/rpc/abort-grace.d.ts +9 -0
- package/dist/modes/rpc/abort-grace.js +11 -0
- package/dist/modes/rpc/client-admission-record.d.ts +1 -0
- package/dist/modes/rpc/client-admissions.d.ts +6 -3
- package/dist/modes/rpc/client-admissions.js +9 -4
- package/dist/modes/rpc/connection-handler.js +8 -3
- package/dist/modes/rpc/effect-commands.d.ts +1 -3
- package/dist/modes/rpc/effect-commands.js +4 -5
- package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
- package/dist/modes/rpc/effect-outcome-copy.js +4 -4
- package/dist/modes/rpc/effect-record.d.ts +2 -0
- package/dist/modes/rpc/effect-record.js +2 -0
- package/dist/modes/rpc/effect-status.js +1 -1
- package/dist/modes/rpc/host-handover-parked.d.ts +6 -0
- package/dist/modes/rpc/host-handover-parked.js +1 -0
- package/dist/modes/rpc/host-handover-reopen.js +11 -2
- package/dist/modes/rpc/rpc-client.d.ts +28 -2
- package/dist/modes/rpc/rpc-client.js +24 -11
- package/dist/modes/rpc/rpc-request-deadline.d.ts +12 -0
- package/dist/modes/rpc/rpc-request-deadline.js +15 -0
- package/dist/modes/rpc/session-command-router.js +7 -4
- package/dist/modes/rpc/session-registry-attach.js +6 -1
- package/dist/modes/rpc/session-registry-claims.d.ts +22 -2
- package/dist/modes/rpc/session-registry-claims.js +68 -0
- package/dist/modes/rpc/session-registry-opening.d.ts +23 -0
- package/dist/modes/rpc/session-registry-opening.js +57 -0
- package/dist/modes/rpc/session-registry.d.ts +1 -0
- package/dist/modes/rpc/session-registry.js +48 -60
- package/dist/modes/rpc/session-unload.js +4 -1
- package/docs/rpc.md +10 -3
- package/docs/settings.md +16 -1
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-6MEETY7N.js +0 -8
- package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
- package/dist/bundle/chunks/chunk-TEGEPQIA.js +0 -3
- package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
- package/dist/bundle/chunks/google-vertex-CF6AYVXL.js +0 -2
- package/dist/bundle/chunks/session-picker-6TSBGWOA.js +0 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accountDisplayName } from "@earendil-works/pi-ai/auth/pool/slots";
|
|
2
2
|
import { emitProviderAccountsChanged } from "./account-events.js";
|
|
3
3
|
import { emptyCredential, listAccounts, pinAccount, removeAccount, } from "./accounts.js";
|
|
4
|
+
import { forgetSessionBlocksFor } from "./failover.js";
|
|
4
5
|
export const ANTHROPIC_SUBSCRIPTION_PROVIDER_ID = "anthropic-subscription";
|
|
5
6
|
export function getProviderAccounts(storage, provider, env = process.env) {
|
|
6
7
|
assertManagedProvider(provider);
|
|
@@ -50,6 +51,45 @@ export async function removeProviderAccount(storage, provider, name) {
|
|
|
50
51
|
});
|
|
51
52
|
emitProviderAccountsChanged(provider);
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Lifts a usage or rate-limit block the user knows is wrong (extra usage bought, a misread reset).
|
|
56
|
+
* Auth blocks stay: only a login or the re-probe can lift those. Returns whether a block was lifted.
|
|
57
|
+
*/
|
|
58
|
+
export async function unblockProviderAccount(storage, provider, name, env = process.env) {
|
|
59
|
+
assertManagedProvider(provider);
|
|
60
|
+
let lifted = false;
|
|
61
|
+
await storage.modify(provider, async (current) => {
|
|
62
|
+
const credential = credentialFrom(current);
|
|
63
|
+
const account = listAccounts(credential, (key) => env[key]).find((candidate) => candidate.name === name);
|
|
64
|
+
if (!account)
|
|
65
|
+
throw new Error(`Provider account not found: ${name}`);
|
|
66
|
+
if (account.blockedUntil === undefined || account.blockReason === "auth_error")
|
|
67
|
+
return current;
|
|
68
|
+
lifted = true;
|
|
69
|
+
const state = credential.slotState?.[name];
|
|
70
|
+
return {
|
|
71
|
+
...credential,
|
|
72
|
+
...(state === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: {
|
|
75
|
+
slotState: {
|
|
76
|
+
...credential.slotState,
|
|
77
|
+
[name]: state.modelBlocks ? { modelBlocks: state.modelBlocks } : {},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
accounts: (credential.accounts ?? []).map((candidate) => {
|
|
81
|
+
if (candidate.name !== name)
|
|
82
|
+
return candidate;
|
|
83
|
+
const { blockedUntil: _blockedUntil, blockReason: _blockReason, blockedAt: _blockedAt, ...rest } = candidate;
|
|
84
|
+
return rest;
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const sessionScoped = forgetSessionBlocksFor(name);
|
|
89
|
+
if (lifted || sessionScoped)
|
|
90
|
+
emitProviderAccountsChanged(provider);
|
|
91
|
+
return lifted || sessionScoped;
|
|
92
|
+
}
|
|
53
93
|
function assertManagedProvider(provider) {
|
|
54
94
|
if (provider !== ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) {
|
|
55
95
|
throw new Error(`Provider account management is unavailable for: ${provider}`);
|
|
@@ -10,6 +10,8 @@ export type AccountSlot = {
|
|
|
10
10
|
source: "login" | "import" | "env";
|
|
11
11
|
blockedUntil?: number;
|
|
12
12
|
blockReason?: string;
|
|
13
|
+
/** When the current block was stored, so a turn that started earlier never clears it (senpi#3462). */
|
|
14
|
+
blockedAt?: number;
|
|
13
15
|
/** Rate limits that bind one model family on this account, not the account (senpi#2555). */
|
|
14
16
|
modelBlocks?: ModelBlocks;
|
|
15
17
|
/**
|
|
@@ -23,6 +25,7 @@ export type AccountSlot = {
|
|
|
23
25
|
export type SlotState = Record<string, {
|
|
24
26
|
blockedUntil?: number;
|
|
25
27
|
blockReason?: string;
|
|
28
|
+
blockedAt?: number;
|
|
26
29
|
modelBlocks?: ModelBlocks;
|
|
27
30
|
}>;
|
|
28
31
|
export type AnthropicSubscriptionCredential = OAuthCredential & {
|
|
@@ -57,7 +57,7 @@ export function upsertAccount(credential, slot) {
|
|
|
57
57
|
const existing = storedSlots(credential).find((candidate) => candidate.name === slot.name);
|
|
58
58
|
if (!existing)
|
|
59
59
|
return { ...credential, accounts: [...storedSlots(credential), slot] };
|
|
60
|
-
const { blockedUntil: _blockedUntil, blockReason: _blockReason, modelBlocks: _modelBlocks, authRecoveryGrant: _authRecoveryGrant, ...identity } = existing;
|
|
60
|
+
const { blockedUntil: _blockedUntil, blockReason: _blockReason, blockedAt: _blockedAt, modelBlocks: _modelBlocks, authRecoveryGrant: _authRecoveryGrant, ...identity } = existing;
|
|
61
61
|
const refreshed = {
|
|
62
62
|
...identity,
|
|
63
63
|
access: slot.access,
|
|
@@ -47,7 +47,7 @@ export function clearExpiredBlocks(accounts, now = Date.now()) {
|
|
|
47
47
|
return accounts.map((account) => {
|
|
48
48
|
let available = account;
|
|
49
49
|
if (account.blockReason !== undefined && (account.blockedUntil === undefined || account.blockedUntil <= now)) {
|
|
50
|
-
const { blockedUntil: _blockedUntil, blockReason: _blockReason, ...rest } = account;
|
|
50
|
+
const { blockedUntil: _blockedUntil, blockReason: _blockReason, blockedAt: _blockedAt, ...rest } = account;
|
|
51
51
|
available = rest;
|
|
52
52
|
}
|
|
53
53
|
if (available.modelBlocks === undefined)
|
|
@@ -228,6 +228,7 @@ export async function* queryWithAuthLane(input) {
|
|
|
228
228
|
});
|
|
229
229
|
},
|
|
230
230
|
classify: classifySdkError,
|
|
231
|
+
...(input.sessionId === undefined ? {} : { sessionKey: input.sessionId }),
|
|
231
232
|
onLongContextRefused: () => markLongContextRefused(),
|
|
232
233
|
store: pool.store,
|
|
233
234
|
providerId: ANTHROPIC_SUBSCRIPTION_PROVIDER_ID,
|
|
@@ -3,7 +3,7 @@ import type { AccountSlot } from "./accounts.ts";
|
|
|
3
3
|
import type { SdkErrorClassification } from "./errors.ts";
|
|
4
4
|
export declare const MAX_RATE_LIMIT_BLOCK_MS: number;
|
|
5
5
|
export declare const DEFAULT_RATE_LIMIT_BLOCK_MS = 60000;
|
|
6
|
-
/** Same-account retries per turn for a transient failure (overload, network, server error)
|
|
6
|
+
/** Same-account retries per turn for a transient failure (overload, network, server error); it never rotates. */
|
|
7
7
|
export declare const TRANSIENT_RETRIES_PER_TURN = 2;
|
|
8
8
|
export declare const TRANSIENT_RETRY_DELAY_MS = 1000;
|
|
9
9
|
export declare const TURN_RETRY_SUPPRESSION_PREFIX = "senpi:no-turn-retry:";
|
|
@@ -33,7 +33,11 @@ export type FailoverOptions<TEvent> = {
|
|
|
33
33
|
signal?: AbortSignal;
|
|
34
34
|
/** Records a refused 1M window so the retry, built by `runAttempt`, requests the standard one. */
|
|
35
35
|
onLongContextRefused?: () => void;
|
|
36
|
+
/** The session this turn belongs to: blocks that bind only this session are remembered under it. */
|
|
37
|
+
sessionKey?: string;
|
|
36
38
|
};
|
|
39
|
+
/** `/claude-account unblock` lifts the account's session-scoped blocks in every session too. Returns whether one was lifted. */
|
|
40
|
+
export declare function forgetSessionBlocksFor(name: string): boolean;
|
|
37
41
|
export declare class ClassifiedSdkError extends Error {
|
|
38
42
|
readonly classification: SdkErrorClassification;
|
|
39
43
|
readonly original: unknown;
|
|
@@ -41,10 +45,11 @@ export declare class ClassifiedSdkError extends Error {
|
|
|
41
45
|
constructor(classification: SdkErrorClassification, original: unknown, suppressTurnRetry: boolean);
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
44
|
-
* Runs one attempt per account, plus at most
|
|
45
|
-
* for a transient failure
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* Runs one attempt per account for account-level failures (a limit, billing, auth), plus at most
|
|
49
|
+
* TRANSIENT_RETRIES_PER_TURN same-account retries for a transient failure, after which the turn fails on
|
|
50
|
+
* that account instead of rotating (and one retry on newer stored credentials). A retry is transparent only
|
|
51
|
+
* before a text, thinking, or tool-call event reaches the caller; post-delta failures are marked so
|
|
52
|
+
* AgentSession never replays the partial turn.
|
|
48
53
|
*/
|
|
49
54
|
export declare function runFailover<TEvent>(options: FailoverOptions<TEvent>): AsyncGenerator<TEvent>;
|
|
50
55
|
//# sourceMappingURL=failover.d.ts.map
|
|
@@ -1,13 +1,95 @@
|
|
|
1
1
|
import { AUTH_BLOCK_REPROBE_MS } from "../../../credential-pool/auth-reprobe.js";
|
|
2
|
-
import { activeModelBlockUntil, mergeModelBlocks, modelBlockKey, withModelBlock, } from "../../../credential-pool/model-scope.js";
|
|
2
|
+
import { activeModelBlockUntil, mergeModelBlocks, modelBlockKey, pruneModelBlocks, withModelBlock, } from "../../../credential-pool/model-scope.js";
|
|
3
3
|
import { usageLimitResetMs } from "../../../credential-pool/reset-time.js";
|
|
4
4
|
import { clearExpiredBlocks } from "./affinity.js";
|
|
5
|
+
import { isConfirmedAccountUsageLimit, usageLimitBlockMs } from "./usage-limit-block.js";
|
|
5
6
|
export const MAX_RATE_LIMIT_BLOCK_MS = 48 * 60 * 60 * 1_000;
|
|
6
7
|
export const DEFAULT_RATE_LIMIT_BLOCK_MS = 60_000;
|
|
7
|
-
/** Same-account retries per turn for a transient failure (overload, network, server error)
|
|
8
|
+
/** Same-account retries per turn for a transient failure (overload, network, server error); it never rotates. */
|
|
8
9
|
export const TRANSIENT_RETRIES_PER_TURN = 2;
|
|
9
10
|
export const TRANSIENT_RETRY_DELAY_MS = 1_000;
|
|
10
11
|
export const TURN_RETRY_SUPPRESSION_PREFIX = "senpi:no-turn-retry:";
|
|
12
|
+
/**
|
|
13
|
+
* Blocks that bind only the session that hit them, in memory for the process: a plain rate limit and a
|
|
14
|
+
* one-model limit. Another session on the same account keeps using it and finds out on its own request.
|
|
15
|
+
*/
|
|
16
|
+
const sessionBlocks = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Only a verdict that the account itself cannot serve anyone is shared through the credential store: a
|
|
19
|
+
* confirmed account-level usage limit, a billing or organization refusal, or a rejected token. Moving
|
|
20
|
+
* every session off the account is right then, since leaving them would retry a dead account. A plain
|
|
21
|
+
* rate limit, an overload or a one-model limit is not that verdict, so it never moves other sessions
|
|
22
|
+
* (senpi#3466).
|
|
23
|
+
*/
|
|
24
|
+
function isSharedVerdict(classification, error) {
|
|
25
|
+
if (classification.kind === "auth_error" ||
|
|
26
|
+
classification.kind === "billing" ||
|
|
27
|
+
classification.kind === "org_not_allowed")
|
|
28
|
+
return true;
|
|
29
|
+
return (classification.kind === "rate_limit" &&
|
|
30
|
+
classification.modelFamily === undefined &&
|
|
31
|
+
isConfirmedAccountUsageLimit(errorText(error)));
|
|
32
|
+
}
|
|
33
|
+
function rememberSessionBlock(sessionKey, account) {
|
|
34
|
+
if (sessionKey === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const blocks = sessionBlocks.get(sessionKey) ?? new Map();
|
|
37
|
+
blocks.set(account.name, {
|
|
38
|
+
blockedUntil: account.blockedUntil,
|
|
39
|
+
blockReason: account.blockReason,
|
|
40
|
+
blockedAt: account.blockedAt,
|
|
41
|
+
modelBlocks: account.modelBlocks,
|
|
42
|
+
});
|
|
43
|
+
sessionBlocks.set(sessionKey, blocks);
|
|
44
|
+
}
|
|
45
|
+
/** Drops elapsed session blocks for every session, so sessions that ended leave nothing behind. */
|
|
46
|
+
function pruneSessionBlocks(now) {
|
|
47
|
+
for (const [sessionKey, blocks] of sessionBlocks) {
|
|
48
|
+
for (const [name, block] of blocks) {
|
|
49
|
+
if ((block.blockedUntil ?? 0) <= now && pruneModelBlocks(block.modelBlocks, now) === undefined)
|
|
50
|
+
blocks.delete(name);
|
|
51
|
+
}
|
|
52
|
+
if (blocks.size === 0)
|
|
53
|
+
sessionBlocks.delete(sessionKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function withSessionBlocks(accounts, sessionKey, now) {
|
|
57
|
+
pruneSessionBlocks(now);
|
|
58
|
+
const blocks = sessionKey === undefined ? undefined : sessionBlocks.get(sessionKey);
|
|
59
|
+
if (blocks === undefined)
|
|
60
|
+
return [...accounts];
|
|
61
|
+
return accounts.map((account) => {
|
|
62
|
+
const own = blocks.get(account.name);
|
|
63
|
+
if (own === undefined)
|
|
64
|
+
return account;
|
|
65
|
+
const later = (own.blockedUntil ?? 0) > (account.blockedUntil ?? 0);
|
|
66
|
+
return {
|
|
67
|
+
...account,
|
|
68
|
+
...(later ? { blockedUntil: own.blockedUntil, blockReason: own.blockReason, blockedAt: own.blockedAt } : {}),
|
|
69
|
+
modelBlocks: mergeModelBlocks(account.modelBlocks, own.modelBlocks, 0),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function forgetSessionBlock(sessionKey, name) {
|
|
74
|
+
if (sessionKey === undefined)
|
|
75
|
+
return;
|
|
76
|
+
const blocks = sessionBlocks.get(sessionKey);
|
|
77
|
+
if (!blocks?.delete(name))
|
|
78
|
+
return;
|
|
79
|
+
if (blocks.size === 0)
|
|
80
|
+
sessionBlocks.delete(sessionKey);
|
|
81
|
+
}
|
|
82
|
+
/** `/claude-account unblock` lifts the account's session-scoped blocks in every session too. Returns whether one was lifted. */
|
|
83
|
+
export function forgetSessionBlocksFor(name) {
|
|
84
|
+
let lifted = false;
|
|
85
|
+
for (const [sessionKey, blocks] of [...sessionBlocks]) {
|
|
86
|
+
if (!blocks.has(name))
|
|
87
|
+
continue;
|
|
88
|
+
lifted = true;
|
|
89
|
+
forgetSessionBlock(sessionKey, name);
|
|
90
|
+
}
|
|
91
|
+
return lifted;
|
|
92
|
+
}
|
|
11
93
|
export class ClassifiedSdkError extends Error {
|
|
12
94
|
constructor(classification, original, suppressTurnRetry) {
|
|
13
95
|
const detail = original instanceof Error ? original.message : String(original);
|
|
@@ -53,7 +135,7 @@ function retryAfterMs(error) {
|
|
|
53
135
|
}
|
|
54
136
|
function blockedAccount(account, classification, now, attempt, baseBlockMs, error, model) {
|
|
55
137
|
if (classification.kind === "auth_error") {
|
|
56
|
-
return { ...account, blockedUntil: now + AUTH_BLOCK_REPROBE_MS, blockReason: "auth_error" };
|
|
138
|
+
return { ...account, blockedUntil: now + AUTH_BLOCK_REPROBE_MS, blockReason: "auth_error", blockedAt: now };
|
|
57
139
|
}
|
|
58
140
|
const fallback = Math.min(MAX_RATE_LIMIT_BLOCK_MS, baseBlockMs * 2 ** attempt);
|
|
59
141
|
if (classification.modelFamily !== undefined && model !== undefined) {
|
|
@@ -64,8 +146,45 @@ function blockedAccount(account, classification, now, attempt, baseBlockMs, erro
|
|
|
64
146
|
const key = modelBlockKey(classification.modelFamily, model);
|
|
65
147
|
return { ...account, modelBlocks: withModelBlock(account.modelBlocks, key, now + duration, now) };
|
|
66
148
|
}
|
|
67
|
-
|
|
68
|
-
|
|
149
|
+
// A spent account is out until its limit resets, capped at the limit's own window; a fixed
|
|
150
|
+
// short block sent a pinned account back to the same limit every minute (senpi#3462).
|
|
151
|
+
const hinted = retryAfterMs(error) ?? usageLimitBlockMs(errorText(error), account.name, now);
|
|
152
|
+
const duration = Math.min(MAX_RATE_LIMIT_BLOCK_MS, hinted ?? fallback);
|
|
153
|
+
return { ...account, blockedUntil: now + duration, blockReason: classification.kind, blockedAt: now };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* An account that just served a turn is not spent: a limit block stored before the turn started (a
|
|
157
|
+
* misread reset, a stale failure) is cleared so it cannot hold a working account out. A block stored
|
|
158
|
+
* after the turn started is newer evidence than this success and stays. Auth blocks keep their re-probe.
|
|
159
|
+
*/
|
|
160
|
+
async function clearBlockAfterSuccess(store, providerId, name, startedAt) {
|
|
161
|
+
const clearable = (slot) => slot?.blockedUntil !== undefined && slot.blockReason !== "auth_error" && (slot.blockedAt ?? 0) <= startedAt;
|
|
162
|
+
const stored = await store.read(providerId);
|
|
163
|
+
if (stored?.type !== "oauth")
|
|
164
|
+
return;
|
|
165
|
+
const read = stored;
|
|
166
|
+
const slot = read.accounts?.find((account) => account.name === name) ?? read.slotState?.[name];
|
|
167
|
+
if (!clearable(slot))
|
|
168
|
+
return;
|
|
169
|
+
await store.modify(providerId, async (current) => {
|
|
170
|
+
if (current?.type !== "oauth")
|
|
171
|
+
return current;
|
|
172
|
+
const credential = current;
|
|
173
|
+
const state = credential.slotState?.[name];
|
|
174
|
+
const slotState = state === undefined || !clearable(state)
|
|
175
|
+
? credential.slotState
|
|
176
|
+
: { ...credential.slotState, [name]: { ...(state.modelBlocks ? { modelBlocks: state.modelBlocks } : {}) } };
|
|
177
|
+
return {
|
|
178
|
+
...credential,
|
|
179
|
+
...(slotState === undefined ? {} : { slotState }),
|
|
180
|
+
accounts: (credential.accounts ?? []).map((account) => {
|
|
181
|
+
if (account.name !== name || !clearable(account))
|
|
182
|
+
return account;
|
|
183
|
+
const { blockedUntil: _blockedUntil, blockReason: _blockReason, blockedAt: _blockedAt, ...rest } = account;
|
|
184
|
+
return rest;
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
69
188
|
}
|
|
70
189
|
function isTransient(classification) {
|
|
71
190
|
return classification.kind === "overloaded" || classification.kind === "other";
|
|
@@ -111,6 +230,7 @@ async function persistBlock(store, providerId, account, now) {
|
|
|
111
230
|
[account.name]: {
|
|
112
231
|
blockedUntil: account.blockedUntil,
|
|
113
232
|
blockReason: account.blockReason,
|
|
233
|
+
...(account.blockedAt === undefined ? {} : { blockedAt: account.blockedAt }),
|
|
114
234
|
// Union with what a concurrent request stored meanwhile, later expiry winning.
|
|
115
235
|
modelBlocks: mergeModelBlocks(credential.slotState?.[account.name]?.modelBlocks, account.modelBlocks, now),
|
|
116
236
|
},
|
|
@@ -129,6 +249,7 @@ async function persistBlock(store, providerId, account, now) {
|
|
|
129
249
|
...existing,
|
|
130
250
|
blockedUntil: account.blockedUntil,
|
|
131
251
|
blockReason: account.blockReason,
|
|
252
|
+
...(account.blockedAt === undefined ? {} : { blockedAt: account.blockedAt }),
|
|
132
253
|
modelBlocks: mergeModelBlocks(existing.modelBlocks, account.modelBlocks, now),
|
|
133
254
|
}
|
|
134
255
|
: existing);
|
|
@@ -142,15 +263,16 @@ function usable(account, now, model) {
|
|
|
142
263
|
activeModelBlockUntil(account.modelBlocks, model, now) === undefined);
|
|
143
264
|
}
|
|
144
265
|
/**
|
|
145
|
-
* Runs one attempt per account, plus at most
|
|
146
|
-
* for a transient failure
|
|
147
|
-
*
|
|
148
|
-
*
|
|
266
|
+
* Runs one attempt per account for account-level failures (a limit, billing, auth), plus at most
|
|
267
|
+
* TRANSIENT_RETRIES_PER_TURN same-account retries for a transient failure, after which the turn fails on
|
|
268
|
+
* that account instead of rotating (and one retry on newer stored credentials). A retry is transparent only
|
|
269
|
+
* before a text, thinking, or tool-call event reaches the caller; post-delta failures are marked so
|
|
270
|
+
* AgentSession never replays the partial turn.
|
|
149
271
|
*/
|
|
150
272
|
export async function* runFailover(options) {
|
|
151
273
|
const now = options.now ?? Date.now;
|
|
152
274
|
const baseBlockMs = options.baseBlockMs ?? DEFAULT_RATE_LIMIT_BLOCK_MS;
|
|
153
|
-
let accounts = clearExpiredBlocks(options.accounts, now());
|
|
275
|
+
let accounts = clearExpiredBlocks(withSessionBlocks(options.accounts, options.sessionKey, now()), now());
|
|
154
276
|
let lastError;
|
|
155
277
|
const retriedOnStoredMaterial = new Set();
|
|
156
278
|
let transientRetriesUsed = 0;
|
|
@@ -163,6 +285,8 @@ export async function* runFailover(options) {
|
|
|
163
285
|
retryAccountName = undefined;
|
|
164
286
|
const account = { ...(retrySlot ?? options.selectFn(accounts)) };
|
|
165
287
|
let visibleDeltaEmitted = false;
|
|
288
|
+
let completed = false;
|
|
289
|
+
const startedAt = now();
|
|
166
290
|
try {
|
|
167
291
|
const attemptStream = await options.runAttempt(account);
|
|
168
292
|
for await (const event of attemptStream) {
|
|
@@ -172,7 +296,7 @@ export async function* runFailover(options) {
|
|
|
172
296
|
visibleDeltaEmitted ||= (options.isVisibleDelta ?? defaultIsVisibleDelta)(event);
|
|
173
297
|
yield event;
|
|
174
298
|
}
|
|
175
|
-
|
|
299
|
+
completed = true;
|
|
176
300
|
}
|
|
177
301
|
catch (error) {
|
|
178
302
|
const classification = options.classify(error);
|
|
@@ -191,11 +315,14 @@ export async function* runFailover(options) {
|
|
|
191
315
|
attempt--;
|
|
192
316
|
continue;
|
|
193
317
|
}
|
|
194
|
-
// A transient failure says nothing about the account
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
318
|
+
// A transient failure says nothing about the account: it is retried in place a bounded
|
|
319
|
+
// number of times, then the turn fails on this account and the session's own retry and
|
|
320
|
+
// model fallback take over. It never blocks or rotates the account: moving to another
|
|
321
|
+
// account writes the whole conversation to a cold prompt cache, and a persisted block would
|
|
322
|
+
// move every other session off a healthy account too (senpi#2891, senpi#3466).
|
|
323
|
+
if (isTransient(classification)) {
|
|
324
|
+
if (visibleDeltaEmitted || transientRetriesUsed >= TRANSIENT_RETRIES_PER_TURN)
|
|
325
|
+
throw classified;
|
|
199
326
|
const delayMs = TRANSIENT_RETRY_DELAY_MS * 2 ** transientRetriesUsed;
|
|
200
327
|
transientRetriesUsed += 1;
|
|
201
328
|
try {
|
|
@@ -209,7 +336,12 @@ export async function* runFailover(options) {
|
|
|
209
336
|
continue;
|
|
210
337
|
}
|
|
211
338
|
const blocked = blockedAccount(account, classification, now(), attempt, baseBlockMs, error, options.model);
|
|
212
|
-
|
|
339
|
+
// With no session to hold it, a block is shared as before: dropping it would re-send every
|
|
340
|
+
// sessionless request to the spent account until its window resets.
|
|
341
|
+
const shared = isSharedVerdict(classification, error) || options.sessionKey === undefined;
|
|
342
|
+
if (!shared)
|
|
343
|
+
rememberSessionBlock(options.sessionKey, blocked);
|
|
344
|
+
const superseded = shared ? await persistBlock(options.store, options.providerId, blocked, now()) : undefined;
|
|
213
345
|
if (superseded &&
|
|
214
346
|
!visibleDeltaEmitted &&
|
|
215
347
|
usable(superseded, now(), options.model) &&
|
|
@@ -239,6 +371,15 @@ export async function* runFailover(options) {
|
|
|
239
371
|
if (visibleDeltaEmitted)
|
|
240
372
|
throw classified;
|
|
241
373
|
}
|
|
374
|
+
if (completed) {
|
|
375
|
+
forgetSessionBlock(options.sessionKey, account.name);
|
|
376
|
+
// Outside the attempt's try: clearing is best-effort hygiene after a finished turn, so a store
|
|
377
|
+
// failure is logged and the block stays for the next success; it never fails or replays the turn.
|
|
378
|
+
await clearBlockAfterSuccess(options.store, options.providerId, account.name, startedAt).catch((error) => {
|
|
379
|
+
process.stderr.write(`senpi anthropic-subscription: could not clear a stored block on ${account.name}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
242
383
|
}
|
|
243
384
|
throw lastError ?? new Error("Anthropic Subscription failover exhausted without an attempt");
|
|
244
385
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's own verdict that the account's subscription is spent: its anchored usage-limit prose
|
|
3
|
+
* or its structured terminal reason, under no status other than 429 (senpi#3462, senpi#3466).
|
|
4
|
+
*/
|
|
5
|
+
export declare function isConfirmedAccountUsageLimit(text: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* How long a spent account stays blocked: until the reset its limit names, or the reset its spent
|
|
8
|
+
* window reported, never past the window the limit belongs to. Undefined when the error is not
|
|
9
|
+
* Claude Code's account usage limit or names no reset, so the caller keeps its short default
|
|
10
|
+
* (senpi#3462).
|
|
11
|
+
*/
|
|
12
|
+
export declare function usageLimitBlockMs(text: string, accountName: string, now: number): number | undefined;
|
|
13
|
+
//# sourceMappingURL=usage-limit-block.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { usageLimitResetMs } from "../../../credential-pool/reset-time.js";
|
|
2
|
+
import { subscriptionLimits } from "../../../subscription-limits/limit-store.js";
|
|
3
|
+
// The subscription-limit store keys accounts by this provider id (auth-attempt.ts).
|
|
4
|
+
const PROVIDER = "anthropic-subscription";
|
|
5
|
+
const HOUR_MS = 60 * 60 * 1_000;
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code's own account usage-limit errors, anchored at the start of the message: "You've hit
|
|
8
|
+
* your session limit · resets 3pm", "You've reached your 5-hour limit", "Claude AI usage limit
|
|
9
|
+
* reached". Only a window word Claude Code uses may stand before "limit", so a context or rate
|
|
10
|
+
* limit, a warning ("approaching your usage limit") or ordinary prose never takes a long block.
|
|
11
|
+
*/
|
|
12
|
+
const CLAUDE_USAGE_LIMIT = /^(?:You['\u2019]ve|You have) (?:hit|reached) your (?:(session|weekly|daily|monthly|\d+-hour|usage) )?limit(?=$|\s*[\u00b7\u2219.,{(]|\s+resets?\b)|^Claude AI usage limit reached\b/i;
|
|
13
|
+
/**
|
|
14
|
+
* Claude Code's structured terminal reason for a spent subscription. `rapid_refill_breaker` is a short burst
|
|
15
|
+
* breaker, not a spent account, so it keeps the short, session-scoped block (senpi#3466).
|
|
16
|
+
*/
|
|
17
|
+
const TERMINAL_LIMIT_REASON = /\bblocking_limit\b/;
|
|
18
|
+
/** A status the error names; a usage limit is a 429, so any other status is not one. */
|
|
19
|
+
const NAMED_STATUS = /\b(?:HTTP|status(?: code)?)\s*(\d{3})\b/i;
|
|
20
|
+
/** The longest a block may last for each window word: a reset read past it is a misread. */
|
|
21
|
+
const WINDOW_MS = {
|
|
22
|
+
session: 5 * HOUR_MS,
|
|
23
|
+
daily: 24 * HOUR_MS,
|
|
24
|
+
weekly: 7 * 24 * HOUR_MS,
|
|
25
|
+
monthly: 31 * 24 * HOUR_MS,
|
|
26
|
+
};
|
|
27
|
+
/** A limit that names no window is capped at the shortest subscription window. */
|
|
28
|
+
const UNNAMED_WINDOW_MS = 5 * HOUR_MS;
|
|
29
|
+
function namedWindowMs(word) {
|
|
30
|
+
if (word === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const lower = word.toLowerCase();
|
|
33
|
+
const hours = /^(\d+)-hour$/.exec(lower);
|
|
34
|
+
if (hours)
|
|
35
|
+
return Number(hours[1]) * HOUR_MS;
|
|
36
|
+
return WINDOW_MS[lower];
|
|
37
|
+
}
|
|
38
|
+
/** The latest-resetting fully used window the SDK last reported for this account. */
|
|
39
|
+
function spentWindow(accountName, now) {
|
|
40
|
+
const limits = subscriptionLimits
|
|
41
|
+
.snapshot()
|
|
42
|
+
.find((entry) => entry.provider === PROVIDER && entry.account === accountName);
|
|
43
|
+
let spent;
|
|
44
|
+
for (const window of limits?.windows ?? []) {
|
|
45
|
+
if (window.usedPercent < 100 || window.resetsAt === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
const resetsAt = Date.parse(window.resetsAt);
|
|
48
|
+
if (!Number.isFinite(resetsAt) || resetsAt <= now)
|
|
49
|
+
continue;
|
|
50
|
+
if (spent === undefined || resetsAt - now > spent.resetMs) {
|
|
51
|
+
spent = {
|
|
52
|
+
resetMs: resetsAt - now,
|
|
53
|
+
windowMs: window.windowMinutes === undefined ? undefined : window.windowMinutes * 60_000,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return spent;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Claude Code's own verdict that the account's subscription is spent: its anchored usage-limit prose
|
|
61
|
+
* or its structured terminal reason, under no status other than 429 (senpi#3462, senpi#3466).
|
|
62
|
+
*/
|
|
63
|
+
export function isConfirmedAccountUsageLimit(text) {
|
|
64
|
+
const trimmed = text.trim();
|
|
65
|
+
if (!CLAUDE_USAGE_LIMIT.test(trimmed) && !TERMINAL_LIMIT_REASON.test(trimmed))
|
|
66
|
+
return false;
|
|
67
|
+
const status = NAMED_STATUS.exec(trimmed)?.[1];
|
|
68
|
+
return status === undefined || status === "429";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* How long a spent account stays blocked: until the reset its limit names, or the reset its spent
|
|
72
|
+
* window reported, never past the window the limit belongs to. Undefined when the error is not
|
|
73
|
+
* Claude Code's account usage limit or names no reset, so the caller keeps its short default
|
|
74
|
+
* (senpi#3462).
|
|
75
|
+
*/
|
|
76
|
+
export function usageLimitBlockMs(text, accountName, now) {
|
|
77
|
+
const trimmed = text.trim();
|
|
78
|
+
if (!isConfirmedAccountUsageLimit(trimmed))
|
|
79
|
+
return undefined;
|
|
80
|
+
const shape = CLAUDE_USAGE_LIMIT.exec(trimmed);
|
|
81
|
+
const window = spentWindow(accountName, now);
|
|
82
|
+
const parsed = usageLimitResetMs(trimmed, now);
|
|
83
|
+
const resetMs = parsed !== undefined && parsed > 0 ? parsed : window?.resetMs;
|
|
84
|
+
if (resetMs === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
const capMs = namedWindowMs(shape?.[1]) ?? window?.windowMs ?? UNNAMED_WINDOW_MS;
|
|
87
|
+
return Math.min(resetMs, capMs);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=usage-limit-block.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MANUAL_CONTINUE_CUSTOM_TYPE } from "../../../manual-continue.js";
|
|
2
2
|
import { GOAL_CONTINUATION_MESSAGE_TYPE } from "../../../messages.js";
|
|
3
|
+
import { buildDeadlineStopNotice, deadlineStopPendingSinceLastTurn, TURN_DEADLINE_ENTRY_TYPE, } from "../../../turn-deadline.js";
|
|
4
|
+
import { noticeEntryRenderer } from "../../notice/index.js";
|
|
3
5
|
import { continueGoalAfterAgentEnd } from "./agent-end-continuation.js";
|
|
4
6
|
import { GOAL_CACHE_WARMUP_ENTRY_TYPE } from "./cache-warm.js";
|
|
5
7
|
import { isSameGoalCacheWarmCard, renderGoalCacheWarmupEntry } from "./cache-warm-renderer.js";
|
|
@@ -58,6 +60,12 @@ export default function goalExtension(pi) {
|
|
|
58
60
|
pi.registerEntryRenderer(GOAL_CACHE_WARMUP_ENTRY_TYPE, renderGoalCacheWarmupEntry, {
|
|
59
61
|
replaces: isSameGoalCacheWarmCard,
|
|
60
62
|
});
|
|
63
|
+
pi.registerEntryRenderer(TURN_DEADLINE_ENTRY_TYPE, noticeEntryRenderer((entry) => {
|
|
64
|
+
const data = entry.data;
|
|
65
|
+
if (data === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
return { title: "turn stopped on a deadline", why: buildDeadlineStopNotice(data) };
|
|
68
|
+
}));
|
|
61
69
|
registerGoalTools(pi, {
|
|
62
70
|
goalStoreRef: (ctx) => buildGoalStoreRef(ctx.sessionManager, ctx.cwd),
|
|
63
71
|
accountCurrentAgentTurn,
|
|
@@ -246,14 +254,16 @@ export default function goalExtension(pi) {
|
|
|
246
254
|
goal = continuationGoal;
|
|
247
255
|
syncContinuationGoal(ctx, goal);
|
|
248
256
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
if (deadlineStopPendingSinceLastTurn(ctx.sessionManager.getBranch()) === undefined) {
|
|
258
|
+
todoOwedBackstop.afterAgentEnd({
|
|
259
|
+
ctx,
|
|
260
|
+
event,
|
|
261
|
+
goal,
|
|
262
|
+
continuationPending,
|
|
263
|
+
hasActiveWakeSources: monitorContinuation.hasActiveWakeSources(),
|
|
264
|
+
wakeSourceNames: monitorContinuation.liveWakeSources(),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
257
267
|
});
|
|
258
268
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
259
269
|
const goal = await monitorContinuation.afterAgentSettled();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deadlineStopPendingSinceLastTurn } from "../../../turn-deadline.js";
|
|
1
2
|
import { createGoalCacheWarmScheduleData, estimateCacheWarmMetrics, GOAL_CACHE_WARMUP_ENTRY_TYPE, GOAL_MONITOR_CONTINUATION_FALLBACK_DELAY_MS, resolveGoalBackstopMaxSecondsForCache, resolveGoalMonitorContinuationDelayMs, resolveGoalPromptCacheLifetime, } from "./cache-warm.js";
|
|
2
3
|
import { subscribeGoalChannelState } from "./channel-state-subscriptions.js";
|
|
3
4
|
import { publishGoalContinuationEvent } from "./rpc-wait-surface.js";
|
|
@@ -146,6 +147,10 @@ export class MonitorAwareGoalContinuation {
|
|
|
146
147
|
}
|
|
147
148
|
if (options.willRetry)
|
|
148
149
|
return options.goal;
|
|
150
|
+
// A deadline stop is never auto-continued; the hold releases on admitted user input.
|
|
151
|
+
if (deadlineStopPendingSinceLastTurn(options.ctx.sessionManager.getBranch()) !== undefined) {
|
|
152
|
+
return options.goal;
|
|
153
|
+
}
|
|
149
154
|
if (this.#activeWakeSourceCount() > 0)
|
|
150
155
|
this.#schedule(options.goal, "monitor");
|
|
151
156
|
else if (lastAssistantMessage(options.messages)?.stopReason === "error") {
|
|
@@ -429,6 +434,8 @@ export class MonitorAwareGoalContinuation {
|
|
|
429
434
|
const goal = this.#goal;
|
|
430
435
|
if (ctx === undefined || goal?.status !== "active" || !ctx.isIdle() || ctx.hasPendingMessages())
|
|
431
436
|
return;
|
|
437
|
+
if (deadlineStopPendingSinceLastTurn(ctx.sessionManager.getBranch()) !== undefined)
|
|
438
|
+
return;
|
|
432
439
|
if (kind === "monitor" && this.#activeWakeSourceCount() === 0 && !drainFire)
|
|
433
440
|
return;
|
|
434
441
|
const admission = await this.#admitAndQueue(ctx, goal, kind === "monitor" ? "monitorDelayed" : kind === "providerRetry" ? "providerRecovery" : "userGrace", this.#lastAgentEndMessages);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deadlineStopPendingSinceLastTurn } from "../../../turn-deadline.js";
|
|
1
2
|
import { GOAL_CONTINUATION_CAP } from "./continuation.js";
|
|
2
3
|
/**
|
|
3
4
|
* Re-engages a goal after a config reload retired the previous extension
|
|
@@ -21,6 +22,10 @@ export async function reengageGoalAfterReload(options) {
|
|
|
21
22
|
const { ctx, goal, monitor } = options;
|
|
22
23
|
if (goal.status !== "active")
|
|
23
24
|
return "skipped-inactive";
|
|
25
|
+
// A deadline stop stays held across reload: re-engagement resumes only on admitted input.
|
|
26
|
+
if (deadlineStopPendingSinceLastTurn(ctx.sessionManager.getBranch()) !== undefined) {
|
|
27
|
+
return "skipped-inactive";
|
|
28
|
+
}
|
|
24
29
|
if (monitor.hasActiveWakeSources()) {
|
|
25
30
|
monitor.rearmMonitorBackstop(goal);
|
|
26
31
|
return "backstop-rearmed";
|
|
@@ -47,6 +47,7 @@ import toolPairGuardExtension from "./tool-pair-guard/index.js";
|
|
|
47
47
|
import toolSearchExtension from "./tool-search/index.js";
|
|
48
48
|
import tpsExtension from "./tps.js";
|
|
49
49
|
import ttsrExtension from "./ttsr/index.js";
|
|
50
|
+
import vertexModelGardenExtension from "./vertex-model-garden/index.js";
|
|
50
51
|
import videoInExtension from "./video-in/index.js";
|
|
51
52
|
import webfetchExtension from "./webfetch/index.js";
|
|
52
53
|
import websearchExtension from "./websearch/index.js";
|
|
@@ -117,6 +118,8 @@ export const builtinExtensions = [
|
|
|
117
118
|
{ id: "claude-sdk-oauth", factory: anthropicSubscriptionExtension },
|
|
118
119
|
// Registers unconditionally and reports executable/auth state through its oauth check, so it stays beside the other provider lane.
|
|
119
120
|
{ id: "cursor-cli-oauth", factory: cursorCliOauthExtension },
|
|
121
|
+
// Provider-only lane beside the other provider registrations; no hooks, so order is not load-bearing.
|
|
122
|
+
{ id: "vertex-model-garden", factory: vertexModelGardenExtension },
|
|
120
123
|
// Config reload follows settings-dependent builtins so reloads rebuild their resolved settings before catalog feeders observe them.
|
|
121
124
|
{ id: "config-reload", factory: configReloadExtension },
|
|
122
125
|
// Shared catalog wiring loads before MCP, which feeds its tools into the shared catalog as the final builtin.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wider "Allow always" a prompt may offer for a tool WITHOUT its own permission parser, where
|
|
3
|
+
* plain "Allow always" remembers only the exact argument (one URL, one input) and so asks again for
|
|
4
|
+
* the next one (senpi#3403):
|
|
5
|
+
* - a URL argument: that URL's DOMAIN (scheme + canonical host + non-default port), stored as one
|
|
6
|
+
* literal and matched by equality, never the whole tool: a tool-wide allow for a browser or
|
|
7
|
+
* fetch tool would mean "go anywhere", close to Full access for the web;
|
|
8
|
+
* - a free `input`/`pattern` argument: the tool itself, for this project;
|
|
9
|
+
* - a command or a path: nothing wider (that would grant a shell or file access wholesale).
|
|
10
|
+
* Built-in tools (bash, eval, edit, write, read, ...) have parsers and never reach this.
|
|
11
|
+
*/
|
|
12
|
+
export interface WiderAlwaysScope {
|
|
13
|
+
/** The option label, stating the scope in plain words. */
|
|
14
|
+
readonly label: string;
|
|
15
|
+
/** The patterns an answer with this option approves for the permission. */
|
|
16
|
+
readonly patterns: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The one normalizer both sides of a domain grant go through (senpi#3403). A parser-less tool (an
|
|
20
|
+
* MCP server, a custom tool) parses the URL itself, and the permission layer cannot make it reuse
|
|
21
|
+
* this parse. So a domain origin is given only for raw text every parser reads as the same host:
|
|
22
|
+
* the strict shape above, no ambiguous character anywhere, and the same host and port as the WHATWG
|
|
23
|
+
* parser finds (#3416 review: `https://example.invalid\\@evil.test/` is example.invalid to WHATWG and
|
|
24
|
+
* evil.test to curl and Python). Anything else gets no alias and no domain option, so it asks.
|
|
25
|
+
*/
|
|
26
|
+
export declare function canonicalUrlOrigin(raw: string): string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The match target a domain grant is written for: a literal that never contains a wildcard
|
|
29
|
+
* character, so the grant matches by equality. The `origin=` prefix keeps it from colliding with a
|
|
30
|
+
* user's own URL rules.
|
|
31
|
+
*/
|
|
32
|
+
export declare function urlOriginAlias(raw: string): string | undefined;
|
|
33
|
+
export declare function widerAlwaysScope(toolName: string, argKey: string, arg: string): WiderAlwaysScope | undefined;
|
|
34
|
+
//# sourceMappingURL=always-scope.d.ts.map
|