diff --git a/src/agent/credentials.ts b/src/agent/credentials.ts index 223798e6..5e553f73 100644 --- a/src/agent/credentials.ts +++ b/src/agent/credentials.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; import JSON5 from "json5"; @@ -90,6 +90,8 @@ export class CredentialManager { private coreConfig: CredentialsConfig | null = null; private skillsConfig: SkillsEnvConfig | null = null; private resolvedSkillsEnv: Record | null = null; + private coreMtimeMs: number | null = null; + private skillsMtimeMs: number | null = null; private isDisabled(): boolean { if (process.env.SMC_CREDENTIALS_DISABLE === "1") return true; @@ -99,17 +101,32 @@ export class CredentialManager { private loadCore(): void { const path = getCredentialsPath(); const disabled = this.isDisabled(); + let mtimeMs: number | null = null; - if (this.corePath === path && this.disabledState === disabled && this.coreConfig) { + if (!disabled && existsSync(path)) { + try { + mtimeMs = statSync(path).mtimeMs; + } catch { + mtimeMs = null; + } + } + + if ( + this.corePath === path + && this.disabledState === disabled + && this.coreConfig + && this.coreMtimeMs === mtimeMs + ) { return; } this.corePath = path; this.disabledState = disabled; this.coreConfig = null; + this.coreMtimeMs = mtimeMs; if (disabled) return; - if (!existsSync(path)) return; + if (mtimeMs === null) return; const raw = readFileSync(path, "utf8"); try { @@ -123,8 +140,22 @@ export class CredentialManager { private loadSkillsEnv(): void { const path = getSkillsEnvPath(); const disabled = this.isDisabled(); + let mtimeMs: number | null = null; - if (this.skillsPath === path && this.disabledState === disabled && this.resolvedSkillsEnv) { + if (!disabled && existsSync(path)) { + try { + mtimeMs = statSync(path).mtimeMs; + } catch { + mtimeMs = null; + } + } + + if ( + this.skillsPath === path + && this.disabledState === disabled + && this.resolvedSkillsEnv + && this.skillsMtimeMs === mtimeMs + ) { return; } @@ -132,9 +163,10 @@ export class CredentialManager { this.disabledState = disabled; this.skillsConfig = null; this.resolvedSkillsEnv = null; + this.skillsMtimeMs = mtimeMs; if (disabled) return; - if (!existsSync(path)) return; + if (mtimeMs === null) return; const raw = readFileSync(path, "utf8"); try { @@ -228,6 +260,8 @@ export class CredentialManager { this.coreConfig = null; this.skillsConfig = null; this.resolvedSkillsEnv = null; + this.coreMtimeMs = null; + this.skillsMtimeMs = null; } /** diff --git a/src/agent/providers/registry.ts b/src/agent/providers/registry.ts index cd406482..2ea1f93a 100644 --- a/src/agent/providers/registry.ts +++ b/src/agent/providers/registry.ts @@ -264,7 +264,7 @@ export function getLoginInstructions(providerId: string): string { if (info.authMethod === "oauth") { if (info.loginCommand) { - return `Run: ${info.loginCommand}\nThen restart Super Multica to use the credentials.`; + return `Run: ${info.loginCommand}\nThen retry in Super Multica to use the credentials.`; } } diff --git a/src/agent/runner.ts b/src/agent/runner.ts index 65fd528b..9b732e91 100644 --- a/src/agent/runner.ts +++ b/src/agent/runner.ts @@ -95,6 +95,7 @@ export class Agent { private profileCandidates: string[]; private profileIndex: number; private readonly pinnedProfile: boolean; + private readonly explicitApiKey: boolean; /** Current session ID */ readonly sessionId: string; @@ -126,6 +127,7 @@ export class Agent { // === Auth profile resolution === this.pinnedProfile = !!(options.authProfileId || options.apiKey); + this.explicitApiKey = !!options.apiKey; if (options.apiKey) { // Explicit API key — no rotation @@ -159,11 +161,9 @@ export class Agent { : 0; } - this.agent = new PiAgentCore( - this.currentApiKey - ? { getApiKey: (_provider: string) => this.currentApiKey! } - : {}, - ); + this.agent = new PiAgentCore({ + getApiKey: (_provider: string) => this.currentApiKey ?? "", + }); // Load Agent Profile (if profileId is specified) // Every Agent should have a Profile for memory, tools config, and other settings @@ -354,6 +354,10 @@ export class Agent { async run(prompt: string): Promise { await this.ensureInitialized(); + this.refreshAuthState(); + if (!this.currentApiKey) { + throw new Error(`No API key configured for provider: ${this.resolvedProvider}`); + } this.output.state.lastAssistantText = ""; const canRotate = !this.pinnedProfile && this.profileCandidates.length > 1; @@ -433,12 +437,67 @@ export class Agent { this.currentApiKey = apiKey; this.currentProfileId = candidateId; this.profileIndex = nextIndex; + this.updateSessionApiKey(); return true; } return false; } + private refreshAuthState(): void { + if (this.explicitApiKey) { + return; + } + + const store = loadAuthProfileStore(); + + if (this.pinnedProfile) { + const profileId = this.currentProfileId ?? this.resolvedProvider; + this.currentApiKey = resolveApiKeyForProfile(profileId) ?? resolveApiKey(this.resolvedProvider); + this.currentProfileId = profileId; + this.profileCandidates = []; + this.profileIndex = 0; + this.updateSessionApiKey(); + return; + } + + const candidates = resolveAuthProfileOrder(this.resolvedProvider, store); + this.profileCandidates = candidates; + + if (this.currentProfileId) { + const currentIndex = candidates.indexOf(this.currentProfileId); + if (currentIndex >= 0) { + const stats = store.usageStats?.[this.currentProfileId]; + if (!stats || !isProfileInCooldown(stats)) { + const apiKey = resolveApiKeyForProfile(this.currentProfileId); + if (apiKey) { + this.currentApiKey = apiKey; + this.profileIndex = currentIndex; + this.updateSessionApiKey(); + return; + } + } + } + } + + const resolved = resolveApiKeyForProvider(this.resolvedProvider); + if (resolved) { + this.currentApiKey = resolved.apiKey; + this.currentProfileId = resolved.profileId; + this.profileIndex = Math.max(0, candidates.indexOf(resolved.profileId)); + } else { + this.currentApiKey = undefined; + this.currentProfileId = undefined; + this.profileIndex = 0; + } + this.updateSessionApiKey(); + } + + private updateSessionApiKey(): void { + if (this.session.getCompactionMode() !== "summary") return; + this.session.setApiKey(this.currentApiKey); + } + private handleSessionEvent(event: AgentEvent) { if (event.type === "message_end") { const message = event.message as AgentMessage; @@ -680,6 +739,8 @@ export class Agent { throw new Error(`No API key configured for provider: ${providerId}`); } + this.updateSessionApiKey(); + // Update the agent's model and API key const baseUrl = resolveBaseUrl(actualProvider); const modelWithBaseUrl = baseUrl ? { ...model, baseUrl } : model;