@sayknow-cli/coding-agent 0.5.26 → 0.6.1
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 +33 -0
- package/dist/types/config/settings-schema.d.ts +25 -5
- package/dist/types/decisions/keyword-learning.d.ts +61 -0
- package/dist/types/decisions/llm-backend.d.ts +13 -1
- package/dist/types/decisions/prompt-triage.d.ts +42 -0
- package/dist/types/decisions/skill-routing.d.ts +41 -6
- package/dist/types/hooks/native-prompt-routing.d.ts +21 -0
- package/dist/types/hooks/native-skill-hook.d.ts +3 -0
- package/dist/types/hooks/skill-keywords.d.ts +9 -0
- package/dist/types/hooks/skill-state.d.ts +20 -3
- package/dist/types/hooks/ui-skill-keywords.d.ts +15 -0
- package/dist/types/sdk/session.d.ts +3 -13
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/auth-storage-discovery.d.ts +13 -0
- package/dist/types/tools/browser.d.ts +2 -2
- package/package.json +7 -7
- package/scripts/eval-skill-routing.ts +37 -12
- package/src/config/model-profiles.ts +19 -1
- package/src/config/settings-schema.ts +27 -5
- package/src/decisions/index.ts +8 -2
- package/src/decisions/keyword-learning.ts +678 -0
- package/src/decisions/llm-backend.ts +213 -67
- package/src/decisions/prompt-triage.ts +163 -0
- package/src/decisions/skill-routing.ts +39 -56
- package/src/decisions/typesafe-backend.ts +3 -0
- package/src/hooks/native-prompt-routing.ts +190 -0
- package/src/hooks/native-skill-hook.ts +21 -12
- package/src/hooks/skill-keywords.ts +9 -0
- package/src/hooks/skill-state.ts +41 -10
- package/src/hooks/ui-skill-keywords.ts +67 -10
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/sdk/session.ts +5 -82
- package/src/session/agent-session.ts +137 -37
- package/src/session/auth-storage-discovery.ts +83 -0
|
@@ -203,7 +203,9 @@ import type { SettingPath } from "../config/settings-schema";
|
|
|
203
203
|
import { getDefault } from "../config/settings-schema";
|
|
204
204
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
205
205
|
import { createDecisionService } from "../decisions";
|
|
206
|
-
import {
|
|
206
|
+
import { loadLearnedKeywordDefinitions, observeRouting } from "../decisions/keyword-learning";
|
|
207
|
+
import { createPromptTriage, type PromptTriager } from "../decisions/prompt-triage";
|
|
208
|
+
import type { BundledSkcUiSkillName } from "../defaults/skc-ui-skills";
|
|
207
209
|
import { loadCapability } from "../discovery";
|
|
208
210
|
import { expandApplyPatchToEntries, normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
209
211
|
import { MAX_EDIT_FILE_BYTES } from "../edit/read-file";
|
|
@@ -267,7 +269,11 @@ import {
|
|
|
267
269
|
detectPrimarySkillKeyword,
|
|
268
270
|
ensureWorkflowSkillActivationState,
|
|
269
271
|
} from "../hooks/skill-state";
|
|
270
|
-
import {
|
|
272
|
+
import {
|
|
273
|
+
buildUiSkillActivationContext,
|
|
274
|
+
buildUiSkillDirectiveForSkill,
|
|
275
|
+
detectUiSkillKeywords,
|
|
276
|
+
} from "../hooks/ui-skill-keywords";
|
|
271
277
|
import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
272
278
|
import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
|
|
273
279
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
@@ -533,6 +539,14 @@ export interface AgentSessionConfig {
|
|
|
533
539
|
toolRegistry?: Map<string, AgentTool>;
|
|
534
540
|
/** Tool-session factory context used to lazily attach workflow-gate-only tools. */
|
|
535
541
|
workflowGateToolSession?: ToolSession;
|
|
542
|
+
/**
|
|
543
|
+
* Stage two of workflow routing: before a genuine user turn, ask a small model
|
|
544
|
+
* through the session's own transport which SKC workflow the prompt calls for.
|
|
545
|
+
* Hosts that own an interactive user (`createAgentSession`) turn this on; a bare
|
|
546
|
+
* session leaves it off so a scripted or injected transport is never consumed by a
|
|
547
|
+
* call the host did not script. `decisions.enabled` still governs the user side.
|
|
548
|
+
*/
|
|
549
|
+
semanticWorkflowRouting?: boolean;
|
|
536
550
|
/** Current session pre-LLM message transform pipeline */
|
|
537
551
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
538
552
|
/** Provider payload hook used by the active session request path */
|
|
@@ -1851,11 +1865,18 @@ export class AgentSession {
|
|
|
1851
1865
|
#modelRegistry: ModelRegistry;
|
|
1852
1866
|
|
|
1853
1867
|
/** Built on first use; the decision service resolves model and credential lazily. */
|
|
1854
|
-
#
|
|
1868
|
+
#promptTriager?: PromptTriager;
|
|
1869
|
+
|
|
1870
|
+
/**
|
|
1871
|
+
* UI skill the semantic stage picked for this turn, when the regex table did
|
|
1872
|
+
* not. Cleared per prompt: it describes one sentence, not the session.
|
|
1873
|
+
*/
|
|
1874
|
+
#semanticUiSkill?: BundledSkcUiSkillName;
|
|
1855
1875
|
|
|
1856
1876
|
// Tool registry and prompt builder for extensions
|
|
1857
1877
|
#toolRegistry: Map<string, AgentTool>;
|
|
1858
1878
|
#workflowGateToolSession: ToolSession | undefined;
|
|
1879
|
+
#semanticWorkflowRouting: boolean;
|
|
1859
1880
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
1860
1881
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
1861
1882
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
@@ -1979,6 +2000,12 @@ export class AgentSession {
|
|
|
1979
2000
|
nonEditDeterminations: 0,
|
|
1980
2001
|
};
|
|
1981
2002
|
#promptInFlightCount = 0;
|
|
2003
|
+
/**
|
|
2004
|
+
* A genuine user prompt is being routed to a workflow before its turn starts. Counts
|
|
2005
|
+
* as streaming so a second `prompt()` queues or rejects exactly as it would mid-turn,
|
|
2006
|
+
* and `abort()` cancels the routing through the preflight signal.
|
|
2007
|
+
*/
|
|
2008
|
+
#workflowRoutingInFlight = false;
|
|
1982
2009
|
#agentEventHandlersInFlight = 0;
|
|
1983
2010
|
#queuedExtensionEventCount = 0;
|
|
1984
2011
|
#extensionTurnGeneration = 0;
|
|
@@ -2405,6 +2432,7 @@ export class AgentSession {
|
|
|
2405
2432
|
}
|
|
2406
2433
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
2407
2434
|
this.#workflowGateToolSession = config.workflowGateToolSession;
|
|
2435
|
+
this.#semanticWorkflowRouting = config.semanticWorkflowRouting ?? false;
|
|
2408
2436
|
this.#requestedToolNames = config.requestedToolNames;
|
|
2409
2437
|
this.#explicitlyDisabledTools = config.explicitlyDisabledTools === true;
|
|
2410
2438
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
@@ -5497,7 +5525,7 @@ export class AgentSession {
|
|
|
5497
5525
|
|
|
5498
5526
|
/** Whether agent is currently streaming a response */
|
|
5499
5527
|
get isStreaming(): boolean {
|
|
5500
|
-
return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
|
|
5528
|
+
return this.agent.state.isStreaming || this.#promptInFlightCount > 0 || this.#workflowRoutingInFlight;
|
|
5501
5529
|
}
|
|
5502
5530
|
|
|
5503
5531
|
/** Wait until streaming and session settlement work are fully settled. */
|
|
@@ -7560,55 +7588,112 @@ export class AgentSession {
|
|
|
7560
7588
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
7561
7589
|
*/
|
|
7562
7590
|
/**
|
|
7563
|
-
*
|
|
7591
|
+
* Per-turn prompt triage: every routing question SKC asks about a user's
|
|
7592
|
+
* sentence, answered in one place.
|
|
7564
7593
|
*
|
|
7565
7594
|
* Runs in this process, not the hook process: the hook only receives paths and
|
|
7566
7595
|
* config, so it has no model registry and no credentials to call anything with.
|
|
7567
7596
|
*
|
|
7568
|
-
*
|
|
7569
|
-
*
|
|
7570
|
-
*
|
|
7571
|
-
*
|
|
7572
|
-
*
|
|
7573
|
-
*
|
|
7574
|
-
*
|
|
7597
|
+
* Three stages, cheapest first, and each one shrinks the next:
|
|
7598
|
+
*
|
|
7599
|
+
* 1. **Hand-written tables.** The eighteen workflow keywords and the twenty UI
|
|
7600
|
+
* regexes. Free, deterministic, measured at zero false positives, so they
|
|
7601
|
+
* are not gated behind the opt-in setting.
|
|
7602
|
+
* 2. **Learned patterns.** Two-stem rules mined from previous semantic answers
|
|
7603
|
+
* (`decisions/keyword-learning.ts`). Also free, also deterministic, and the
|
|
7604
|
+
* reason a phrasing the user repeats stops costing a model call.
|
|
7605
|
+
* 3. **One typed decision** for whatever stages 1 and 2 left unanswered. Both
|
|
7606
|
+
* questions ride the same call, so adding UI routing cost no round trip.
|
|
7575
7607
|
*
|
|
7576
|
-
*
|
|
7577
|
-
* the
|
|
7578
|
-
*
|
|
7579
|
-
*
|
|
7608
|
+
* Whatever stage three answers is fed back into stage two, which is what makes
|
|
7609
|
+
* the keyword table self-populating rather than a list somebody has to grow by
|
|
7610
|
+
* hand — the original list recalled 0/9 on Korean prompts for exactly that
|
|
7611
|
+
* reason.
|
|
7580
7612
|
*
|
|
7581
|
-
*
|
|
7582
|
-
*
|
|
7583
|
-
*
|
|
7613
|
+
* Keyword hits activate here rather than returning early. An earlier version
|
|
7614
|
+
* returned early on the assumption that the deterministic stage had already
|
|
7615
|
+
* activated the workflow, which holds only under the Codex host where `skc
|
|
7616
|
+
* codex-native-hook` runs on `UserPromptSubmit`. This session never fires that
|
|
7617
|
+
* hook, so the early return meant an enumerated keyword activated *nothing*.
|
|
7618
|
+
*
|
|
7619
|
+
* Deliberately best-effort — a disabled setting, a missing credential, a
|
|
7620
|
+
* timeout, a nonsense answer or low confidence all resolve to "no activation",
|
|
7621
|
+
* which is precisely the behaviour before this stage existed.
|
|
7584
7622
|
*/
|
|
7585
|
-
async #routeWorkflowSemantically(text: string): Promise<void> {
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
//
|
|
7589
|
-
//
|
|
7590
|
-
|
|
7623
|
+
async #routeWorkflowSemantically(text: string, signal: AbortSignal): Promise<void> {
|
|
7624
|
+
this.#semanticUiSkill = undefined;
|
|
7625
|
+
const learningEnabled = this.settings.get("decisions.keywordLearning");
|
|
7626
|
+
// Loaded per turn rather than cached on the session: a second session in the
|
|
7627
|
+
// same repo promotes patterns too, and a user who watched one get learned
|
|
7628
|
+
// expects the next prompt to use it, not the next restart.
|
|
7629
|
+
const learned = learningEnabled ? await loadLearnedKeywordDefinitions() : [];
|
|
7630
|
+
const keyword = detectPrimarySkillKeyword(text, learned);
|
|
7591
7631
|
if (keyword) {
|
|
7632
|
+
logger.debug("agent-session: workflow keyword match", {
|
|
7633
|
+
skill: keyword.skill,
|
|
7634
|
+
learned: keyword.learned === true,
|
|
7635
|
+
});
|
|
7592
7636
|
await this.#activateWorkflowSkill(keyword.skill);
|
|
7593
|
-
return;
|
|
7594
7637
|
}
|
|
7595
|
-
|
|
7596
|
-
//
|
|
7597
|
-
|
|
7638
|
+
// The regex table answers the UI question for free when it matches; asking
|
|
7639
|
+
// the model as well would pay for an answer already in hand.
|
|
7640
|
+
const uiMatched = detectUiSkillKeywords(text).length > 0;
|
|
7641
|
+
if (keyword && uiMatched) return;
|
|
7642
|
+
|
|
7643
|
+
// Stage three costs a model call: the host must have opted the session in and
|
|
7644
|
+
// the user must not have turned the setting (on by default) off.
|
|
7645
|
+
if (!this.#semanticWorkflowRouting || !this.settings.get("decisions.enabled")) return;
|
|
7646
|
+
// Only the model call counts as streaming. Flagging the keyword stage too made
|
|
7647
|
+
// `isStreaming` flicker true for one microtask on every prompt, which a poller
|
|
7648
|
+
// mistook for the turn having started.
|
|
7649
|
+
this.#workflowRoutingInFlight = true;
|
|
7598
7650
|
try {
|
|
7599
|
-
this.#
|
|
7651
|
+
this.#promptTriager ??= createPromptTriage(
|
|
7600
7652
|
createDecisionService({
|
|
7601
7653
|
registry: this.#modelRegistry,
|
|
7602
7654
|
settings: this.settings,
|
|
7603
7655
|
sessionId: this.sessionManager.getSessionId(),
|
|
7656
|
+
preferredProvider: () => this.model?.provider,
|
|
7657
|
+
// The turn's own transport, not a bare `completeSimple`: the same proxy,
|
|
7658
|
+
// credential-invalidation and retry wrapping the main request gets, and
|
|
7659
|
+
// a host that injects a stream function (tests, embedders) intercepts the
|
|
7660
|
+
// decision too instead of watching it go to the network behind its back.
|
|
7661
|
+
completeImpl: async (model, context, options) =>
|
|
7662
|
+
(await this.agent.streamFn(model, context, options)).result(),
|
|
7604
7663
|
enabled: true,
|
|
7605
7664
|
}),
|
|
7606
7665
|
);
|
|
7607
|
-
const
|
|
7608
|
-
|
|
7609
|
-
|
|
7666
|
+
const started = Date.now();
|
|
7667
|
+
const triage = await this.#promptTriager({
|
|
7668
|
+
text,
|
|
7669
|
+
skipWorkflow: Boolean(keyword),
|
|
7670
|
+
skipUiSkill: uiMatched,
|
|
7671
|
+
signal,
|
|
7672
|
+
});
|
|
7673
|
+
logger.debug("agent-session: prompt triage", {
|
|
7674
|
+
workflow: triage?.workflow ?? null,
|
|
7675
|
+
uiSkill: triage?.uiSkill ?? null,
|
|
7676
|
+
durationMs: Date.now() - started,
|
|
7677
|
+
});
|
|
7678
|
+
if (!triage || signal.aborted) return;
|
|
7679
|
+
if (triage.uiSkill) this.#semanticUiSkill = triage.uiSkill;
|
|
7680
|
+
if (keyword) return;
|
|
7681
|
+
if (triage.workflow) await this.#activateWorkflowSkill(triage.workflow);
|
|
7682
|
+
if (learningEnabled) {
|
|
7683
|
+
// Awaited, not fired and forgotten: an unawaited write racing the next
|
|
7684
|
+
// turn's read is how a learned pattern gets lost on the prompt that was
|
|
7685
|
+
// supposed to promote it. The store is a few KB and already in memory.
|
|
7686
|
+
await observeRouting({
|
|
7687
|
+
text,
|
|
7688
|
+
skill: triage.workflow,
|
|
7689
|
+
confidence: triage.workflowConfidence,
|
|
7690
|
+
calibrated: triage.calibrated,
|
|
7691
|
+
});
|
|
7692
|
+
}
|
|
7610
7693
|
} catch (error) {
|
|
7611
|
-
logger.debug("agent-session:
|
|
7694
|
+
logger.debug("agent-session: prompt triage failed", { error: String(error) });
|
|
7695
|
+
} finally {
|
|
7696
|
+
this.#workflowRoutingInFlight = false;
|
|
7612
7697
|
}
|
|
7613
7698
|
}
|
|
7614
7699
|
|
|
@@ -7701,7 +7786,12 @@ export class AgentSession {
|
|
|
7701
7786
|
// deep-interview with the setting off and ralplan with it on. Both are plausible
|
|
7702
7787
|
// readings and ralplan is the better one here, but the point is that enabling this
|
|
7703
7788
|
// can *change* an activation rather than only add one where there was none.
|
|
7704
|
-
if (claimsGenuineUserIntent && !this.isStreaming)
|
|
7789
|
+
if (claimsGenuineUserIntent && !this.isStreaming) {
|
|
7790
|
+
const routingGeneration = this.#promptGeneration;
|
|
7791
|
+
const routingSignal = this.#promptPreflightAbortController.signal;
|
|
7792
|
+
await this.#routeWorkflowSemantically(expandedText, routingSignal);
|
|
7793
|
+
this.#throwIfPromptPreflightCancelled(routingGeneration, routingSignal);
|
|
7794
|
+
}
|
|
7705
7795
|
|
|
7706
7796
|
// If streaming, queue via steer() or followUp() based on option
|
|
7707
7797
|
if (this.isStreaming) {
|
|
@@ -12033,12 +12123,22 @@ export class AgentSession {
|
|
|
12033
12123
|
* still there — while a wrong match would load a design skill onto a database task.
|
|
12034
12124
|
* That asymmetry is why a reminder is the right shape here and a forced tool call is
|
|
12035
12125
|
* not.
|
|
12126
|
+
*
|
|
12127
|
+
* The three frontend prompts the patterns missed are now covered by the typed
|
|
12128
|
+
* decision in `#routeWorkflowSemantically`, which rides the same call as workflow
|
|
12129
|
+
* routing and therefore costs nothing extra. The patterns stay in front of it: when
|
|
12130
|
+
* they match, the model is never asked.
|
|
12036
12131
|
*/
|
|
12037
12132
|
#createUiSkillPrelude(promptText: string): AgentMessage | undefined {
|
|
12038
12133
|
if (this.#planModeState?.enabled) return undefined;
|
|
12039
|
-
const
|
|
12134
|
+
const matched = buildUiSkillActivationContext(promptText);
|
|
12135
|
+
const directive =
|
|
12136
|
+
matched ?? (this.#semanticUiSkill ? buildUiSkillDirectiveForSkill(this.#semanticUiSkill) : null);
|
|
12040
12137
|
if (!directive) return undefined;
|
|
12041
|
-
logger.debug("agent-session: bundled UI skill matched", {
|
|
12138
|
+
logger.debug("agent-session: bundled UI skill matched", {
|
|
12139
|
+
promptChars: promptText.length,
|
|
12140
|
+
source: matched ? "pattern" : "semantic",
|
|
12141
|
+
});
|
|
12042
12142
|
return {
|
|
12043
12143
|
role: "developer",
|
|
12044
12144
|
content: [{ type: "text", text: `<system-reminder>\n${directive}\n</system-reminder>` }],
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential store discovery, on its own so a short-lived process can open the
|
|
3
|
+
* store without importing the SDK.
|
|
4
|
+
*
|
|
5
|
+
* `sdk/session.ts` re-exports `discoverAuthStorage` and remains the public entry
|
|
6
|
+
* point. Measured: importing `sdk/session` costs ~400ms of module graph; the
|
|
7
|
+
* Codex prompt hook runs once per prompt and already spends 260ms, so it opens
|
|
8
|
+
* credentials through this file instead.
|
|
9
|
+
*/
|
|
10
|
+
import { getAgentDbPath, getAgentDir } from "@sayknow-cli/utils";
|
|
11
|
+
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
12
|
+
import { resolveAuthBrokerConfig } from "./auth-broker-config";
|
|
13
|
+
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./auth-storage";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create an AuthStorage instance.
|
|
17
|
+
*
|
|
18
|
+
* Default: local SQLite store at `<agentDir>/agent.db`.
|
|
19
|
+
*
|
|
20
|
+
* Broker mode: when `SKC_AUTH_BROKER_URL` is set, credentials are pulled from
|
|
21
|
+
* a remote auth-broker over the wire. Refresh tokens never leave the broker;
|
|
22
|
+
* the client receives access tokens with `refresh = "__remote__"` and calls
|
|
23
|
+
* back into the broker through the {@link AuthStorageOptions.refreshOAuthCredential}
|
|
24
|
+
* override to re-mint access tokens when needed.
|
|
25
|
+
*/
|
|
26
|
+
export async function discoverAuthStorage(agentDir: string = getAgentDir()): Promise<AuthStorage> {
|
|
27
|
+
const brokerConfig = await resolveAuthBrokerConfig();
|
|
28
|
+
const credentialRankingMode = resolveCredentialRankingMode();
|
|
29
|
+
if (brokerConfig) {
|
|
30
|
+
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
31
|
+
const initialResult = await client.fetchSnapshot();
|
|
32
|
+
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
33
|
+
const store = new RemoteAuthCredentialStore({ client, initialSnapshot: initialResult.snapshot });
|
|
34
|
+
// Refresh + usage hooks live on RemoteAuthCredentialStore; AuthStorage
|
|
35
|
+
// discovers them automatically when no explicit option overrides them.
|
|
36
|
+
const storage = new AuthStorage(store, {
|
|
37
|
+
configValueResolver: resolveConfigValue,
|
|
38
|
+
sourceLabel: `broker ${brokerConfig.url}`,
|
|
39
|
+
credentialRankingMode,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await storage.reload();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
try {
|
|
45
|
+
storage.close();
|
|
46
|
+
} catch {
|
|
47
|
+
// Preserve the initial reload failure.
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return storage;
|
|
52
|
+
}
|
|
53
|
+
const dbPath = getAgentDbPath(agentDir);
|
|
54
|
+
const storage = await AuthStorage.create(dbPath, {
|
|
55
|
+
configValueResolver: resolveConfigValue,
|
|
56
|
+
sourceLabel: `local ${dbPath}`,
|
|
57
|
+
credentialRankingMode,
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
await storage.reload();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
try {
|
|
63
|
+
storage.close();
|
|
64
|
+
} catch {
|
|
65
|
+
// Preserve the initial reload failure.
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
return storage;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Opt-in multi-account credential ranking mode, read from the
|
|
74
|
+
* `SKC_CREDENTIAL_RANKING_MODE` env var. Unset/unknown → `undefined`, leaving
|
|
75
|
+
* {@link AuthStorage}'s default (`balanced`) untouched. `earliest-reset`
|
|
76
|
+
* switches to earliest-expiry-first selection so soon-to-reset tumbling-window
|
|
77
|
+
* quota is drained before it is lost.
|
|
78
|
+
*/
|
|
79
|
+
function resolveCredentialRankingMode(): "balanced" | "earliest-reset" | undefined {
|
|
80
|
+
const raw = process.env.SKC_CREDENTIAL_RANKING_MODE?.trim();
|
|
81
|
+
if (raw === "balanced" || raw === "earliest-reset") return raw;
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|