@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/bundle/chunks/{chunk-QQUHMCOM.js → chunk-2NO5ZOYE.js} +6 -6
  3. package/dist/bundle/chunks/chunk-74YFRVGQ.js +2 -0
  4. package/dist/bundle/chunks/{chunk-4R4PRTF5.js → chunk-CJRE5LLR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-Q7RD3NOC.js → chunk-GY5O7NIQ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-KRAABTZY.js → chunk-JD4CV3GH.js} +2 -2
  7. package/dist/bundle/chunks/chunk-K2EVQGGT.js +31 -0
  8. package/dist/bundle/chunks/{chunk-SSEGIXRL.js → chunk-KUQ3K33L.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-V5JFWWGJ.js → chunk-KWC3MCLP.js} +1 -1
  10. package/dist/bundle/chunks/chunk-MLL7AGVM.js +3 -0
  11. package/dist/bundle/chunks/chunk-PAJKEG2L.js +8 -0
  12. package/dist/bundle/chunks/{chunk-DPRV3FR7.js → chunk-PWHAOWNH.js} +1 -1
  13. package/dist/bundle/chunks/chunk-WL4Y6XN6.js +18 -0
  14. package/dist/bundle/chunks/{chunk-VY43SSYL.js → chunk-XCRGEMY2.js} +144 -134
  15. package/dist/bundle/chunks/{cli-main-JDL52ADK.js → cli-main-I6JF2GPS.js} +1 -1
  16. package/dist/bundle/chunks/google-generative-ai-AI6PAUPA.js +2 -0
  17. package/dist/bundle/chunks/google-vertex-USNRIFF7.js +2 -0
  18. package/dist/bundle/chunks/google-vertex-adc-JZURL4XM.js +2 -0
  19. package/dist/bundle/chunks/{help-fast-path-SVCMHCYG.js → help-fast-path-APSHUDY6.js} +1 -1
  20. package/dist/bundle/chunks/{host-session-command-46PTZGIB.js → host-session-command-BUJUJHDA.js} +1 -1
  21. package/dist/bundle/chunks/{interactive-mode-7IM7YJYH.js → interactive-mode-4Q3TIXOZ.js} +1 -1
  22. package/dist/bundle/chunks/{multi-session-host-3MEEUMYU.js → multi-session-host-XEZB2XVR.js} +6 -5
  23. package/dist/bundle/chunks/{openai-codex-responses-T26WW2RE.js → openai-codex-responses-NV27Q6VV.js} +1 -1
  24. package/dist/bundle/chunks/{package-manager-cli-UCHXVBQ2.js → package-manager-cli-YWZHOZOY.js} +1 -1
  25. package/dist/bundle/chunks/{rotation-stream-QX7YSKDU.js → rotation-stream-HY3JPQCX.js} +1 -1
  26. package/dist/bundle/chunks/{rpc-mode-D4BJKWFL.js → rpc-mode-SLWXVXXE.js} +1 -1
  27. package/dist/bundle/chunks/{schedule-command-2RHSWBQO.js → schedule-command-JRWZGEDU.js} +1 -1
  28. package/dist/bundle/chunks/{session-control-endpoint-NBSRSAJS.js → session-control-endpoint-E2ZSEWXW.js} +1 -1
  29. package/dist/bundle/chunks/session-picker-5IZY7E3G.js +2 -0
  30. package/dist/bundle/chunks/session-worker.js +266 -255
  31. package/dist/bundle/cli.js +2 -2
  32. package/dist/bundle/index.js +1 -1
  33. package/dist/bundle/rpc-entry.js +1 -1
  34. package/dist/bundle/runtime-manifest.json +1 -1
  35. package/dist/core/agent-session-runtime.d.ts +15 -0
  36. package/dist/core/agent-session-runtime.js +53 -2
  37. package/dist/core/agent-session.d.ts +28 -0
  38. package/dist/core/agent-session.js +88 -3
  39. package/dist/core/context-files.d.ts +21 -0
  40. package/dist/core/context-files.js +175 -0
  41. package/dist/core/diagnostics.d.ts +1 -0
  42. package/dist/core/dynamic-prompt/intent-gate.d.ts +3 -0
  43. package/dist/core/dynamic-prompt/intent-gate.js +4 -3
  44. package/dist/core/dynamic-prompt/policies.d.ts +4 -1
  45. package/dist/core/dynamic-prompt/policies.js +3 -2
  46. package/dist/core/dynamic-prompt/working-task.d.ts +4 -1
  47. package/dist/core/dynamic-prompt/working-task.js +3 -2
  48. package/dist/core/dynamic-prompt/workstation.d.ts +4 -3
  49. package/dist/core/dynamic-prompt/workstation.js +1 -0
  50. package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +26 -3
  51. package/dist/core/extensions/builtin/anthropic-subscription/account-management.d.ts +5 -0
  52. package/dist/core/extensions/builtin/anthropic-subscription/account-management.js +40 -0
  53. package/dist/core/extensions/builtin/anthropic-subscription/accounts.d.ts +3 -0
  54. package/dist/core/extensions/builtin/anthropic-subscription/accounts.js +1 -1
  55. package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +1 -1
  56. package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
  57. package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +10 -5
  58. package/dist/core/extensions/builtin/anthropic-subscription/failover.js +158 -17
  59. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.d.ts +13 -0
  60. package/dist/core/extensions/builtin/anthropic-subscription/usage-limit-block.js +89 -0
  61. package/dist/core/extensions/builtin/goal/index.js +18 -8
  62. package/dist/core/extensions/builtin/goal/monitor-continuation.js +7 -0
  63. package/dist/core/extensions/builtin/goal/reload-reengagement.js +5 -0
  64. package/dist/core/extensions/builtin/index.js +3 -0
  65. package/dist/core/extensions/builtin/permission-system/always-scope.d.ts +34 -0
  66. package/dist/core/extensions/builtin/permission-system/always-scope.js +77 -0
  67. package/dist/core/extensions/builtin/permission-system/index.js +3 -1
  68. package/dist/core/extensions/builtin/permission-system/parsers.d.ts +1 -1
  69. package/dist/core/extensions/builtin/permission-system/parsers.js +24 -3
  70. package/dist/core/extensions/builtin/permission-system/prompt.js +5 -1
  71. package/dist/core/extensions/builtin/permission-system/service.js +1 -1
  72. package/dist/core/extensions/builtin/permission-system/types.d.ts +7 -0
  73. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  74. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.d.ts +7 -0
  75. package/dist/core/extensions/builtin/prompt-preset/gemini-4-argon.js +83 -0
  76. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  77. package/dist/core/extensions/builtin/prompt-preset/matchers.js +12 -0
  78. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  79. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  80. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  81. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +16 -0
  82. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +19 -0
  83. package/dist/core/extensions/builtin/vertex-model-garden/index.d.ts +6 -0
  84. package/dist/core/extensions/builtin/vertex-model-garden/index.js +27 -0
  85. package/dist/core/extensions/builtin/vertex-model-garden/models.d.ts +3 -0
  86. package/dist/core/extensions/builtin/vertex-model-garden/models.js +34 -0
  87. package/dist/core/extensions/builtin/vertex-model-garden/transport.d.ts +14 -0
  88. package/dist/core/extensions/builtin/vertex-model-garden/transport.js +35 -0
  89. package/dist/core/extensions/types.d.ts +32 -1
  90. package/dist/core/package-manager.js +4 -32
  91. package/dist/core/project-trust.d.ts +8 -0
  92. package/dist/core/project-trust.js +12 -0
  93. package/dist/core/provider-api-key-auth.d.ts +9 -1
  94. package/dist/core/provider-api-key-auth.js +34 -0
  95. package/dist/core/provider-composer.d.ts +23 -0
  96. package/dist/core/resource-loader.d.ts +5 -7
  97. package/dist/core/resource-loader.js +11 -84
  98. package/dist/core/retry-fallback/circuit-probes.d.ts +1 -0
  99. package/dist/core/retry-fallback/circuit-probes.js +3 -0
  100. package/dist/core/retry-fallback/circuit.d.ts +5 -0
  101. package/dist/core/retry-fallback/circuit.js +27 -4
  102. package/dist/core/retry-fallback/controller.d.ts +9 -4
  103. package/dist/core/retry-fallback/controller.js +21 -4
  104. package/dist/core/settings-manager.d.ts +19 -0
  105. package/dist/core/settings-manager.js +27 -1
  106. package/dist/core/settings-shapes.d.ts +4 -0
  107. package/dist/core/skills-prompt.d.ts +18 -0
  108. package/dist/core/skills-prompt.js +131 -0
  109. package/dist/core/skills.d.ts +1 -9
  110. package/dist/core/skills.js +1 -59
  111. package/dist/core/trust-manager.d.ts +13 -4
  112. package/dist/core/trust-manager.js +52 -21
  113. package/dist/core/turn-deadline.d.ts +92 -0
  114. package/dist/core/turn-deadline.js +179 -0
  115. package/dist/main.js +23 -4
  116. package/dist/modes/interactive/interactive-mode.js +10 -3
  117. package/dist/modes/rpc/abort-grace.d.ts +9 -0
  118. package/dist/modes/rpc/abort-grace.js +11 -0
  119. package/dist/modes/rpc/client-admission-record.d.ts +1 -0
  120. package/dist/modes/rpc/client-admissions.d.ts +6 -3
  121. package/dist/modes/rpc/client-admissions.js +9 -4
  122. package/dist/modes/rpc/connection-handler.js +8 -3
  123. package/dist/modes/rpc/effect-commands.d.ts +1 -3
  124. package/dist/modes/rpc/effect-commands.js +4 -5
  125. package/dist/modes/rpc/effect-outcome-copy.d.ts +4 -4
  126. package/dist/modes/rpc/effect-outcome-copy.js +4 -4
  127. package/dist/modes/rpc/effect-record.d.ts +2 -0
  128. package/dist/modes/rpc/effect-record.js +2 -0
  129. package/dist/modes/rpc/effect-status.js +1 -1
  130. package/dist/modes/rpc/host-handover-parked.d.ts +6 -0
  131. package/dist/modes/rpc/host-handover-parked.js +1 -0
  132. package/dist/modes/rpc/host-handover-reopen.js +11 -2
  133. package/dist/modes/rpc/rpc-client.d.ts +28 -2
  134. package/dist/modes/rpc/rpc-client.js +24 -11
  135. package/dist/modes/rpc/rpc-request-deadline.d.ts +12 -0
  136. package/dist/modes/rpc/rpc-request-deadline.js +15 -0
  137. package/dist/modes/rpc/session-command-router.js +7 -4
  138. package/dist/modes/rpc/session-registry-attach.js +6 -1
  139. package/dist/modes/rpc/session-registry-claims.d.ts +22 -2
  140. package/dist/modes/rpc/session-registry-claims.js +68 -0
  141. package/dist/modes/rpc/session-registry-opening.d.ts +23 -0
  142. package/dist/modes/rpc/session-registry-opening.js +57 -0
  143. package/dist/modes/rpc/session-registry.d.ts +1 -0
  144. package/dist/modes/rpc/session-registry.js +48 -60
  145. package/dist/modes/rpc/session-unload.js +4 -1
  146. package/docs/rpc.md +10 -3
  147. package/docs/settings.md +16 -1
  148. package/package.json +6 -6
  149. package/dist/bundle/chunks/chunk-6MEETY7N.js +0 -8
  150. package/dist/bundle/chunks/chunk-7ASXJ4AD.js +0 -47
  151. package/dist/bundle/chunks/chunk-TEGEPQIA.js +0 -3
  152. package/dist/bundle/chunks/google-generative-ai-YNIOL5FT.js +0 -2
  153. package/dist/bundle/chunks/google-vertex-CF6AYVXL.js +0 -2
  154. 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) before rotating. */
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 TRANSIENT_RETRIES_PER_TURN same-account retries
45
- * for a transient failure (and one retry on newer stored credentials). A retry is transparent only before a
46
- * text, thinking, or tool-call event reaches the caller; post-delta failures
47
- * are marked so AgentSession never replays the partial turn.
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) before rotating. */
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
- const duration = Math.min(MAX_RATE_LIMIT_BLOCK_MS, retryAfterMs(error) ?? fallback);
68
- return { ...account, blockedUntil: now + duration, blockReason: classification.kind };
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 TRANSIENT_RETRIES_PER_TURN same-account retries
146
- * for a transient failure (and one retry on newer stored credentials). A retry is transparent only before a
147
- * text, thinking, or tool-call event reaches the caller; post-delta failures
148
- * are marked so AgentSession never replays the partial turn.
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
- return;
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, and on the config-dir lane moving
195
- // to another account re-sends the whole conversation (senpi#2891): retry it in place first.
196
- // The budget is per turn, not per account: an outage that fails every account must not
197
- // multiply into retries on each of them.
198
- if (!visibleDeltaEmitted && isTransient(classification) && transientRetriesUsed < TRANSIENT_RETRIES_PER_TURN) {
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
- const superseded = await persistBlock(options.store, options.providerId, blocked, now());
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
- todoOwedBackstop.afterAgentEnd({
250
- ctx,
251
- event,
252
- goal,
253
- continuationPending,
254
- hasActiveWakeSources: monitorContinuation.hasActiveWakeSources(),
255
- wakeSourceNames: monitorContinuation.liveWakeSources(),
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