@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
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { chmodSync, existsSync, globSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { stripBom } from "../utils/text.js";
|
|
5
|
+
import { projectAgentsSkillDirs } from "./trust-manager.js";
|
|
5
6
|
function getEnv() {
|
|
6
7
|
if (process.platform !== "linux" || Object.keys(process.env).length > 0) {
|
|
7
8
|
return process.env;
|
|
@@ -207,36 +208,9 @@ function collectFiles(dir, filePattern, skipNodeModules = true, ignoreMatcher, r
|
|
|
207
208
|
}
|
|
208
209
|
return files;
|
|
209
210
|
}
|
|
210
|
-
function findGitRepoRoot(startDir) {
|
|
211
|
-
let dir = resolve(startDir);
|
|
212
|
-
while (true) {
|
|
213
|
-
if (existsSync(join(dir, ".git"))) {
|
|
214
|
-
return dir;
|
|
215
|
-
}
|
|
216
|
-
const parent = dirname(dir);
|
|
217
|
-
if (parent === dir) {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
dir = parent;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
211
|
function collectAncestorAgentsSkillDirs(startDir) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const gitRepoRoot = findGitRepoRoot(resolvedStartDir);
|
|
227
|
-
let dir = resolvedStartDir;
|
|
228
|
-
while (true) {
|
|
229
|
-
skillDirs.push(join(dir, ".agents", "skills"));
|
|
230
|
-
if (gitRepoRoot && dir === gitRepoRoot) {
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
const parent = dirname(dir);
|
|
234
|
-
if (parent === dir) {
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
dir = parent;
|
|
238
|
-
}
|
|
239
|
-
return skillDirs;
|
|
212
|
+
// The same walk the trust check uses: home and above are never project skills (#3420 review).
|
|
213
|
+
return projectAgentsSkillDirs(startDir);
|
|
240
214
|
}
|
|
241
215
|
function collectAutoPromptEntries(dir) {
|
|
242
216
|
const entries = [];
|
|
@@ -1964,9 +1938,7 @@ export class DefaultPackageManager {
|
|
|
1964
1938
|
};
|
|
1965
1939
|
const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
|
|
1966
1940
|
const projectTrusted = this.settingsManager.isProjectTrusted();
|
|
1967
|
-
const projectAgentsSkillDirs = projectTrusted
|
|
1968
|
-
? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir))
|
|
1969
|
-
: [];
|
|
1941
|
+
const projectAgentsSkillDirs = projectTrusted ? collectAncestorAgentsSkillDirs(this.cwd) : [];
|
|
1970
1942
|
const addResources = (resourceType, paths, metadata, overrides, baseDir) => {
|
|
1971
1943
|
const target = this.getTargetMap(accumulator, resourceType);
|
|
1972
1944
|
for (const path of paths) {
|
|
@@ -2,6 +2,14 @@ import type { LoadExtensionsResult, ProjectTrustContext } from "./extensions/typ
|
|
|
2
2
|
import type { DefaultProjectTrust } from "./settings-manager.ts";
|
|
3
3
|
import { type ProjectTrustStore } from "./trust-manager.ts";
|
|
4
4
|
export type AppMode = "interactive" | "print" | "json" | "rpc";
|
|
5
|
+
/**
|
|
6
|
+
* Full access means no prompt anywhere (sisyphuslabs/omo-desktop-app#2465): a session whose
|
|
7
|
+
* permission preset is explicitly `full-access` trusts its project for that session. Explicit
|
|
8
|
+
* means the session's `permission-preset` flag (CLI, or `open_session.permissionPreset` through
|
|
9
|
+
* the launch profile), else the user's global setting. Project settings never count: they are what
|
|
10
|
+
* trust gates. The implicit default never counts either, so a plain launch keeps asking.
|
|
11
|
+
*/
|
|
12
|
+
export declare function presetTrustsProject(presetFlag: boolean | string | undefined, globalPreset: unknown): boolean;
|
|
5
13
|
export interface ResolveProjectTrustedOptions {
|
|
6
14
|
cwd: string;
|
|
7
15
|
trustStore: ProjectTrustStore;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { APP_NAME, CONFIG_DIR_NAME } from "../config.js";
|
|
2
2
|
import { emitProjectTrustEvent } from "./extensions/runner.js";
|
|
3
3
|
import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* Full access means no prompt anywhere (sisyphuslabs/omo-desktop-app#2465): a session whose
|
|
6
|
+
* permission preset is explicitly `full-access` trusts its project for that session. Explicit
|
|
7
|
+
* means the session's `permission-preset` flag (CLI, or `open_session.permissionPreset` through
|
|
8
|
+
* the launch profile), else the user's global setting. Project settings never count: they are what
|
|
9
|
+
* trust gates. The implicit default never counts either, so a plain launch keeps asking.
|
|
10
|
+
*/
|
|
11
|
+
export function presetTrustsProject(presetFlag, globalPreset) {
|
|
12
|
+
if (typeof presetFlag === "string")
|
|
13
|
+
return presetFlag === "full-access";
|
|
14
|
+
return globalPreset === "full-access";
|
|
15
|
+
}
|
|
4
16
|
function formatProjectTrustPrompt(cwd) {
|
|
5
17
|
return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
|
|
6
18
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { type ApiKeyAuth, type ModelAuth, type Provider, type ProviderHeaders } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type ApiKeyAuth, type AuthContext, type AuthResult, type ModelAuth, type Provider, type ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ModelsJsonProvider } from "./model-config.ts";
|
|
3
|
+
interface AmbientAuthExtensionInput {
|
|
4
|
+
name?: string;
|
|
5
|
+
resolve(input: {
|
|
6
|
+
ctx: AuthContext;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}): Promise<AuthResult | undefined>;
|
|
9
|
+
}
|
|
3
10
|
interface ProviderAuthExtensionInput {
|
|
4
11
|
apiKey?: string;
|
|
5
12
|
headers?: Record<string, string>;
|
|
6
13
|
authHeader?: boolean;
|
|
7
14
|
oauth?: unknown;
|
|
15
|
+
ambientAuth?: AmbientAuthExtensionInput;
|
|
8
16
|
}
|
|
9
17
|
export declare function composeApiKeyAuth(providerId: string, base: Provider | undefined, config: ModelsJsonProvider | undefined, extension: ProviderAuthExtensionInput | undefined): ApiKeyAuth | undefined;
|
|
10
18
|
export declare function configuredApiKey(config: ModelsJsonProvider | undefined, extension: ProviderAuthExtensionInput | undefined): string | undefined;
|
|
@@ -8,6 +8,14 @@ export function composeApiKeyAuth(providerId, base, config, extension) {
|
|
|
8
8
|
const rawHeaders = configuredHeaders(config, extension);
|
|
9
9
|
const headerSource = headerAuthResolutionSource(config?.headers, extension?.headers);
|
|
10
10
|
const authHeader = extension?.authHeader ?? config?.authHeader ?? false;
|
|
11
|
+
const ambient = extension?.ambientAuth;
|
|
12
|
+
if (!inherited && rawKey === undefined && !headerSource && ambient) {
|
|
13
|
+
// A shared cloud credential chain (Google ADC, AWS env) owns the lane: an
|
|
14
|
+
// explicitly configured key, credential headers, or a base provider still
|
|
15
|
+
// win, and a resolution marked ambient never outranks a configured
|
|
16
|
+
// provider in automatic default selection (senpi#2327).
|
|
17
|
+
return ambientApiKeyAuth(providerId, ambient, rawHeaders, headerSource, authHeader);
|
|
18
|
+
}
|
|
11
19
|
if (!inherited && rawKey === undefined && !headerSource && oauth) {
|
|
12
20
|
// An OAuth provider with no key, no credential headers and no base normally
|
|
13
21
|
// gets no api-key auth at all. Providers with ambient credentials still
|
|
@@ -101,6 +109,32 @@ function ambientOnlyAuth(providerId, oauth, rawHeaders, headerSource, authHeader
|
|
|
101
109
|
resolve,
|
|
102
110
|
};
|
|
103
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Api-key adapter for extension providers whose credentials live in a shared
|
|
114
|
+
* cloud credential chain (Google ADC, AWS env). No login is offered — the chain
|
|
115
|
+
* is the only source — and `Models.checkAuth` resolves this auth, so the
|
|
116
|
+
* `ambient` marker reaches the provider's auth status.
|
|
117
|
+
*/
|
|
118
|
+
function ambientApiKeyAuth(providerId, ambient, rawHeaders, headerSource, authHeader) {
|
|
119
|
+
const resolve = async (input) => {
|
|
120
|
+
const result = await ambient.resolve({ ctx: input.ctx, signal: input.signal });
|
|
121
|
+
if (!result)
|
|
122
|
+
return undefined;
|
|
123
|
+
const explicitEnv = { ...(input.credential?.env ?? {}), ...(result.env ?? {}) };
|
|
124
|
+
const headerEnv = await configContextEnv(Object.values(rawHeaders ?? {}), input.ctx, explicitEnv);
|
|
125
|
+
const headers = await resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, headerEnv);
|
|
126
|
+
return {
|
|
127
|
+
...result,
|
|
128
|
+
auth: withConfiguredAuth(result.auth, headers, authHeader),
|
|
129
|
+
source: result.source ?? headerSource,
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
name: ambient.name ?? "Ambient credentials",
|
|
134
|
+
ambientOnly: true,
|
|
135
|
+
resolve,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
104
138
|
export function configuredApiKey(config, extension) {
|
|
105
139
|
return extension?.apiKey ?? config?.apiKey;
|
|
106
140
|
}
|
|
@@ -80,6 +80,15 @@ export interface ProviderConfigInput {
|
|
|
80
80
|
extraBody?: Record<string, unknown>;
|
|
81
81
|
authHeader?: boolean;
|
|
82
82
|
oauth?: ExtensionOAuthConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Ambient api-key auth resolved from a shared cloud credential chain (Google
|
|
85
|
+
* ADC, AWS env) rather than a credential configured for this provider. The
|
|
86
|
+
* provider counts as configured only while `resolve` yields a result, and the
|
|
87
|
+
* ambient marker keeps it from outranking a provider the user configured when
|
|
88
|
+
* the automatic default model is chosen (senpi#2327). Ignored when `apiKey`,
|
|
89
|
+
* credential headers, or a base provider already supply auth.
|
|
90
|
+
*/
|
|
91
|
+
ambientAuth?: ProviderAmbientAuth;
|
|
83
92
|
retryPolicy?: RetryPolicyProfile;
|
|
84
93
|
models?: ProviderModelConfig[];
|
|
85
94
|
refreshModels?(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
|
|
@@ -92,6 +101,20 @@ export interface ProviderConfigInput {
|
|
|
92
101
|
*/
|
|
93
102
|
fallbackEligible?(): boolean;
|
|
94
103
|
}
|
|
104
|
+
/** Ambient api-key auth hook for extension providers (see `ProviderConfigInput.ambientAuth`). */
|
|
105
|
+
export interface ProviderAmbientAuth {
|
|
106
|
+
/** Display name, e.g. "Google Cloud credentials". */
|
|
107
|
+
name?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve from the ambient chain. `undefined` means the chain currently
|
|
110
|
+
* supplies no credential, so the provider stays unavailable. A returned
|
|
111
|
+
* result should carry `ambient: true`.
|
|
112
|
+
*/
|
|
113
|
+
resolve(input: {
|
|
114
|
+
ctx: AuthContext;
|
|
115
|
+
signal: AbortSignal;
|
|
116
|
+
}): Promise<AuthResult | undefined>;
|
|
117
|
+
}
|
|
95
118
|
export type AuthStatus = {
|
|
96
119
|
configured: boolean;
|
|
97
120
|
source?: "stored" | "runtime" | "environment" | "fallback" | "models_json_key" | "models_json_command" | HeaderAuthStatusSource;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Theme } from "../modes/interactive/theme/theme.ts";
|
|
2
2
|
import type { ResourceDiagnostic } from "./diagnostics.ts";
|
|
3
3
|
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.ts";
|
|
4
|
+
export { loadProjectContextFiles };
|
|
5
|
+
import { loadProjectContextFiles } from "./context-files.ts";
|
|
4
6
|
import { type EventBus } from "./event-bus.ts";
|
|
5
7
|
import type { HostMcpRegistry } from "./extensions/builtin/mcp/host-registry.ts";
|
|
6
8
|
import type { ExtensionFactory, InlineExtension, LoadExtensionsResult, LoadedHookSources, SessionContext, SessionKind } from "./extensions/types.ts";
|
|
@@ -64,6 +66,7 @@ export interface ResourceLoader {
|
|
|
64
66
|
path: string;
|
|
65
67
|
content: string;
|
|
66
68
|
}>;
|
|
69
|
+
diagnostics?: ResourceDiagnostic[];
|
|
67
70
|
};
|
|
68
71
|
getSystemPrompt(): string | undefined;
|
|
69
72
|
getSystemPromptSource(): {
|
|
@@ -85,13 +88,6 @@ export interface ResourceLoader {
|
|
|
85
88
|
* session. Canonicalizing collapses both spellings to one path.
|
|
86
89
|
*/
|
|
87
90
|
export declare function canonicalizeGlobalDefaultExtensionModulePath(modulePath: string): string;
|
|
88
|
-
export declare function loadProjectContextFiles(options: {
|
|
89
|
-
cwd: string;
|
|
90
|
-
agentDir: string;
|
|
91
|
-
}): Array<{
|
|
92
|
-
path: string;
|
|
93
|
-
content: string;
|
|
94
|
-
}>;
|
|
95
91
|
export interface DefaultResourceLoaderOptions {
|
|
96
92
|
cwd: string;
|
|
97
93
|
agentDir: string;
|
|
@@ -185,6 +181,7 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
185
181
|
private themes;
|
|
186
182
|
private themeDiagnostics;
|
|
187
183
|
private agentsFiles;
|
|
184
|
+
private contextDiagnostics;
|
|
188
185
|
private systemPrompt?;
|
|
189
186
|
private systemPromptSourcePath?;
|
|
190
187
|
private appendSystemPrompt;
|
|
@@ -222,6 +219,7 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
222
219
|
path: string;
|
|
223
220
|
content: string;
|
|
224
221
|
}>;
|
|
222
|
+
diagnostics: ResourceDiagnostic[];
|
|
225
223
|
};
|
|
226
224
|
getSystemPrompt(): string | undefined;
|
|
227
225
|
getSystemPromptSource(): {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { detectCapabilities, getTerminalColorMode } from "@earendil-works/pi-tui";
|
|
5
5
|
import chalk from "chalk";
|
|
@@ -7,22 +7,24 @@ import { APP_NAME, CONFIG_DIR_NAME, findNodePackageDir, getAgentDir, getPackageD
|
|
|
7
7
|
import { loadThemeFromPath } from "../modes/interactive/theme/theme.js";
|
|
8
8
|
import { resolveInstallPath } from "../runtime-snapshot/marker.js";
|
|
9
9
|
import { ACCEPTED_SHIM_BANNERS, GENERATED_SHIM_BANNER } from "./generated-shim-banner.js";
|
|
10
|
+
export { loadProjectContextFiles };
|
|
10
11
|
import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.js";
|
|
11
12
|
import { stripBom } from "../utils/text.js";
|
|
12
13
|
import { bundledBuiltinExtensions } from "./bundled-resources.js";
|
|
14
|
+
import { loadProjectContextFiles } from "./context-files.js";
|
|
13
15
|
import { createEventBus } from "./event-bus.js";
|
|
14
16
|
import { builtinExtensions, globalDefaultExtensionFactories, globalDefaultExtensionIds, } from "./extensions/builtin/index.js";
|
|
15
17
|
import { createMcpExtension } from "./extensions/builtin/mcp/index.js";
|
|
16
18
|
import { McpService } from "./extensions/builtin/mcp/service.js";
|
|
17
19
|
import { createExtensionRuntime, loadExtensionFromFactory, loadExtensions, } from "./extensions/loader.js";
|
|
18
20
|
import { EMPTY_SESSION_CONTEXT } from "./extensions/types.js";
|
|
19
|
-
import { findGitPaths } from "./footer-data-provider.js";
|
|
20
21
|
import { dedupePathsByPackageIdentity, findNearestPackageIdentity } from "./package-identity.js";
|
|
21
22
|
import { DefaultPackageManager } from "./package-manager.js";
|
|
22
23
|
import { loadPromptTemplates } from "./prompt-templates.js";
|
|
23
24
|
import { memoizeResolvedPaths, resolvedPathsMemoKey } from "./resolved-paths-memo.js";
|
|
24
25
|
import { SettingsManager } from "./settings-manager.js";
|
|
25
26
|
import { loadSkills } from "./skills.js";
|
|
27
|
+
import { warnIfLargeSkillCatalog } from "./skills-prompt.js";
|
|
26
28
|
import { BUILTIN_PATH_PREFIX, createSourceInfo, getSyntheticPathSource, isSyntheticPath, } from "./source-info.js";
|
|
27
29
|
import { resetTimings, time } from "./timings.js";
|
|
28
30
|
const HOST_PROVIDED_EXTENSION_PACKAGES = new Set([
|
|
@@ -256,86 +258,9 @@ function resolveGeneratedGlobalDefaultExtensionFactory(extensionPath, agentDir)
|
|
|
256
258
|
return undefined;
|
|
257
259
|
}
|
|
258
260
|
}
|
|
259
|
-
function loadContextFileFromDir(dir) {
|
|
260
|
-
const candidates = ["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
261
|
-
for (const filename of candidates) {
|
|
262
|
-
const filePath = join(dir, filename);
|
|
263
|
-
if (existsSync(filePath)) {
|
|
264
|
-
try {
|
|
265
|
-
if (!statSync(filePath).isFile()) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
path: filePath,
|
|
270
|
-
content: stripBom(readFileSync(filePath, "utf-8")),
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* The main repo's context file that a nested linked worktree's own copy shadows: both
|
|
282
|
-
* occupy the same logical repository scope, so loading both applies that context twice. Returns
|
|
283
|
-
* undefined when nothing is shadowed, leaving normal ancestor inheritance alone.
|
|
284
|
-
*
|
|
285
|
-
* Returned canonicalized (realpath), because `git worktree add` writes the `.git`
|
|
286
|
-
* file's `gitdir:` target in realpath form while cwd may still be symlinked
|
|
287
|
-
* (macOS `/tmp` -> `/private/tmp`).
|
|
288
|
-
*/
|
|
289
|
-
function findShadowedContextFile(cwd) {
|
|
290
|
-
const gitPaths = findGitPaths(cwd);
|
|
291
|
-
if (!gitPaths)
|
|
292
|
-
return undefined;
|
|
293
|
-
const commonGitDir = canonicalizePath(gitPaths.commonGitDir);
|
|
294
|
-
const worktreeRoot = canonicalizePath(gitPaths.repoDir);
|
|
295
|
-
const mainRepoRoot = dirname(commonGitDir);
|
|
296
|
-
// False for an ordinary repo, where the two are the same dir, and for a sibling
|
|
297
|
-
// worktree (`git worktree add ../feat`), whose main repo is not an ancestor.
|
|
298
|
-
if (!worktreeRoot.startsWith(`${mainRepoRoot}${sep}`))
|
|
299
|
-
return undefined;
|
|
300
|
-
// dirname of the common git dir is the main worktree root only when that dir is
|
|
301
|
-
// itself checked out from the same repo. In a bare layout (`proj/.bare` +
|
|
302
|
-
// `proj/main`) it is just the directory holding `.bare`, which tracks nothing; a
|
|
303
|
-
// submodule's gitdir has no `commondir`, so it lands under `.git/modules`.
|
|
304
|
-
if (canonicalizePath(join(mainRepoRoot, ".git")) !== commonGitDir)
|
|
305
|
-
return undefined;
|
|
306
|
-
const worktreeContextFile = loadContextFileFromDir(worktreeRoot);
|
|
307
|
-
return worktreeContextFile ? join(mainRepoRoot, basename(worktreeContextFile.path)) : undefined;
|
|
308
|
-
}
|
|
309
|
-
export function loadProjectContextFiles(options) {
|
|
310
|
-
const resolvedCwd = resolvePath(options.cwd);
|
|
311
|
-
const resolvedAgentDir = resolvePath(options.agentDir);
|
|
312
|
-
const contextFiles = [];
|
|
313
|
-
const seenPaths = new Set();
|
|
314
|
-
const globalContext = loadContextFileFromDir(resolvedAgentDir);
|
|
315
|
-
if (globalContext) {
|
|
316
|
-
contextFiles.push(globalContext);
|
|
317
|
-
seenPaths.add(globalContext.path);
|
|
318
|
-
}
|
|
319
|
-
const ancestorContextFiles = [];
|
|
320
|
-
const shadowedContextFile = findShadowedContextFile(resolvedCwd);
|
|
321
|
-
let currentDir = resolvedCwd;
|
|
322
|
-
while (true) {
|
|
323
|
-
const contextFile = loadContextFileFromDir(currentDir);
|
|
324
|
-
const isShadowed = shadowedContextFile !== undefined && canonicalizePath(contextFile?.path ?? "") === shadowedContextFile;
|
|
325
|
-
if (contextFile && !isShadowed && !seenPaths.has(contextFile.path)) {
|
|
326
|
-
ancestorContextFiles.unshift(contextFile);
|
|
327
|
-
seenPaths.add(contextFile.path);
|
|
328
|
-
}
|
|
329
|
-
const parentDir = dirname(currentDir);
|
|
330
|
-
if (parentDir === currentDir)
|
|
331
|
-
break;
|
|
332
|
-
currentDir = parentDir;
|
|
333
|
-
}
|
|
334
|
-
contextFiles.push(...ancestorContextFiles);
|
|
335
|
-
return contextFiles;
|
|
336
|
-
}
|
|
337
261
|
export class DefaultResourceLoader {
|
|
338
262
|
constructor(options) {
|
|
263
|
+
this.contextDiagnostics = [];
|
|
339
264
|
this.cwd = resolvePath(options.cwd);
|
|
340
265
|
this.agentDir = resolvePath(options.agentDir);
|
|
341
266
|
this.settingsManager = options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir);
|
|
@@ -421,7 +346,7 @@ export class DefaultResourceLoader {
|
|
|
421
346
|
return { themes: this.themes, diagnostics: this.themeDiagnostics };
|
|
422
347
|
}
|
|
423
348
|
getAgentsFiles() {
|
|
424
|
-
return { agentsFiles: this.agentsFiles };
|
|
349
|
+
return { agentsFiles: this.agentsFiles, diagnostics: this.contextDiagnostics };
|
|
425
350
|
}
|
|
426
351
|
getSystemPrompt() {
|
|
427
352
|
return this.systemPrompt;
|
|
@@ -616,14 +541,16 @@ export class DefaultResourceLoader {
|
|
|
616
541
|
}
|
|
617
542
|
const agentsFiles = {
|
|
618
543
|
agentsFiles: this.noContextFiles
|
|
619
|
-
? []
|
|
544
|
+
? Object.assign([], { diagnostics: [] })
|
|
620
545
|
: loadProjectContextFiles({
|
|
621
546
|
cwd: this.cwd,
|
|
622
547
|
agentDir: this.agentDir,
|
|
548
|
+
...this.settingsManager.getSettings().contextFiles,
|
|
623
549
|
}),
|
|
624
550
|
};
|
|
625
551
|
const resolvedAgentsFiles = this.agentsFilesOverride ? this.agentsFilesOverride(agentsFiles) : agentsFiles;
|
|
626
|
-
this.agentsFiles = resolvedAgentsFiles.agentsFiles;
|
|
552
|
+
this.agentsFiles = [...resolvedAgentsFiles.agentsFiles];
|
|
553
|
+
this.contextDiagnostics = agentsFiles.agentsFiles.diagnostics;
|
|
627
554
|
time("contextFiles", "extensions");
|
|
628
555
|
// SYSTEM.md / APPEND_SYSTEM.md file discovery was intentionally removed; the explicit
|
|
629
556
|
// options are the only static prompt source (see packages/coding-agent/changes.md).
|
|
@@ -872,7 +799,7 @@ export class DefaultResourceLoader {
|
|
|
872
799
|
skill.sourceInfo ??
|
|
873
800
|
this.getDefaultSourceInfoForPath(skill.filePath),
|
|
874
801
|
}));
|
|
875
|
-
this.skillDiagnostics = resolvedSkills.diagnostics;
|
|
802
|
+
this.skillDiagnostics = [...resolvedSkills.diagnostics, ...warnIfLargeSkillCatalog(this.skills)];
|
|
876
803
|
}
|
|
877
804
|
updatePromptsFromPaths(promptPaths, metadataByPath) {
|
|
878
805
|
let promptsResult;
|
|
@@ -16,6 +16,7 @@ export declare class CircuitProbes {
|
|
|
16
16
|
constructor(circuits: FallbackCircuitAccess | undefined);
|
|
17
17
|
isOpen(selector: string): boolean;
|
|
18
18
|
governs(selector: string): boolean;
|
|
19
|
+
isExhausted(selector: string): boolean;
|
|
19
20
|
admit(selector: string): CircuitAdmission["kind"];
|
|
20
21
|
holds(selector: string): boolean;
|
|
21
22
|
get probing(): string | undefined;
|
|
@@ -23,6 +23,9 @@ export class CircuitProbes {
|
|
|
23
23
|
governs(selector) {
|
|
24
24
|
return this.circuits?.governs(selector) ?? false;
|
|
25
25
|
}
|
|
26
|
+
isExhausted(selector) {
|
|
27
|
+
return this.circuits?.isExhausted(selector) ?? false;
|
|
28
|
+
}
|
|
26
29
|
admit(selector) {
|
|
27
30
|
const admission = this.circuits?.admit(selector) ?? { kind: "closed" };
|
|
28
31
|
if (admission.kind === "probe" && this.token?.selector !== selector) {
|
|
@@ -29,6 +29,8 @@ export interface CircuitOpenRequest {
|
|
|
29
29
|
retryAfterMs?: number;
|
|
30
30
|
/** The reset window a usage limit named for itself; when present it alone sets the open. */
|
|
31
31
|
resetWindowMs?: number;
|
|
32
|
+
/** The failure is a verdict for every session: the entry is spent (billing, quota, an account limit). A provider-named wait is not. */
|
|
33
|
+
exhausted?: boolean;
|
|
32
34
|
}
|
|
33
35
|
export interface ProbeToken {
|
|
34
36
|
readonly selector: string;
|
|
@@ -63,6 +65,7 @@ export declare class FallbackCircuitBreaker {
|
|
|
63
65
|
releaseOwnersWithPrefix(prefix: string): void;
|
|
64
66
|
close(selector: string): void;
|
|
65
67
|
has(selector: string): boolean;
|
|
68
|
+
isExhausted(selector: string): boolean;
|
|
66
69
|
sweep(now: number): void;
|
|
67
70
|
}
|
|
68
71
|
/**
|
|
@@ -80,6 +83,8 @@ export interface FallbackCircuitAccess {
|
|
|
80
83
|
/** Whether the breaker holds any state for the selector, i.e. it, not a session cooldown, governs recovery. */
|
|
81
84
|
governs(selector: string): boolean;
|
|
82
85
|
isOpen(selector: string, lane?: CircuitLane): boolean;
|
|
86
|
+
/** Whether the open circuit records a verdict every session should act on: the entry is spent. */
|
|
87
|
+
isExhausted(selector: string): boolean;
|
|
83
88
|
admit(selector: string, lane?: CircuitLane): CircuitAdmission;
|
|
84
89
|
release(token: ProbeToken): void;
|
|
85
90
|
releaseAll(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { parseRetryAfterMsMarker } from "@earendil-works/pi-ai/utils/retry-hint";
|
|
3
3
|
import { usageLimitReset } from "../credential-pool/reset-time.js";
|
|
4
|
+
import { isHealthExhaustionFailure } from "./circuit-probes.js";
|
|
4
5
|
import { usageLimitScope } from "./usage-limit.js";
|
|
5
6
|
/**
|
|
6
7
|
* Fallback-chain circuit breaker. An entry that fails out of a configured chain
|
|
@@ -34,8 +35,14 @@ export class FallbackCircuitBreaker {
|
|
|
34
35
|
}
|
|
35
36
|
open(selector, request) {
|
|
36
37
|
const previous = this.circuits.get(selector);
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// A failure while the circuit is still open (concurrent sessions failing together) belongs to
|
|
39
|
+
// the same open episode: it takes no backoff step. Only a failure once the open has elapsed, a
|
|
40
|
+
// failed half-open probe, advances it (senpi#3467).
|
|
41
|
+
const sameEpisode = previous !== undefined && request.now < previous.openUntil;
|
|
42
|
+
const consecutiveOpens = (previous?.consecutiveOpens ?? 0) + (sameEpisode ? 0 : 1);
|
|
43
|
+
const cooldownMs = sameEpisode
|
|
44
|
+
? previous.cooldownMs
|
|
45
|
+
: Math.min(request.cooldownMs * 2 ** (consecutiveOpens - 1), request.maxCooldownMs);
|
|
39
46
|
const retryAfterMs = request.retryAfterMs;
|
|
40
47
|
const hinted = retryAfterMs !== undefined && Number.isFinite(retryAfterMs) && retryAfterMs > 0
|
|
41
48
|
? request.now + retryAfterMs
|
|
@@ -53,7 +60,7 @@ export class FallbackCircuitBreaker {
|
|
|
53
60
|
const hintedUntil = Math.min(retryFloorUntil, request.now + request.maxCooldownMs);
|
|
54
61
|
const openUntil = resetWindowMs !== undefined && Number.isFinite(resetWindowMs) && resetWindowMs > 0
|
|
55
62
|
? Math.max(request.now + Math.min(resetWindowMs, request.maxCooldownMs), hintedUntil)
|
|
56
|
-
: Math.max(request.now + cooldownMs, hintedUntil);
|
|
63
|
+
: Math.max(sameEpisode ? previous.openUntil : request.now + cooldownMs, hintedUntil);
|
|
57
64
|
this.circuits.set(selector, {
|
|
58
65
|
openUntil,
|
|
59
66
|
retryFloorUntil,
|
|
@@ -61,6 +68,7 @@ export class FallbackCircuitBreaker {
|
|
|
61
68
|
cooldownMs,
|
|
62
69
|
maxCooldownMs: request.maxCooldownMs,
|
|
63
70
|
probe: undefined,
|
|
71
|
+
exhausted: request.exhausted === true || (sameEpisode && previous.exhausted),
|
|
64
72
|
});
|
|
65
73
|
return openUntil;
|
|
66
74
|
}
|
|
@@ -111,6 +119,9 @@ export class FallbackCircuitBreaker {
|
|
|
111
119
|
has(selector) {
|
|
112
120
|
return this.circuits.has(selector);
|
|
113
121
|
}
|
|
122
|
+
isExhausted(selector) {
|
|
123
|
+
return this.circuits.get(selector)?.exhausted === true;
|
|
124
|
+
}
|
|
114
125
|
sweep(now) {
|
|
115
126
|
for (const [selector, circuit] of this.circuits) {
|
|
116
127
|
if (!circuit.probe && now >= circuit.openUntil + circuit.maxCooldownMs)
|
|
@@ -137,10 +148,22 @@ export function createFallbackCircuitAccess(deps) {
|
|
|
137
148
|
: undefined;
|
|
138
149
|
const resetWindowMs = reset !== undefined && (reset.kind === "absolute" || reset.ms >= cooldownMs) ? reset.ms : undefined;
|
|
139
150
|
deps.breaker.sweep(now);
|
|
140
|
-
|
|
151
|
+
// Only a verdict that the entry is spent for everyone (billing, quota, an account usage limit) moves
|
|
152
|
+
// sessions that never failed on it. A slow request, an overload or a rate limit with a Retry-After is
|
|
153
|
+
// this session's view: siblings keep their model and find out on their own request (senpi#3467).
|
|
154
|
+
const exhausted = message !== undefined && (isHealthExhaustionFailure(message) || usageLimitScope(message) === "account");
|
|
155
|
+
const openUntil = deps.breaker.open(selector, {
|
|
156
|
+
now,
|
|
157
|
+
cooldownMs,
|
|
158
|
+
maxCooldownMs,
|
|
159
|
+
retryAfterMs,
|
|
160
|
+
resetWindowMs,
|
|
161
|
+
exhausted,
|
|
162
|
+
});
|
|
141
163
|
deps.logger.info("circuit_opened", { selector, durationMs: openUntil - now, retryAfterMs, resetWindowMs });
|
|
142
164
|
},
|
|
143
165
|
governs: (selector) => enabled() && deps.breaker.has(selector),
|
|
166
|
+
isExhausted: (selector) => enabled() && deps.breaker.isExhausted(selector),
|
|
144
167
|
isOpen: (selector, lane = "turn") => enabled() && deps.breaker.isOpen(selector, deps.now(), laneOwner(deps.owner(), lane)),
|
|
145
168
|
admit: (selector, lane = "turn") => enabled() ? deps.breaker.admit(selector, deps.now(), laneOwner(deps.owner(), lane)) : { kind: "closed" },
|
|
146
169
|
release: (token) => deps.breaker.release(token),
|
|
@@ -10,6 +10,7 @@ export declare class RetryFallbackController {
|
|
|
10
10
|
private lastExhaustedChainKey;
|
|
11
11
|
private heldForNetworkBlip;
|
|
12
12
|
private blipModel;
|
|
13
|
+
private readonly ownFailures;
|
|
13
14
|
private readonly unusable;
|
|
14
15
|
readonly probes: CircuitProbes;
|
|
15
16
|
private canonicalCache;
|
|
@@ -56,6 +57,8 @@ export declare class RetryFallbackController {
|
|
|
56
57
|
notifyCompactionApplied(): boolean;
|
|
57
58
|
/** A user-driven model change abandons the fallback window entirely. */
|
|
58
59
|
clearForManualModelChange(model: Model<Api>): void;
|
|
60
|
+
/** A response this session got from `selector` succeeded: the entry serves it again (senpi#3467). */
|
|
61
|
+
noteServed(selector: string): void;
|
|
59
62
|
/**
|
|
60
63
|
* Records a failure that makes the entry unusable for every session - billing
|
|
61
64
|
* or quota exhaustion, or a transient failure that spent its budget or failed a
|
|
@@ -63,10 +66,12 @@ export declare class RetryFallbackController {
|
|
|
63
66
|
*/
|
|
64
67
|
noteHealthFailure(model: Model<Api>, thinkingLevel: ThinkingLevel | undefined, failure: CircuitFailure): void;
|
|
65
68
|
/**
|
|
66
|
-
* Turn-boundary skip: when the current entry's circuit
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
69
|
+
* Turn-boundary skip: when the current entry's circuit is open because this session
|
|
70
|
+
* saw it fail (and has not been served by it since), or because its account is spent
|
|
71
|
+
* for everyone, move to the next closed entry without spending a request on it. A
|
|
72
|
+
* sibling's bare failure or Retry-After never moves this session (senpi#3467). With no
|
|
73
|
+
* closed entry left the current one stays and probes, so the chain never refuses a
|
|
74
|
+
* turn; a half-open current entry is claimed for this session.
|
|
70
75
|
*/
|
|
71
76
|
rerouteAroundOpenCircuit(): Promise<boolean>;
|
|
72
77
|
tryFallback(reason: FallbackReason, failure: {
|
|
@@ -11,6 +11,9 @@ export class RetryFallbackController {
|
|
|
11
11
|
this.triedSelectors = new Set();
|
|
12
12
|
// A fallback forced by a network blip serves out its turn; only the next turn boundary reverts it.
|
|
13
13
|
this.heldForNetworkBlip = false;
|
|
14
|
+
// Entries this session itself saw fail out of a chain: only these (or an entry spent for everyone)
|
|
15
|
+
// may move the session off its model at a turn boundary (senpi#3467).
|
|
16
|
+
this.ownFailures = new Set();
|
|
14
17
|
this.deps = deps;
|
|
15
18
|
this.probes = new CircuitProbes(deps.circuits);
|
|
16
19
|
this.unusable = new UnusableEntries(deps.cooldowns);
|
|
@@ -159,11 +162,17 @@ export class RetryFallbackController {
|
|
|
159
162
|
}
|
|
160
163
|
this.state = undefined;
|
|
161
164
|
this.heldForNetworkBlip = false;
|
|
165
|
+
this.ownFailures.delete(formatSelector(model));
|
|
162
166
|
this.deps.cooldowns.clearModel(formatSelector(model));
|
|
163
167
|
this.unusable.clear(model);
|
|
164
168
|
this.probes.release();
|
|
165
169
|
this.deps.circuits?.close(formatSelector(model));
|
|
166
170
|
}
|
|
171
|
+
/** A response this session got from `selector` succeeded: the entry serves it again (senpi#3467). */
|
|
172
|
+
noteServed(selector) {
|
|
173
|
+
this.ownFailures.delete(selector);
|
|
174
|
+
this.probes.accept(selector);
|
|
175
|
+
}
|
|
167
176
|
/**
|
|
168
177
|
* Records a failure that makes the entry unusable for every session - billing
|
|
169
178
|
* or quota exhaustion, or a transient failure that spent its budget or failed a
|
|
@@ -176,21 +185,29 @@ export class RetryFallbackController {
|
|
|
176
185
|
// A pure network fault is the transport's, not the model's: it never opens the shared circuit (senpi#3306).
|
|
177
186
|
if (isPureNetworkTransient(failure.errorMessage))
|
|
178
187
|
return;
|
|
188
|
+
this.ownFailures.add(formatSelector(model));
|
|
179
189
|
this.probes.noteFailure(formatSelector(model), failure);
|
|
180
190
|
}
|
|
181
191
|
/**
|
|
182
|
-
* Turn-boundary skip: when the current entry's circuit
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
192
|
+
* Turn-boundary skip: when the current entry's circuit is open because this session
|
|
193
|
+
* saw it fail (and has not been served by it since), or because its account is spent
|
|
194
|
+
* for everyone, move to the next closed entry without spending a request on it. A
|
|
195
|
+
* sibling's bare failure or Retry-After never moves this session (senpi#3467). With no
|
|
196
|
+
* closed entry left the current one stays and probes, so the chain never refuses a
|
|
197
|
+
* turn; a half-open current entry is claimed for this session.
|
|
186
198
|
*/
|
|
187
199
|
async rerouteAroundOpenCircuit() {
|
|
188
200
|
const current = this.deps.getCurrentSelector();
|
|
189
201
|
if (!this.deps.circuits || !current)
|
|
190
202
|
return false;
|
|
191
203
|
const currentBase = formatSelector(current.model);
|
|
204
|
+
// A half-open entry is still claimed as this session's single probe whoever opened it.
|
|
192
205
|
if (this.probes.admit(currentBase) !== "open")
|
|
193
206
|
return false;
|
|
207
|
+
// Another session's bare failure says this entry was slow for that session, not that it is
|
|
208
|
+
// down for this one: a session that never failed on it keeps its model and finds out itself.
|
|
209
|
+
if (!this.ownFailures.has(currentBase) && !this.probes.isExhausted(currentBase))
|
|
210
|
+
return false;
|
|
194
211
|
const candidate = this.nextCandidate(false, true);
|
|
195
212
|
if (!candidate || candidate.circuitOpen)
|
|
196
213
|
return false;
|