@openadapter/koda 1.0.0-beta.15 → 1.0.0-beta.17

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.
@@ -22,7 +22,7 @@ export declare function runInteractiveImport(opts?: {
22
22
  * automatically get newly-added models without re-running setup. Best-effort and
23
23
  * throttled (24h); KODA_NO_MODEL_REFRESH=1 or KODA_OFFLINE disables.
24
24
  */
25
- export declare function maybeRefreshOpenAdapterModels(agentDir: string): Promise<void>;
25
+ export declare function maybeRefreshOpenAdapterModels(agentDir: string, maxAgeMs?: number): Promise<boolean>;
26
26
  /** `koda import` — import sessions from other CLIs any time. */
27
27
  export declare function handleImportCommand(args: string[]): Promise<boolean>;
28
28
  /** Run the bundled wizard interactively. Returns true on success. */
@@ -1,4 +1,4 @@
1
- var y=Object.defineProperty;var n=(t,e)=>y(t,"name",{value:e,configurable:!0});import{spawnSync as h}from"node:child_process";import{existsSync as S,readFileSync as p,writeFileSync as g}from"node:fs";import{dirname as w,join as i}from"node:path";import{createInterface as O}from"node:readline/promises";import{fileURLToPath as $}from"node:url";import u from"chalk";import{getPackageDir as x}from"../config.js";import{detectImportable as A,importExternalSessions as C}from"./import-sessions.js";async function b(t,e=!0){const r=O({input:process.stdin,output:process.stdout});try{const o=(await r.question(`${t} ${e?"[Y/n]":"[y/N]"} `)).trim().toLowerCase();return o?o==="y"||o==="yes":e}finally{r.close()}}n(b,"askYesNo");async function f(t){let e;try{e=await A()}catch{return}if(e.claude+e.opencode===0){t?.auto||console.log("No new sessions to import from Claude Code or OpenCode.");return}const o=[e.claude?`${e.claude} from Claude Code`:"",e.opencode?`${e.opencode} from OpenCode`:""].filter(Boolean).join(" and ");if(!await b(u.bold(`Import ${o} into Koda so all your history is here?`)))return;process.stdout.write("Importing\u2026 ");const s=await C({onProgress:n((l,c)=>{process.stdout.write(`\rImporting\u2026 ${l}/${c} `)},"onProgress")});process.stdout.write("\r"),console.log(`\u2713 Imported ${s.imported} session(s) (${s.perSource.claude} Claude Code, ${s.perSource.opencode} OpenCode)${s.errors?`, ${s.errors} skipped`:""}. Find them with \`koda --resume\`.`)}n(f,"runInteractiveImport");async function E(t){if(process.env.KODA_NO_MODEL_REFRESH==="1"||process.env.KODA_OFFLINE)return;const e=i(t,"models.json");let r;try{r=JSON.parse(p(e,"utf-8"))}catch{return}const o=r?.providers?.openadapter;if(!o?.apiKey||!o?.baseUrl)return;const d=typeof o.modelsRefreshedAt=="number"?o.modelsRefreshedAt:0;if(!(Date.now()-d<1440*60*1e3))try{const s=await fetch(`${String(o.baseUrl).replace(/\/$/,"")}/models`,{headers:{Authorization:`Bearer ${o.apiKey}`},signal:AbortSignal.timeout(5e3)});if(!s.ok)return;const c=((await s.json()).data??[]).filter(a=>(a.endpoint_format||a.model_type)==="chat");if(c.length===0)return;o.models=c.map(a=>({id:a.id,name:a.id,input:a.supports_vision?["text","image"]:["text"],maxTokens:16e3})),o.modelsRefreshedAt=Date.now(),g(e,`${JSON.stringify(r,null,2)}
2
- `,"utf-8")}catch{}}n(E,"maybeRefreshOpenAdapterModels");async function J(t){return t[0]!=="import"?!1:(await f(),!0)}n(J,"handleImportCommand");function I(){const t=w($(import.meta.url)),e=[i(x(),"openadapter","setup.mjs"),i(t,"..","..","openadapter","setup.mjs"),i(t,"..","..","..","..","scripts","setup-openadapter.mjs")];for(const r of e)if(S(r))return r;return null}n(I,"findSetupScript");function m(t=[]){const e=I();return e?h(process.execPath,[e,...t],{stdio:"inherit"}).status===0:(console.error(u.red("Koda setup script not found in this install.")),!1)}n(m,"runOpenAdapterSetup");async function L(t){if(t[0]!=="setup")return!1;const e=t.slice(1).find(r=>r.startsWith("sk-"));return m(e?[e]:[]),!0}n(L,"handleSetupCommand");function K(t){try{if(JSON.parse(p(i(t,"settings.json"),"utf-8"))?.defaultProvider==="openadapter")return!1}catch{}try{if(JSON.parse(p(i(t,"models.json"),"utf-8"))?.providers?.openadapter?.apiKey)return!1}catch{}return!0}n(K,"needsOpenAdapterSetup");async function U(t){if(process.env.KODA_SKIP_SETUP!=="1"&&K(t)){console.log(u.bold(`
1
+ var g=Object.defineProperty;var s=(t,e)=>g(t,"name",{value:e,configurable:!0});import{spawnSync as O}from"node:child_process";import{existsSync as w,readFileSync as d,writeFileSync as $}from"node:fs";import{dirname as A,join as c}from"node:path";import{createInterface as x}from"node:readline/promises";import{fileURLToPath as C}from"node:url";import l from"chalk";import{getPackageDir as b}from"../config.js";import{detectImportable as I,importExternalSessions as N}from"./import-sessions.js";async function K(t,e=!0){const r=x({input:process.stdin,output:process.stdout});try{const a=(await r.question(`${t} ${e?"[Y/n]":"[y/N]"} `)).trim().toLowerCase();return a?a==="y"||a==="yes":e}finally{r.close()}}s(K,"askYesNo");async function y(t){let e;try{e=await I()}catch{return}if(e.claude+e.opencode===0){t?.auto||console.log("No new sessions to import from Claude Code or OpenCode.");return}const a=[e.claude?`${e.claude} from Claude Code`:"",e.opencode?`${e.opencode} from OpenCode`:""].filter(Boolean).join(" and ");if(!await K(l.bold(`Import ${a} into Koda so all your history is here?`)))return;process.stdout.write("Importing\u2026 ");const i=await N({onProgress:s((p,f)=>{process.stdout.write(`\rImporting\u2026 ${p}/${f} `)},"onProgress")});process.stdout.write("\r"),console.log(`\u2713 Imported ${i.imported} session(s) (${i.perSource.claude} Claude Code, ${i.perSource.opencode} OpenCode)${i.errors?`, ${i.errors} skipped`:""}. Find them with \`koda --resume\`.`)}s(y,"runInteractiveImport");async function U(t,e=1440*60*1e3){if(process.env.KODA_NO_MODEL_REFRESH==="1"||process.env.KODA_OFFLINE)return!1;const r=c(t,"models.json");let a;try{a=JSON.parse(d(r,"utf-8"))}catch{return!1}const o=a?.providers?.openadapter;if(!o?.apiKey||!o?.baseUrl)return!1;const i=typeof o.modelsRefreshedAt=="number"?o.modelsRefreshedAt:0;if(Date.now()-i<e)return!1;try{const p=await fetch(`${String(o.baseUrl).replace(/\/$/,"")}/models`,{headers:{Authorization:`Bearer ${o.apiKey}`},signal:AbortSignal.timeout(5e3)});if(!p.ok)return!1;const m=((await p.json()).data??[]).filter(n=>(n.endpoint_format||n.model_type)==="chat");if(m.length===0)return!1;const u=m.map(n=>({id:n.id,name:n.id,input:n.supports_vision?["text","image"]:["text"],maxTokens:16e3})),S=Array.isArray(o.models)&&o.models.length===u.length&&JSON.stringify(o.models.map(n=>n.id).sort())===JSON.stringify(u.map(n=>n.id).sort());return o.models=u,o.modelsRefreshedAt=Date.now(),$(r,`${JSON.stringify(a,null,2)}
2
+ `,"utf-8"),!S}catch{}return!1}s(U,"maybeRefreshOpenAdapterModels");async function T(t){return t[0]!=="import"?!1:(await y(),!0)}s(T,"handleImportCommand");function j(){const t=A(C(import.meta.url)),e=[c(b(),"openadapter","setup.mjs"),c(t,"..","..","openadapter","setup.mjs"),c(t,"..","..","..","..","scripts","setup-openadapter.mjs")];for(const r of e)if(w(r))return r;return null}s(j,"findSetupScript");function h(t=[]){const e=j();return e?O(process.execPath,[e,...t],{stdio:"inherit"}).status===0:(console.error(l.red("Koda setup script not found in this install.")),!1)}s(h,"runOpenAdapterSetup");async function B(t){if(t[0]!=="setup")return!1;const e=t.slice(1).find(r=>r.startsWith("sk-"));return h(e?[e]:[]),!0}s(B,"handleSetupCommand");function k(t){try{if(JSON.parse(d(c(t,"settings.json"),"utf-8"))?.defaultProvider==="openadapter")return!1}catch{}try{if(JSON.parse(d(c(t,"models.json"),"utf-8"))?.providers?.openadapter?.apiKey)return!1}catch{}return!0}s(k,"needsOpenAdapterSetup");async function M(t){if(process.env.KODA_SKIP_SETUP!=="1"&&k(t)){console.log(l.bold(`
3
3
  Welcome to Koda \u2014 let's connect it to OpenAdapter (one-time).
4
- `)),m([]),console.log("");try{await f({auto:!0})}catch{}}}n(U,"maybeFirstRunSetup");export{J as handleImportCommand,L as handleSetupCommand,U as maybeFirstRunSetup,E as maybeRefreshOpenAdapterModels,K as needsOpenAdapterSetup,f as runInteractiveImport,m as runOpenAdapterSetup};
4
+ `)),h([]),console.log("");try{await y({auto:!0})}catch{}}}s(M,"maybeFirstRunSetup");export{T as handleImportCommand,B as handleSetupCommand,M as maybeFirstRunSetup,U as maybeRefreshOpenAdapterModels,k as needsOpenAdapterSetup,y as runInteractiveImport,h as runOpenAdapterSetup};
@@ -1,32 +1,32 @@
1
- var J=Object.defineProperty;var f=(E,e)=>J(E,"name",{value:e,configurable:!0});import{existsSync as k,mkdirSync as D,readFileSync as T,unlinkSync as Q,writeFileSync as L}from"node:fs";import{homedir as Y}from"node:os";import{basename as Z,dirname as U,join as C,relative as X,resolve as K}from"node:path";import{clampThinkingLevel as ee,cleanupSessionResources as te,getSupportedThinkingLevels as se,isContextOverflow as $,modelsAreEqual as S,resetApiProviders as ne,streamSimple as B}from"@openadapter/koda-ai";import{theme as ie}from"../modes/interactive/theme/theme.js";import{stripFrontmatter as F}from"../utils/frontmatter.js";import{sendKodaTelemetry as re}from"../utils/koda-telemetry.js";import{resolvePath as oe}from"../utils/paths.js";import{sleep as ae}from"../utils/sleep.js";import{formatNoApiKeyFoundMessage as O,formatNoModelSelectedMessage as H}from"./auth-guidance.js";import{executeBashWithOperations as le}from"./bash-executor.js";import{calculateContextTokens as z,collectEntriesForBranchSummary as ce,compact as j,estimateContextTokens as G,generateBranchSummary as he,prepareCompaction as W,shouldCompact as de}from"./compaction/index.js";import{DEFAULT_THINKING_LEVEL as ue}from"./defaults.js";import{execCommand as me}from"./exec.js";import{exportSessionToHtml as ge}from"./export-html/index.js";import{createToolHtmlRenderer as pe}from"./export-html/tool-renderer.js";import{ExtensionRunner as fe,wrapRegisteredTools as q}from"./extensions/index.js";import{emitSessionShutdownEvent as _e}from"./extensions/runner.js";import{expandPromptTemplate as N}from"./prompt-templates.js";import{CURRENT_SESSION_VERSION as ye,getLatestCompactionEntry as V}from"./session-manager.js";import{createSyntheticSourceInfo as I}from"./source-info.js";import{buildSystemPrompt as we}from"./system-prompt.js";import{createLocalBashOperations as be}from"./tools/bash.js";import{createAllToolDefinitions as xe}from"./tools/index.js";import{createToolDefinitionFromAgentTool as ve}from"./tools/tool-definition-wrapper.js";function Qe(E){const e=E.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);return e?{name:e[1],location:e[2],content:e[3],userMessage:e[4]?.trim()||void 0}:null}f(Qe,"parseSkillBlock");const Ae=["off","minimal","low","medium","high"];class Ye{static{f(this,"AgentSession")}agent;sessionManager;settingsManager;_scopedModels;_unsubscribeAgent;_eventListeners=[];_steeringMessages=[];_followUpMessages=[];_pendingNextTurnMessages=[];_compactionAbortController=void 0;_autoCompactionAbortController=void 0;_overflowRecoveryAttempted=!1;_branchSummaryAbortController=void 0;_retryAbortController=void 0;_retryAttempt=0;_overseerAbortController=void 0;_recapAbortController=void 0;_turnActive=!1;_failoverTried=new Set;get _zenitsu(){return process.env.KODA_ZENITSU==="1"}_zenitsuPreferred;_zenitsuCooldown=new Map;_zenitsuHealthCache;_overseerRounds=0;_currentGoal;_tierCache;_overseerLastError="";_escalationsDone=0;_escalateTried=new Set;_sessionEscalations=0;_stickyModel;_homeModel;_sessionGoal;_pinnedSkills=new Map;_codeChangedThisTask=!1;_verifyCmd;_taskStartTs=0;_taskHadActivity=!1;_suggestShown=new Set;_suggestedThisTask=!1;_harnessTrace=[];_gatewayCircuit={failures:0,openUntil:0};_bashAbortController=void 0;_pendingBashMessages=[];_extensionRunner;_turnIndex=0;_resourceLoader;_customTools;_baseToolDefinitions=new Map;_cwd;_protectedSnap=new Map;_extensionRunnerRef;_initialActiveToolNames;_allowedToolNames;_excludedToolNames;_baseToolsOverride;_sessionStartEvent;_extensionUIContext;_extensionMode="print";_extensionCommandContextActions;_extensionAbortHandler;_extensionShutdownHandler;_extensionErrorListener;_extensionErrorUnsubscriber;_modelRegistry;_toolRegistry=new Map;_toolDefinitions=new Map;_toolPromptSnippets=new Map;_toolPromptGuidelines=new Map;_baseSystemPrompt="";_baseSystemPromptOptions;constructor(e){this.agent=e.agent,this.sessionManager=e.sessionManager,this.settingsManager=e.settingsManager,this._scopedModels=e.scopedModels??[],this._resourceLoader=e.resourceLoader,this._customTools=e.customTools??[],this._cwd=e.cwd,this._modelRegistry=e.modelRegistry,this._extensionRunnerRef=e.extensionRunnerRef,this._initialActiveToolNames=e.initialActiveToolNames,this._allowedToolNames=e.allowedToolNames?new Set(e.allowedToolNames):void 0,this._excludedToolNames=e.excludedToolNames?new Set(e.excludedToolNames):void 0,this._baseToolsOverride=e.baseToolsOverride,this._sessionStartEvent=e.sessionStartEvent??{type:"session_start",reason:"startup"},this._unsubscribeAgent=this.agent.subscribe(this._handleAgentEvent),this._installAgentToolHooks(),this._buildRuntime({activeToolNames:this._initialActiveToolNames,includeAllExtensionTools:!0})}get modelRegistry(){return this._modelRegistry}async _getRequiredRequestAuth(e){const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(!t.ok)throw t.error.startsWith("No API key found")?new Error(O(e.provider)):new Error(t.error);if(t.apiKey)return{apiKey:t.apiKey,headers:t.headers};throw this._modelRegistry.isUsingOAuth(e)?new Error(`Authentication failed for "${e.provider}". Credentials may have expired or network is unavailable. Run '/login ${e.provider}' to re-authenticate.`):new Error(O(e.provider))}async _getCompactionRequestAuth(e){if(this.agent.streamFn===B)return this._getRequiredRequestAuth(e);const t=await this._modelRegistry.getApiKeyAndHeaders(e);return t.ok?{apiKey:t.apiKey,headers:t.headers}:{}}_installAgentToolHooks(){const e=process.env.KODA_UNSAFE==="1",t=process.cwd(),s=[/(^|\/)test_[^/]*\.[A-Za-z0-9]+$/,/(^|\/)[^/]*_test\.[A-Za-z0-9]+$/,/\.(test|spec)\.[A-Za-z0-9]+$/,/(^|\/)(tests?|__tests__|spec)\//,/(^|\/)\.git(\/|$)/,/(^|\/)\.env(\.[^/]*)?$/,/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Cargo\.lock)$/],n=(process.env.KODA_PROTECT||"").split(",").map(g=>g.trim()).filter(Boolean),i=f(g=>{const p=g.replace(/\\/g,"/");return s.some(_=>_.test(p))||n.some(_=>p.includes(_))},"isProtected"),o=f(g=>{for(const p of g.matchAll(/(?:>>?|\btee\s+(?:-a\s+)?)\s*([^\s;|&>]+)/g))if(i(p[1]))return`output redirect onto protected file '${p[1]}'`;return/\brm\s+(?:-\w+\s+)*[^\n;|&]*(?:test_|tests?\/|__tests__|\.git|\.env)/.test(g)?"rm targeting a protected path":/\bgit\s+(?:reset\s+--hard|clean\b|checkout\s+(?:--\s+)?\.)/.test(g)?"destructive git command":null},"dangerousBash"),r=C(process.env.KODA_CODING_AGENT_DIR||C(Y(),".koda","agent"),"checkpoints",String(this.sessionId||"session")),a=new Set,l=f(g=>{if(!a.has(g)){a.add(g);try{D(r,{recursive:!0});const p=C(r,`${g.replace(/[^A-Za-z0-9]/g,"_")}.json`),_=k(g);L(p,JSON.stringify({path:g,existed:_,content:_?T(g,"utf-8"):null}))}catch{}}},"backup");this.agent.beforeToolCall=async({toolCall:g,args:p})=>{if((g.name==="write"||g.name==="edit")&&(this._codeChangedThisTask=!0),this._taskHadActivity=!0,g.name==="bash"){const x=String(p?.command??"");/\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|preview|watch)\b|\bvite\b|\bnext\s+(dev|start)\b|\bnuxt\s+dev\b|\bnodemon\b|http[-.]server\b|\bserve\b|flask\s+run|rails\s+s(erver)?\b|manage\.py\s+runserver|\b(uvicorn|gunicorn)\b/i.test(x)&&this._suggest("bg","That looks like a long-running server. Use run_background (or /bg) so it keeps running without blocking Koda.")}if(process.env.KODA_MODE==="plan"){const x=p,v=g.name;if(v==="write"||v==="edit")return{block:!0,reason:"Koda is in PLAN MODE \u2014 file changes are disabled. Investigate and present a step-by-step plan instead; the user will run /build to execute it."};if(v==="bash"&&this._isMutatingBash(String(x?.command??"")))return{block:!0,reason:"Koda is in PLAN MODE \u2014 commands that change state are disabled. Use read-only commands to investigate, then present a plan; the user will run /build to execute it."}}if(!e){const x=p,v=g.name;if(v==="write"||v==="edit"){const y=String(x?.path??x?.file_path??"");if(y){const A=K(t,y);if(X(t,A).startsWith(".."))return{block:!0,reason:`Refused: '${y}' is outside the workspace. Only modify files under the project directory.`};if(i(y))return{block:!0,reason:`Refused: '${y}' is a protected file (tests / .git / secrets / lockfiles). Do not modify it \u2014 change the source code instead.`};l(A)}}else if(v==="bash"){const y=o(String(x?.command??""));if(y)return{block:!0,reason:`Refused: ${y}. Use the edit/write tools on source files instead of shell redirects, and never touch tests/.git/secrets.`}}const M=[];if(v==="write"||v==="edit"){const y=String(x?.path??x?.file_path??"");y&&M.push(y)}else if(v==="bash")for(const y of String(x?.command??"").matchAll(/[\w./-]*[\w]\.[A-Za-z0-9]+/g))M.push(y[0]);for(const y of M){if(!i(y))continue;const A=K(t,y);if(!this._protectedSnap.has(A))try{this._protectedSnap.set(A,k(A)?T(A,"utf-8"):null)}catch{}}}const _=this._extensionRunner;if(_.hasHandlers("tool_call"))try{return await _.emitToolCall({type:"tool_call",toolName:g.name,toolCallId:g.id,input:p})}catch(x){throw x instanceof Error?x:new Error(`Extension failed, blocking execution: ${String(x)}`)}};const c=process.env.KODA_CASCADE_MODEL,h=Number(process.env.KODA_CASCADE_ERRORS)||3,u=Number(process.env.KODA_CASCADE_STEPS)||16;let m=0,w=0,d=!1,b;this.agent.afterToolCall=async({toolCall:g,args:p,result:_,isError:x})=>{for(const[y,A]of this._protectedSnap){let P=null;try{P=k(y)?T(y,"utf-8"):null}catch{continue}if(P!==A)try{A===null?k(y)&&Q(y):L(y,A,"utf-8"),console.error(`[koda safety] reverted unauthorized change to protected file ${y}`)}catch{}}if(c&&!d&&(w++,x?m++:m=0,m>=h||w>=u)){const y=this.agent.state.model,[A,P]=c.includes("/")?[c.slice(0,c.indexOf("/")),c.slice(c.indexOf("/")+1)]:[y.provider,c],R=this._modelRegistry.find(A,P);R&&!S(R,y)&&(b=R,this.agent.state.model=R,this.sessionManager.appendModelChange(R.provider,R.id),this._emitModelSelect(R,y,"cycle"),d=!0,console.error(`[koda cascade] escalated ${y.provider}/${y.id} \u2192 ${R.provider}/${R.id} (errors=${m}, toolCalls=${w})`))}const v=this._extensionRunner;if(!v.hasHandlers("tool_result"))return;const M=await v.emitToolResult({type:"tool_result",toolName:g.name,toolCallId:g.id,input:p,content:_.content,details:_.details,isError:x});if(M)return{content:M.content,details:M.details,isError:M.isError??x}},this.agent.prepareNextTurn=async()=>{if(this._zenitsu){const g=await this._zenitsuPickModel();if(g&&!S(g,this.agent.state.model))return{model:g}}return d&&b?{model:b}:void 0}}_emit(e){for(const t of this._eventListeners)t(e)}_emitQueueUpdate(){this._emit({type:"queue_update",steering:[...this._steeringMessages],followUp:[...this._followUpMessages]})}_lastAssistantMessage=void 0;_handleAgentEvent=f(async e=>{if(e.type==="message_start"&&e.message.role==="user"){this._overflowRecoveryAttempted=!1;const t=this._getUserMessageText(e.message);if(t){const s=this._steeringMessages.indexOf(t);if(s!==-1)this._steeringMessages.splice(s,1),this._emitQueueUpdate();else{const n=this._followUpMessages.indexOf(t);n!==-1&&(this._followUpMessages.splice(n,1),this._emitQueueUpdate())}}}if(await this._emitExtensionEvent(e),this._emit(e.type==="agent_end"?{...e,willRetry:this._willRetryAfterAgentEnd(e)}:e),e.type==="message_end"&&(e.message.role==="custom"?this.sessionManager.appendCustomMessageEntry(e.message.customType,e.message.content,e.message.display,e.message.details):(e.message.role==="user"||e.message.role==="assistant"||e.message.role==="toolResult")&&this.sessionManager.appendMessage(e.message),e.message.role==="assistant")){this._lastAssistantMessage=e.message;const t=e.message;t.stopReason!=="error"&&(this._overflowRecoveryAttempted=!1),t.stopReason!=="error"&&this._retryAttempt>0&&(this._emit({type:"auto_retry_end",success:!0,attempt:this._retryAttempt}),this._retryAttempt=0)}},"_handleAgentEvent");_willRetryAfterAgentEnd(e){const t=this.settingsManager.getRetrySettings();if(!t.enabled||this._retryAttempt>=t.maxRetries)return!1;for(let s=e.messages.length-1;s>=0;s--){const n=e.messages[s];if(n.role==="assistant")return this._isRetryableError(n)}return!1}_getUserMessageText(e){if(e.role!=="user")return"";const t=e.content;return typeof t=="string"?t:t.filter(n=>n.type==="text").map(n=>n.text).join("")}_findLastAssistantMessage(){const e=this.agent.state.messages;for(let t=e.length-1;t>=0;t--){const s=e[t];if(s.role==="assistant")return s}}_replaceMessageInPlace(e,t){if(e===t)return;const s=e;for(const n of Object.keys(s))delete s[n];Object.assign(s,t)}async _emitExtensionEvent(e){if(e.type==="agent_start")this._turnIndex=0,await this._extensionRunner.emit({type:"agent_start"});else if(e.type==="agent_end")await this._extensionRunner.emit({type:"agent_end",messages:e.messages});else if(e.type==="turn_start"){const t={type:"turn_start",turnIndex:this._turnIndex,timestamp:Date.now()};await this._extensionRunner.emit(t)}else if(e.type==="turn_end"){const t={type:"turn_end",turnIndex:this._turnIndex,message:e.message,toolResults:e.toolResults};await this._extensionRunner.emit(t),this._turnIndex++}else if(e.type==="message_start"){const t={type:"message_start",message:e.message};await this._extensionRunner.emit(t)}else if(e.type==="message_update"){const t={type:"message_update",message:e.message,assistantMessageEvent:e.assistantMessageEvent};await this._extensionRunner.emit(t)}else if(e.type==="message_end"){const t={type:"message_end",message:e.message},s=await this._extensionRunner.emitMessageEnd(t);s&&this._replaceMessageInPlace(e.message,s)}else if(e.type==="tool_execution_start"){const t={type:"tool_execution_start",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args};await this._extensionRunner.emit(t)}else if(e.type==="tool_execution_update"){const t={type:"tool_execution_update",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args,partialResult:e.partialResult};await this._extensionRunner.emit(t)}else if(e.type==="tool_execution_end"){const t={type:"tool_execution_end",toolCallId:e.toolCallId,toolName:e.toolName,result:e.result,isError:e.isError};await this._extensionRunner.emit(t)}}subscribe(e){return this._eventListeners.push(e),()=>{const t=this._eventListeners.indexOf(e);t!==-1&&this._eventListeners.splice(t,1)}}_disconnectFromAgent(){this._unsubscribeAgent&&(this._unsubscribeAgent(),this._unsubscribeAgent=void 0)}_reconnectToAgent(){this._unsubscribeAgent||(this._unsubscribeAgent=this.agent.subscribe(this._handleAgentEvent))}dispose(){try{this.abortRetry(),this.abortCompaction(),this.abortBranchSummary(),this.abortBash(),this.agent.abort()}catch{}this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload()."),this._disconnectFromAgent(),this._eventListeners=[],te(this.sessionId)}get state(){return this.agent.state}get model(){return this.agent.state.model}get thinkingLevel(){return this.agent.state.thinkingLevel}get isStreaming(){return this.agent.state.isStreaming}get systemPrompt(){return this.agent.state.systemPrompt}get retryAttempt(){return this._retryAttempt}getActiveToolNames(){return this.agent.state.tools.map(e=>e.name)}getAllTools(){return Array.from(this._toolDefinitions.values()).map(({definition:e,sourceInfo:t})=>({name:e.name,description:e.description,parameters:e.parameters,promptGuidelines:e.promptGuidelines,sourceInfo:t}))}getToolDefinition(e){return this._toolDefinitions.get(e)?.definition}setActiveToolsByName(e){const t=[],s=[];for(const n of e){const i=this._toolRegistry.get(n);i&&(t.push(i),s.push(n))}this.agent.state.tools=t,this._baseSystemPrompt=this._rebuildSystemPrompt(s),this.agent.state.systemPrompt=this._baseSystemPrompt}get isCompacting(){return this._autoCompactionAbortController!==void 0||this._compactionAbortController!==void 0||this._branchSummaryAbortController!==void 0}get isWorking(){return this.isStreaming||this.isCompacting||this._turnActive}get messages(){return this.agent.state.messages}get steeringMode(){return this.agent.steeringMode}get followUpMode(){return this.agent.followUpMode}get sessionFile(){return this.sessionManager.getSessionFile()}get sessionId(){return this.sessionManager.getSessionId()}get sessionName(){return this.sessionManager.getSessionName()}get scopedModels(){return this._scopedModels}setScopedModels(e){this._scopedModels=e}get promptTemplates(){return this._resourceLoader.getPrompts().prompts}_normalizePromptSnippet(e){if(!e)return;const t=e.replace(/[\r\n]+/g," ").replace(/\s+/g," ").trim();return t.length>0?t:void 0}_normalizePromptGuidelines(e){if(!e||e.length===0)return[];const t=new Set;for(const s of e){const n=s.trim();n.length>0&&t.add(n)}return Array.from(t)}_rebuildSystemPrompt(e){const t=e.filter(h=>this._toolRegistry.has(h)),s={},n=[];for(const h of t){const u=this._toolPromptSnippets.get(h);u&&(s[h]=u);const m=this._toolPromptGuidelines.get(h);m&&n.push(...m)}const i=this._resourceLoader.getSystemPrompt(),r=[...this._resourceLoader.getAppendSystemPrompt(),...this._pinnedSkills.values()],a=r.length>0?r.join(`
1
+ var Y=Object.defineProperty;var w=(P,e)=>Y(P,"name",{value:e,configurable:!0});import{existsSync as C,mkdirSync as K,readFileSync as E,unlinkSync as Z,writeFileSync as L}from"node:fs";import{homedir as X}from"node:os";import{basename as ee,dirname as B,join as S,relative as te,resolve as F}from"node:path";import{clampThinkingLevel as se,cleanupSessionResources as ne,getSupportedThinkingLevels as ie,isContextOverflow as $,modelsAreEqual as T,resetApiProviders as re,streamSimple as H}from"@openadapter/koda-ai";import{theme as oe}from"../modes/interactive/theme/theme.js";import{stripFrontmatter as z}from"../utils/frontmatter.js";import{sendKodaTelemetry as ae}from"../utils/koda-telemetry.js";import{resolvePath as le}from"../utils/paths.js";import{sleep as ce}from"../utils/sleep.js";import{formatNoApiKeyFoundMessage as O,formatNoModelSelectedMessage as j}from"./auth-guidance.js";import{executeBashWithOperations as he}from"./bash-executor.js";import{calculateContextTokens as G,collectEntriesForBranchSummary as de,compact as W,estimateContextTokens as q,generateBranchSummary as ue,prepareCompaction as V,shouldCompact as me}from"./compaction/index.js";import{DEFAULT_THINKING_LEVEL as ge}from"./defaults.js";import{execCommand as pe}from"./exec.js";import{exportSessionToHtml as fe}from"./export-html/index.js";import{createToolHtmlRenderer as _e}from"./export-html/tool-renderer.js";import{ExtensionRunner as ye,wrapRegisteredTools as J}from"./extensions/index.js";import{emitSessionShutdownEvent as we}from"./extensions/runner.js";import{expandPromptTemplate as I}from"./prompt-templates.js";import{CURRENT_SESSION_VERSION as be,getLatestCompactionEntry as Q}from"./session-manager.js";import{createSyntheticSourceInfo as N}from"./source-info.js";import{buildSystemPrompt as xe}from"./system-prompt.js";import{createLocalBashOperations as ve}from"./tools/bash.js";import{createAllToolDefinitions as Ae}from"./tools/index.js";import{createToolDefinitionFromAgentTool as Me}from"./tools/tool-definition-wrapper.js";function Ze(P){const e=P.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);return e?{name:e[1],location:e[2],content:e[3],userMessage:e[4]?.trim()||void 0}:null}w(Ze,"parseSkillBlock");const ke=["off","minimal","low","medium","high"];class Xe{static{w(this,"AgentSession")}agent;sessionManager;settingsManager;_scopedModels;_unsubscribeAgent;_eventListeners=[];_steeringMessages=[];_followUpMessages=[];_pendingNextTurnMessages=[];_compactionAbortController=void 0;_autoCompactionAbortController=void 0;_overflowRecoveryAttempted=!1;_branchSummaryAbortController=void 0;_retryAbortController=void 0;_retryAttempt=0;_overseerAbortController=void 0;_recapAbortController=void 0;_turnActive=!1;_failoverTried=new Set;get _zenitsu(){return process.env.KODA_ZENITSU==="1"}_zenitsuPreferred;_zenitsuCooldown=new Map;_zenitsuHealthCache;_overseerRounds=0;_currentGoal;_tierCache;_overseerLastError="";_escalationsDone=0;_escalateTried=new Set;_sessionEscalations=0;_stickyModel;_homeModel;_sessionGoal;_pinnedSkills=new Map;_codeChangedThisTask=!1;_verifyCmd;_taskStartTs=0;_taskHadActivity=!1;_suggestShown=new Set;_suggestedThisTask=!1;_harnessTrace=[];_gatewayCircuit={failures:0,openUntil:0};_bashAbortController=void 0;_pendingBashMessages=[];_extensionRunner;_turnIndex=0;_resourceLoader;_customTools;_baseToolDefinitions=new Map;_cwd;_protectedSnap=new Map;_extensionRunnerRef;_initialActiveToolNames;_allowedToolNames;_excludedToolNames;_baseToolsOverride;_sessionStartEvent;_extensionUIContext;_extensionMode="print";_extensionCommandContextActions;_extensionAbortHandler;_extensionShutdownHandler;_extensionErrorListener;_extensionErrorUnsubscriber;_modelRegistry;_toolRegistry=new Map;_toolDefinitions=new Map;_toolPromptSnippets=new Map;_toolPromptGuidelines=new Map;_baseSystemPrompt="";_baseSystemPromptOptions;constructor(e){this.agent=e.agent,this.sessionManager=e.sessionManager,this.settingsManager=e.settingsManager,this._scopedModels=e.scopedModels??[],this._resourceLoader=e.resourceLoader,this._customTools=e.customTools??[],this._cwd=e.cwd,this._modelRegistry=e.modelRegistry,this._extensionRunnerRef=e.extensionRunnerRef,this._initialActiveToolNames=e.initialActiveToolNames,this._allowedToolNames=e.allowedToolNames?new Set(e.allowedToolNames):void 0,this._excludedToolNames=e.excludedToolNames?new Set(e.excludedToolNames):void 0,this._baseToolsOverride=e.baseToolsOverride,this._sessionStartEvent=e.sessionStartEvent??{type:"session_start",reason:"startup"},this._unsubscribeAgent=this.agent.subscribe(this._handleAgentEvent),this._installAgentToolHooks(),this._buildRuntime({activeToolNames:this._initialActiveToolNames,includeAllExtensionTools:!0})}get modelRegistry(){return this._modelRegistry}async _getRequiredRequestAuth(e){const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(!t.ok)throw t.error.startsWith("No API key found")?new Error(O(e.provider)):new Error(t.error);if(t.apiKey)return{apiKey:t.apiKey,headers:t.headers};throw this._modelRegistry.isUsingOAuth(e)?new Error(`Authentication failed for "${e.provider}". Credentials may have expired or network is unavailable. Run '/login ${e.provider}' to re-authenticate.`):new Error(O(e.provider))}async _getCompactionRequestAuth(e){if(this.agent.streamFn===H)return this._getRequiredRequestAuth(e);const t=await this._modelRegistry.getApiKeyAndHeaders(e);return t.ok?{apiKey:t.apiKey,headers:t.headers}:{}}_installAgentToolHooks(){const e=process.env.KODA_UNSAFE==="1",t=process.cwd(),s=[/(^|\/)test_[^/]*\.[A-Za-z0-9]+$/,/(^|\/)[^/]*_test\.[A-Za-z0-9]+$/,/\.(test|spec)\.[A-Za-z0-9]+$/,/(^|\/)(tests?|__tests__|spec)\//,/(^|\/)\.git(\/|$)/,/(^|\/)\.env(\.[^/]*)?$/,/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Cargo\.lock)$/],n=(process.env.KODA_PROTECT||"").split(",").map(m=>m.trim()).filter(Boolean),i=w(m=>{const p=m.replace(/\\/g,"/");return s.some(y=>y.test(p))||n.some(y=>p.includes(y))},"isProtected"),o=w(m=>{for(const p of m.matchAll(/(?:>>?|\btee\s+(?:-a\s+)?)\s*([^\s;|&>]+)/g))if(i(p[1]))return`output redirect onto protected file '${p[1]}'`;return/\brm\s+(?:-\w+\s+)*[^\n;|&]*(?:test_|tests?\/|__tests__|\.git|\.env)/.test(m)?"rm targeting a protected path":/\bgit\s+(?:reset\s+--hard|clean\b|checkout\s+(?:--\s+)?\.)/.test(m)?"destructive git command":null},"dangerousBash"),r=S(process.env.KODA_CODING_AGENT_DIR||S(X(),".koda","agent"),"checkpoints",String(this.sessionId||"session")),l=new Set,a=w(m=>{if(!l.has(m)){l.add(m);try{K(r,{recursive:!0});const p=S(r,`${m.replace(/[^A-Za-z0-9]/g,"_")}.json`),y=C(m);L(p,JSON.stringify({path:m,existed:y,content:y?E(m,"utf-8"):null}))}catch{}}},"backup");this.agent.beforeToolCall=async({toolCall:m,args:p})=>{if((m.name==="write"||m.name==="edit")&&(this._codeChangedThisTask=!0),this._taskHadActivity=!0,m.name==="bash"){const v=String(p?.command??"");/\b(npm|pnpm|yarn|bun)\s+(run\s+)?(dev|start|serve|preview|watch)\b|\bvite\b|\bnext\s+(dev|start)\b|\bnuxt\s+dev\b|\bnodemon\b|http[-.]server\b|\bserve\b|flask\s+run|rails\s+s(erver)?\b|manage\.py\s+runserver|\b(uvicorn|gunicorn)\b/i.test(v)&&this._suggest("bg","That looks like a long-running server. Use run_background (or /bg) so it keeps running without blocking Koda.")}if(process.env.KODA_MODE==="plan"){const v=p,f=m.name;if(f==="write"||f==="edit")return{block:!0,reason:"Koda is in PLAN MODE \u2014 file changes are disabled. Investigate and present a step-by-step plan instead; the user will run /build to execute it."};if(f==="bash"&&this._isMutatingBash(String(v?.command??"")))return{block:!0,reason:"Koda is in PLAN MODE \u2014 commands that change state are disabled. Use read-only commands to investigate, then present a plan; the user will run /build to execute it."}}if(!e){const v=p,f=m.name;if(f==="write"||f==="edit"){const _=String(v?.path??v?.file_path??"");if(_){const A=F(t,_);if(te(t,A).startsWith(".."))return{block:!0,reason:`Refused: '${_}' is outside the workspace. Only modify files under the project directory.`};if(i(_))return{block:!0,reason:`Refused: '${_}' is a protected file (tests / .git / secrets / lockfiles). Do not modify it \u2014 change the source code instead.`};a(A)}}else if(f==="bash"){const _=o(String(v?.command??""));if(_)return{block:!0,reason:`Refused: ${_}. Use the edit/write tools on source files instead of shell redirects, and never touch tests/.git/secrets.`}}const M=[];if(f==="write"||f==="edit"){const _=String(v?.path??v?.file_path??"");_&&M.push(_)}else if(f==="bash")for(const _ of String(v?.command??"").matchAll(/[\w./-]*[\w]\.[A-Za-z0-9]+/g))M.push(_[0]);for(const _ of M){if(!i(_))continue;const A=F(t,_);if(!this._protectedSnap.has(A))try{this._protectedSnap.set(A,C(A)?E(A,"utf-8"):null)}catch{}}}const y=this._extensionRunner;if(y.hasHandlers("tool_call"))try{return await y.emitToolCall({type:"tool_call",toolName:m.name,toolCallId:m.id,input:p})}catch(v){throw v instanceof Error?v:new Error(`Extension failed, blocking execution: ${String(v)}`)}};const c=process.env.KODA_CASCADE_MODEL,d=Number(process.env.KODA_CASCADE_ERRORS)||3,u=Number(process.env.KODA_CASCADE_STEPS)||16;let g=0,x=0,h=!1,b;this.agent.afterToolCall=async({toolCall:m,args:p,result:y,isError:v})=>{for(const[_,A]of this._protectedSnap){let k=null;try{k=C(_)?E(_,"utf-8"):null}catch{continue}if(k!==A)try{A===null?C(_)&&Z(_):L(_,A,"utf-8"),console.error(`[koda safety] reverted unauthorized change to protected file ${_}`)}catch{}}if(c&&!h&&(x++,v?g++:g=0,g>=d||x>=u)){const _=this.agent.state.model,[A,k]=c.includes("/")?[c.slice(0,c.indexOf("/")),c.slice(c.indexOf("/")+1)]:[_.provider,c],R=this._modelRegistry.find(A,k);R&&!T(R,_)&&(b=R,this.agent.state.model=R,this.sessionManager.appendModelChange(R.provider,R.id),this._emitModelSelect(R,_,"cycle"),h=!0,console.error(`[koda cascade] escalated ${_.provider}/${_.id} \u2192 ${R.provider}/${R.id} (errors=${g}, toolCalls=${x})`))}const f=this._extensionRunner;if(!f.hasHandlers("tool_result"))return;const M=await f.emitToolResult({type:"tool_result",toolName:m.name,toolCallId:m.id,input:p,content:y.content,details:y.details,isError:v});if(M)return{content:M.content,details:M.details,isError:M.isError??v}},this.agent.prepareNextTurn=async()=>{if(this._zenitsu){const m=await this._zenitsuPickModel();if(m&&!T(m,this.agent.state.model))return{model:m}}return h&&b?{model:b}:void 0}}_emit(e){for(const t of this._eventListeners)t(e)}_emitQueueUpdate(){this._emit({type:"queue_update",steering:[...this._steeringMessages],followUp:[...this._followUpMessages]})}_lastAssistantMessage=void 0;_handleAgentEvent=w(async e=>{if(e.type==="message_start"&&e.message.role==="user"){this._overflowRecoveryAttempted=!1;const t=this._getUserMessageText(e.message);if(t){const s=this._steeringMessages.indexOf(t);if(s!==-1)this._steeringMessages.splice(s,1),this._emitQueueUpdate();else{const n=this._followUpMessages.indexOf(t);n!==-1&&(this._followUpMessages.splice(n,1),this._emitQueueUpdate())}}}if(await this._emitExtensionEvent(e),this._emit(e.type==="agent_end"?{...e,willRetry:this._willRetryAfterAgentEnd(e)}:e),e.type==="message_end"&&(e.message.role==="custom"?this.sessionManager.appendCustomMessageEntry(e.message.customType,e.message.content,e.message.display,e.message.details):(e.message.role==="user"||e.message.role==="assistant"||e.message.role==="toolResult")&&this.sessionManager.appendMessage(e.message),e.message.role==="assistant")){this._lastAssistantMessage=e.message;const t=e.message;t.stopReason!=="error"&&(this._overflowRecoveryAttempted=!1),t.stopReason!=="error"&&this._retryAttempt>0&&(this._emit({type:"auto_retry_end",success:!0,attempt:this._retryAttempt}),this._retryAttempt=0)}},"_handleAgentEvent");_willRetryAfterAgentEnd(e){const t=this.settingsManager.getRetrySettings();if(!t.enabled||this._retryAttempt>=t.maxRetries)return!1;for(let s=e.messages.length-1;s>=0;s--){const n=e.messages[s];if(n.role==="assistant")return this._isRetryableError(n)}return!1}_getUserMessageText(e){if(e.role!=="user")return"";const t=e.content;return typeof t=="string"?t:t.filter(n=>n.type==="text").map(n=>n.text).join("")}_findLastAssistantMessage(){const e=this.agent.state.messages;for(let t=e.length-1;t>=0;t--){const s=e[t];if(s.role==="assistant")return s}}_replaceMessageInPlace(e,t){if(e===t)return;const s=e;for(const n of Object.keys(s))delete s[n];Object.assign(s,t)}async _emitExtensionEvent(e){if(e.type==="agent_start")this._turnIndex=0,await this._extensionRunner.emit({type:"agent_start"});else if(e.type==="agent_end")await this._extensionRunner.emit({type:"agent_end",messages:e.messages});else if(e.type==="turn_start"){const t={type:"turn_start",turnIndex:this._turnIndex,timestamp:Date.now()};await this._extensionRunner.emit(t)}else if(e.type==="turn_end"){const t={type:"turn_end",turnIndex:this._turnIndex,message:e.message,toolResults:e.toolResults};await this._extensionRunner.emit(t),this._turnIndex++}else if(e.type==="message_start"){const t={type:"message_start",message:e.message};await this._extensionRunner.emit(t)}else if(e.type==="message_update"){const t={type:"message_update",message:e.message,assistantMessageEvent:e.assistantMessageEvent};await this._extensionRunner.emit(t)}else if(e.type==="message_end"){const t={type:"message_end",message:e.message},s=await this._extensionRunner.emitMessageEnd(t);s&&this._replaceMessageInPlace(e.message,s)}else if(e.type==="tool_execution_start"){const t={type:"tool_execution_start",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args};await this._extensionRunner.emit(t)}else if(e.type==="tool_execution_update"){const t={type:"tool_execution_update",toolCallId:e.toolCallId,toolName:e.toolName,args:e.args,partialResult:e.partialResult};await this._extensionRunner.emit(t)}else if(e.type==="tool_execution_end"){const t={type:"tool_execution_end",toolCallId:e.toolCallId,toolName:e.toolName,result:e.result,isError:e.isError};await this._extensionRunner.emit(t)}}subscribe(e){return this._eventListeners.push(e),()=>{const t=this._eventListeners.indexOf(e);t!==-1&&this._eventListeners.splice(t,1)}}_disconnectFromAgent(){this._unsubscribeAgent&&(this._unsubscribeAgent(),this._unsubscribeAgent=void 0)}_reconnectToAgent(){this._unsubscribeAgent||(this._unsubscribeAgent=this.agent.subscribe(this._handleAgentEvent))}dispose(){try{this.abortRetry(),this.abortCompaction(),this.abortBranchSummary(),this.abortBash(),this.agent.abort()}catch{}this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload()."),this._disconnectFromAgent(),this._eventListeners=[],ne(this.sessionId)}get state(){return this.agent.state}get model(){return this.agent.state.model}get thinkingLevel(){return this.agent.state.thinkingLevel}get isStreaming(){return this.agent.state.isStreaming}get systemPrompt(){return this.agent.state.systemPrompt}get retryAttempt(){return this._retryAttempt}getActiveToolNames(){return this.agent.state.tools.map(e=>e.name)}getAllTools(){return Array.from(this._toolDefinitions.values()).map(({definition:e,sourceInfo:t})=>({name:e.name,description:e.description,parameters:e.parameters,promptGuidelines:e.promptGuidelines,sourceInfo:t}))}getToolDefinition(e){return this._toolDefinitions.get(e)?.definition}setActiveToolsByName(e){const t=[],s=[];for(const n of e){const i=this._toolRegistry.get(n);i&&(t.push(i),s.push(n))}this.agent.state.tools=t,this._baseSystemPrompt=this._rebuildSystemPrompt(s),this.agent.state.systemPrompt=this._baseSystemPrompt}get isCompacting(){return this._autoCompactionAbortController!==void 0||this._compactionAbortController!==void 0||this._branchSummaryAbortController!==void 0}get isWorking(){return this.isStreaming||this.isCompacting||this._turnActive}get messages(){return this.agent.state.messages}get steeringMode(){return this.agent.steeringMode}get followUpMode(){return this.agent.followUpMode}get sessionFile(){return this.sessionManager.getSessionFile()}get sessionId(){return this.sessionManager.getSessionId()}get sessionName(){return this.sessionManager.getSessionName()}get scopedModels(){return this._scopedModels}setScopedModels(e){this._scopedModels=e}get promptTemplates(){return this._resourceLoader.getPrompts().prompts}_normalizePromptSnippet(e){if(!e)return;const t=e.replace(/[\r\n]+/g," ").replace(/\s+/g," ").trim();return t.length>0?t:void 0}_normalizePromptGuidelines(e){if(!e||e.length===0)return[];const t=new Set;for(const s of e){const n=s.trim();n.length>0&&t.add(n)}return Array.from(t)}_rebuildSystemPrompt(e){const t=e.filter(d=>this._toolRegistry.has(d)),s={},n=[];for(const d of t){const u=this._toolPromptSnippets.get(d);u&&(s[d]=u);const g=this._toolPromptGuidelines.get(d);g&&n.push(...g)}const i=this._resourceLoader.getSystemPrompt(),r=[...this._resourceLoader.getAppendSystemPrompt(),...this._pinnedSkills.values()],l=r.length>0?r.join(`
2
2
 
3
- `):void 0,l=this._resourceLoader.getSkills().skills,c=this._resourceLoader.getAgentsFiles().agentsFiles;return this._baseSystemPromptOptions={cwd:this._cwd,skills:l,contextFiles:c,customPrompt:i,appendSystemPrompt:a,selectedTools:t,toolSnippets:s,promptGuidelines:n},we(this._baseSystemPromptOptions)}async _continueWithHeal(){try{return await this.agent.continue(),!0}catch(e){const t=e?.message||String(e);if(this._trace("recover",`continue failed: ${t.slice(0,90)}`),/cannot continue from message role/i.test(t)){this.agent.followUp({role:"user",content:[{type:"text",text:"Continue from where you left off and finish the task."}],timestamp:Date.now()});try{return await this.agent.continue(),!0}catch{}}return this._emit({type:"model_switch_notice",message:`Koda hit a snag and stopped cleanly (${t.slice(0,60)}). Your work is saved \u2014 send another message to continue.`}),re({type:"error",error:t}),!1}}async _runAgentPrompt(e){this._turnActive=!0,this._failoverTried.clear(),this._overseerRounds=0,this._escalationsDone=0,this._escalateTried.clear(),this._codeChangedThisTask=!1,this._taskStartTs=Date.now(),this._taskHadActivity=!1,this._suggestedThisTask=!1;const t=this._goalFromMessages(e);if(t&&(this._currentGoal=t),!this._homeModel)this._homeModel=this.agent.state.model;else{const s=this._stickyModel??this._homeModel;s.id!==this.agent.state.model.id&&this._modelRegistry.hasConfiguredAuth(s)&&(this.agent.state.model=s,this._zenitsuPreferred=s,this._stickyModel||(this._trace("revert",`\u2192 ${s.id} (new task)`),this._emit({type:"model_switch_notice",message:`\u2190 Back on your pick \u2014 ${s.id}`})))}try{await this.agent.prompt(e);let s=!0;for(;s&&await this._handlePostAgentRun();)s=await this._continueWithHeal();if(this._taskHadActivity&&process.env.KODA_RECAP!=="0"){this._emit({type:"recap_start"}),this._recapAbortController=new AbortController;const n=this._recapAbortController.signal;try{const i=await this._generateRecap(n);n.aborted||this._emit({type:"task_recap",elapsedMs:Date.now()-this._taskStartTs,recap:i||""})}finally{this._recapAbortController=void 0,this._emit({type:"recap_end"})}}for(this._taskHadActivity&&!this._sessionGoal&&Date.now()-this._taskStartTs>18e4&&this._suggest("goal","Working on something big? Set a goal with /goal \u2014 Koda tracks the milestones and won't stop until they're done.");s&&this.agent.hasQueuedMessages();)for(s=await this._continueWithHeal();s&&await this._handlePostAgentRun();)s=await this._continueWithHeal()}finally{this._turnActive=!1,this._flushPendingBashMessages()}}async _generateRecap(e){const t=this.agent.state.model,s=process.env.KODA_RECAP_MODEL?[process.env.KODA_RECAP_MODEL]:["free/qwen3-coder","free/qwen3-next-80b-a3b-instruct","GLM-4.7-Flash","GLM-4.7"];for(const n of s){if(e?.aborted)return null;const i=this._modelRegistry.find(t.provider,n);if(!i)continue;const o=await this._recapOnce(i,e);if(o)return o}return null}async _recapOnce(e,t){try{if(!e.baseUrl)return null;const s=await this._modelRegistry.getApiKeyAndHeaders(e);if(!s?.apiKey)return null;const n=new AbortController,i=setTimeout(()=>n.abort(),2e4),o=f(()=>n.abort(),"onExternalAbort");t&&(t.aborted?n.abort():t.addEventListener("abort",o));try{const r=await fetch(`${e.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${s.apiKey}`,"Content-Type":"application/json",...s.headers||{}},body:JSON.stringify({model:e.id,temperature:.2,max_tokens:150,messages:[{role:"system",content:'Output ONLY a 1-2 sentence, past-tense recap of what was done this turn (and the next step if any). No analysis, no numbered lists, no markdown, no preamble \u2014 just the recap. Example: "Added a /health endpoint to server.ts and confirmed the build passes. Next: add a test for it."'},{role:"user",content:`REQUEST:
3
+ `):void 0,a=this._resourceLoader.getSkills().skills,c=this._resourceLoader.getAgentsFiles().agentsFiles;return this._baseSystemPromptOptions={cwd:this._cwd,skills:a,contextFiles:c,customPrompt:i,appendSystemPrompt:l,selectedTools:t,toolSnippets:s,promptGuidelines:n},xe(this._baseSystemPromptOptions)}async _continueWithHeal(){try{return await this.agent.continue(),!0}catch(e){const t=e?.message||String(e);if(this._trace("recover",`continue failed: ${t.slice(0,90)}`),/cannot continue from message role/i.test(t)){this.agent.followUp({role:"user",content:[{type:"text",text:"Continue from where you left off and finish the task."}],timestamp:Date.now()});try{return await this.agent.continue(),!0}catch{}}return this._emit({type:"model_switch_notice",message:`Koda hit a snag and stopped cleanly (${t.slice(0,60)}). Your work is saved \u2014 send another message to continue.`}),ae({type:"error",error:t}),!1}}async _runAgentPrompt(e){this._turnActive=!0,this._failoverTried.clear(),this._overseerRounds=0,this._escalationsDone=0,this._escalateTried.clear(),this._codeChangedThisTask=!1,this._taskStartTs=Date.now(),this._taskHadActivity=!1,this._suggestedThisTask=!1;const t=this._goalFromMessages(e);if(t&&(this._currentGoal=t),!this._homeModel)this._homeModel=this.agent.state.model;else{const s=this._stickyModel??this._homeModel;s.id!==this.agent.state.model.id&&this._modelRegistry.hasConfiguredAuth(s)&&(this.agent.state.model=s,this._zenitsuPreferred=s,this._stickyModel||(this._trace("revert",`\u2192 ${s.id} (new task)`),this._emit({type:"model_switch_notice",message:`\u2190 Back on your pick \u2014 ${s.id}`})))}try{await this.agent.prompt(e);let s=!0;for(;s&&await this._handlePostAgentRun();)s=await this._continueWithHeal();if(this._taskHadActivity&&process.env.KODA_RECAP!=="0"){this._emit({type:"recap_start"}),this._recapAbortController=new AbortController;const n=this._recapAbortController.signal;try{const i=await this._generateRecap(n);n.aborted||this._emit({type:"task_recap",elapsedMs:Date.now()-this._taskStartTs,recap:i||""})}finally{this._recapAbortController=void 0,this._emit({type:"recap_end"})}}for(this._taskHadActivity&&!this._sessionGoal&&Date.now()-this._taskStartTs>18e4&&this._suggest("goal","Working on something big? Set a goal with /goal \u2014 Koda tracks the milestones and won't stop until they're done.");s&&this.agent.hasQueuedMessages();)for(s=await this._continueWithHeal();s&&await this._handlePostAgentRun();)s=await this._continueWithHeal()}finally{this._turnActive=!1,this._flushPendingBashMessages()}}async _generateRecap(e){const t=this.agent.state.model,s=process.env.KODA_RECAP_MODEL?[process.env.KODA_RECAP_MODEL]:["free/qwen3-coder","free/qwen3-next-80b-a3b-instruct","GLM-4.7-Flash","GLM-4.7"];for(const n of s){if(e?.aborted)return null;const i=this._modelRegistry.find(t.provider,n);if(!i)continue;const o=await this._recapOnce(i,e);if(o)return o}return null}async _recapOnce(e,t){try{if(!e.baseUrl)return null;const s=await this._modelRegistry.getApiKeyAndHeaders(e);if(!s?.apiKey)return null;const n=new AbortController,i=setTimeout(()=>n.abort(),2e4),o=w(()=>n.abort(),"onExternalAbort");t&&(t.aborted?n.abort():t.addEventListener("abort",o));try{const r=await fetch(`${e.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${s.apiKey}`,"Content-Type":"application/json",...s.headers||{}},body:JSON.stringify({model:e.id,temperature:.2,max_tokens:150,messages:[{role:"system",content:'Output ONLY a 1-2 sentence, past-tense recap of what was done this turn (and the next step if any). No analysis, no numbered lists, no markdown, no preamble \u2014 just the recap. Example: "Added a /health endpoint to server.ts and confirmed the build passes. Next: add a test for it."'},{role:"user",content:`REQUEST:
4
4
  ${this._currentGoal||"(none)"}
5
5
 
6
6
  WORK LOG:
7
- ${this._overseerContext()}`}]}),signal:n.signal});return r.ok&&((await r.json())?.choices?.[0]?.message?.content??"").replace(/<think>[\s\S]*?<\/think>/gi,"").trim().slice(0,400)||null}finally{clearTimeout(i),t&&t.removeEventListener("abort",o)}}catch{return null}}async _handlePostAgentRun(){const e=this._lastAssistantMessage;return this._lastAssistantMessage=void 0,e?(e.stopReason!=="error"&&this._maybeReturnHome(),this._isDegenerateOutput(e)&&await this._escalate("output broke down \u2014 bringing in a stronger model",{stripLast:!0})||this._zenitsu&&await this._zenitsuHandle429(e)||(!this._zenitsu&&this._isRateLimited(e)&&this._suggest("zenitsu","Getting rate limited \u2014 zenitsu mode reroutes to a faster model instantly instead of waiting. Turn it on with /zenitsu."),this._isRetryableError(e)&&await this._prepareRetry(e))||e.stopReason==="error"&&await this._failoverModel(e)||(e.stopReason==="error"&&this._retryAttempt>0&&(this._emit({type:"auto_retry_end",success:!1,attempt:this._retryAttempt,finalError:e.errorMessage}),this._retryAttempt=0),await this._checkCompaction(e))||await this._overseerCheck(e)?!0:this.agent.hasQueuedMessages()):!1}async prompt(e,t){const s=t?.expandPromptTemplates??!0,n=t?.preflightResult;let i;try{if(s&&e.startsWith("/")&&await this._tryExecuteExtensionCommand(e)){n?.(!0);return}let o=e,r=t?.images;if(this._extensionRunner.hasHandlers("input")){const u=await this._extensionRunner.emitInput(o,r,t?.source??"interactive",this.isStreaming||this._turnActive?t?.streamingBehavior:void 0);if(u.action==="handled"){n?.(!0);return}u.action==="transform"&&(o=u.text,r=u.images??r)}let a=o;if(s&&(a=this._expandSkillCommand(a),a=N(a,[...this.promptTemplates])),this.isStreaming||this._turnActive){if(!t?.streamingBehavior)throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");t.streamingBehavior==="followUp"?await this._queueFollowUp(a,r):await this._queueSteer(a,r),n?.(!0);return}if(this._flushPendingBashMessages(),!this.model)throw new Error(H());if(!this._modelRegistry.hasConfiguredAuth(this.model))throw this._modelRegistry.isUsingOAuth(this.model)?new Error(`Authentication failed for "${this.model.provider}". Credentials may have expired or network is unavailable. Run '/login ${this.model.provider}' to re-authenticate.`):new Error(O(this.model.provider));const l=this._findLastAssistantMessage();if(l&&await this._checkCompaction(l,!1))try{for(await this.agent.continue();await this._handlePostAgentRun();)await this.agent.continue()}finally{this._flushPendingBashMessages()}i=[];const c=[{type:"text",text:a}];r&&c.push(...r),i.push({role:"user",content:c,timestamp:Date.now()});for(const u of this._pendingNextTurnMessages)i.push(u);this._pendingNextTurnMessages=[];const h=await this._extensionRunner.emitBeforeAgentStart(a,r,this._baseSystemPrompt,this._baseSystemPromptOptions);if(h?.messages)for(const u of h.messages)i.push({role:"custom",customType:u.customType,content:u.content,display:u.display,details:u.details,timestamp:Date.now()});h?.systemPrompt?this.agent.state.systemPrompt=h.systemPrompt:this.agent.state.systemPrompt=this._baseSystemPrompt}catch(o){throw n?.(!1),o}i&&(n?.(!0),await this._runAgentPrompt(i))}async _tryExecuteExtensionCommand(e){const t=e.indexOf(" "),s=t===-1?e.slice(1):e.slice(1,t),n=t===-1?"":e.slice(t+1),i=this._extensionRunner.getCommand(s);if(!i)return!1;const o=this._extensionRunner.createCommandContext();try{return await i.handler(n,o),!0}catch(r){return this._extensionRunner.emitError({extensionPath:`command:${s}`,event:"command",error:r instanceof Error?r.message:String(r)}),!0}}_expandSkillCommand(e){if(!e.startsWith("/skill:"))return e;const t=e.indexOf(" "),s=t===-1?e.slice(7):e.slice(7,t),n=t===-1?"":e.slice(t+1).trim(),i=this.resourceLoader.getSkills().skills.find(o=>o.name===s);if(!i)return e;try{const o=T(i.filePath,"utf-8"),r=F(o).trim(),a=`<skill name="${i.name}" location="${i.filePath}">
7
+ ${this._overseerContext()}`}]}),signal:n.signal});return r.ok&&((await r.json())?.choices?.[0]?.message?.content??"").replace(/<think>[\s\S]*?<\/think>/gi,"").trim().slice(0,400)||null}finally{clearTimeout(i),t&&t.removeEventListener("abort",o)}}catch{return null}}async _handlePostAgentRun(){const e=this._lastAssistantMessage;return this._lastAssistantMessage=void 0,e?(e.stopReason!=="error"&&this._maybeReturnHome(),this._isDegenerateOutput(e)&&await this._escalate("output broke down \u2014 bringing in a stronger model",{stripLast:!0})||this._zenitsu&&await this._zenitsuHandle429(e)||(!this._zenitsu&&this._isRateLimited(e)&&this._suggest("zenitsu","Getting rate limited \u2014 zenitsu mode reroutes to a faster model instantly instead of waiting. Turn it on with /zenitsu."),this._isRetryableError(e)&&await this._prepareRetry(e))||e.stopReason==="error"&&await this._failoverModel(e)||(e.stopReason==="error"&&this._retryAttempt>0&&(this._emit({type:"auto_retry_end",success:!1,attempt:this._retryAttempt,finalError:e.errorMessage}),this._retryAttempt=0),await this._checkCompaction(e))||await this._overseerCheck(e)?!0:this.agent.hasQueuedMessages()):!1}async prompt(e,t){const s=t?.expandPromptTemplates??!0,n=t?.preflightResult;let i;try{if(s&&e.startsWith("/")&&await this._tryExecuteExtensionCommand(e)){n?.(!0);return}let o=e,r=t?.images;if(this._extensionRunner.hasHandlers("input")){const u=await this._extensionRunner.emitInput(o,r,t?.source??"interactive",this.isStreaming||this._turnActive?t?.streamingBehavior:void 0);if(u.action==="handled"){n?.(!0);return}u.action==="transform"&&(o=u.text,r=u.images??r)}let l=o;if(s&&(l=this._expandSkillCommand(l),l=I(l,[...this.promptTemplates])),this.isStreaming||this._turnActive){if(!t?.streamingBehavior)throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");t.streamingBehavior==="followUp"?await this._queueFollowUp(l,r):await this._queueSteer(l,r),n?.(!0);return}if(this._flushPendingBashMessages(),!this.model)throw new Error(j());if(!this._modelRegistry.hasConfiguredAuth(this.model))throw this._modelRegistry.isUsingOAuth(this.model)?new Error(`Authentication failed for "${this.model.provider}". Credentials may have expired or network is unavailable. Run '/login ${this.model.provider}' to re-authenticate.`):new Error(O(this.model.provider));const a=this._findLastAssistantMessage();if(a&&await this._checkCompaction(a,!1))try{for(await this.agent.continue();await this._handlePostAgentRun();)await this.agent.continue()}finally{this._flushPendingBashMessages()}i=[];const c=[{type:"text",text:l}];r&&c.push(...r),i.push({role:"user",content:c,timestamp:Date.now()});for(const u of this._pendingNextTurnMessages)i.push(u);this._pendingNextTurnMessages=[];const d=await this._extensionRunner.emitBeforeAgentStart(l,r,this._baseSystemPrompt,this._baseSystemPromptOptions);if(d?.messages)for(const u of d.messages)i.push({role:"custom",customType:u.customType,content:u.content,display:u.display,details:u.details,timestamp:Date.now()});d?.systemPrompt?this.agent.state.systemPrompt=d.systemPrompt:this.agent.state.systemPrompt=this._baseSystemPrompt}catch(o){throw n?.(!1),o}i&&(n?.(!0),await this._runAgentPrompt(i))}async _tryExecuteExtensionCommand(e){const t=e.indexOf(" "),s=t===-1?e.slice(1):e.slice(1,t),n=t===-1?"":e.slice(t+1),i=this._extensionRunner.getCommand(s);if(!i)return!1;const o=this._extensionRunner.createCommandContext();try{return await i.handler(n,o),!0}catch(r){return this._extensionRunner.emitError({extensionPath:`command:${s}`,event:"command",error:r instanceof Error?r.message:String(r)}),!0}}_expandSkillCommand(e){if(!e.startsWith("/skill:"))return e;const t=e.indexOf(" "),s=t===-1?e.slice(7):e.slice(7,t),n=t===-1?"":e.slice(t+1).trim(),i=this.resourceLoader.getSkills().skills.find(o=>o.name===s);if(!i)return e;try{const o=E(i.filePath,"utf-8"),r=z(o).trim(),l=`<skill name="${i.name}" location="${i.filePath}">
8
8
  References are relative to ${i.baseDir}.
9
9
 
10
10
  ${r}
11
- </skill>`;return n?`${a}
11
+ </skill>`;return n?`${l}
12
12
 
13
- ${n}`:a}catch(o){return this._extensionRunner.emitError({extensionPath:i.filePath,event:"skill_expansion",error:o instanceof Error?o.message:String(o)}),e}}async steer(e,t){e.startsWith("/")&&this._throwIfExtensionCommand(e);let s=this._expandSkillCommand(e);s=N(s,[...this.promptTemplates]),await this._queueSteer(s,t)}async followUp(e,t){e.startsWith("/")&&this._throwIfExtensionCommand(e);let s=this._expandSkillCommand(e);s=N(s,[...this.promptTemplates]),await this._queueFollowUp(s,t)}async _queueSteer(e,t){this._steeringMessages.push(e),this._emitQueueUpdate();const s=[{type:"text",text:e}];t&&s.push(...t),this.agent.steer({role:"user",content:s,timestamp:Date.now()})}async _queueFollowUp(e,t){this._followUpMessages.push(e),this._emitQueueUpdate();const s=[{type:"text",text:e}];t&&s.push(...t),this.agent.followUp({role:"user",content:s,timestamp:Date.now()})}_throwIfExtensionCommand(e){const t=e.indexOf(" "),s=t===-1?e.slice(1):e.slice(1,t);if(this._extensionRunner.getCommand(s))throw new Error(`Extension command "/${s}" cannot be queued. Use prompt() or execute the command when not streaming.`)}async sendCustomMessage(e,t){const s={role:"custom",customType:e.customType,content:e.content,display:e.display,details:e.details,timestamp:Date.now()};t?.deliverAs==="nextTurn"?this._pendingNextTurnMessages.push(s):this.isStreaming?t?.deliverAs==="followUp"?this.agent.followUp(s):this.agent.steer(s):t?.triggerTurn?await this._runAgentPrompt(s):(this.agent.state.messages.push(s),this.sessionManager.appendCustomMessageEntry(e.customType,e.content,e.display,e.details),this._emit({type:"message_start",message:s}),this._emit({type:"message_end",message:s}))}async sendUserMessage(e,t){let s,n;if(typeof e=="string")s=e;else{const i=[];n=[];for(const o of e)o.type==="text"?i.push(o.text):n.push(o);s=i.join(`
14
- `),n.length===0&&(n=void 0)}await this.prompt(s,{expandPromptTemplates:!1,streamingBehavior:t?.deliverAs,images:n,source:"extension"})}clearQueue(){const e=[...this._steeringMessages],t=[...this._followUpMessages];return this._steeringMessages=[],this._followUpMessages=[],this.agent.clearAllQueues(),this._emitQueueUpdate(),{steering:e,followUp:t}}get pendingMessageCount(){return this._steeringMessages.length+this._followUpMessages.length}getSteeringMessages(){return this._steeringMessages}getFollowUpMessages(){return this._followUpMessages}get resourceLoader(){return this._resourceLoader}async abort(){this.abortRetry(),this.agent.abort(),await this.agent.waitForIdle()}async _emitModelSelect(e,t,s){S(t,e)||await this._extensionRunner.emit({type:"model_select",model:e,previousModel:t,source:s})}async setModel(e){if(!this._modelRegistry.hasConfiguredAuth(e))throw new Error(`No API key for ${e.provider}/${e.id}`);const t=this.model,s=this._getThinkingLevelForModelSwitch();this.agent.state.model=e,this._homeModel=e,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(e.provider,e.id),this.settingsManager.setDefaultModelAndProvider(e.provider,e.id),this.setThinkingLevel(s),await this._emitModelSelect(e,t,"set")}async cycleModel(e="forward"){return this._scopedModels.length>0?this._cycleScopedModel(e):this._cycleAvailableModel(e)}async _cycleScopedModel(e){const t=this._scopedModels.filter(l=>this._modelRegistry.hasConfiguredAuth(l.model));if(t.length<=1)return;const s=this.model;let n=t.findIndex(l=>S(l.model,s));n===-1&&(n=0);const i=t.length,o=e==="forward"?(n+1)%i:(n-1+i)%i,r=t[o],a=this._getThinkingLevelForModelSwitch(r.thinkingLevel);return this.agent.state.model=r.model,this._homeModel=r.model,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(r.model.provider,r.model.id),this.settingsManager.setDefaultModelAndProvider(r.model.provider,r.model.id),this.setThinkingLevel(a),await this._emitModelSelect(r.model,s,"cycle"),{model:r.model,thinkingLevel:this.thinkingLevel,isScoped:!0}}async _cycleAvailableModel(e){const t=await this._modelRegistry.getAvailable();if(t.length<=1)return;const s=this.model;let n=t.findIndex(l=>S(l,s));n===-1&&(n=0);const i=t.length,o=e==="forward"?(n+1)%i:(n-1+i)%i,r=t[o],a=this._getThinkingLevelForModelSwitch();return this.agent.state.model=r,this._homeModel=r,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(r.provider,r.id),this.settingsManager.setDefaultModelAndProvider(r.provider,r.id),this.setThinkingLevel(a),await this._emitModelSelect(r,s,"cycle"),{model:r,thinkingLevel:this.thinkingLevel,isScoped:!1}}setThinkingLevel(e){const t=this.getAvailableThinkingLevels(),s=t.includes(e)?e:this._clampThinkingLevel(e,t),n=this.agent.state.thinkingLevel,i=s!==n;this.agent.state.thinkingLevel=s,i&&(this.sessionManager.appendThinkingLevelChange(s),(this.supportsThinking()||s!=="off")&&this.settingsManager.setDefaultThinkingLevel(s),this._emit({type:"thinking_level_changed",level:s}),this._extensionRunner.emit({type:"thinking_level_select",level:s,previousLevel:n}))}cycleThinkingLevel(){if(!this.supportsThinking())return;const e=this.getAvailableThinkingLevels(),s=(e.indexOf(this.thinkingLevel)+1)%e.length,n=e[s];return this.setThinkingLevel(n),n}getAvailableThinkingLevels(){return this.model?se(this.model):Ae}supportsThinking(){return!!this.model?.reasoning}_getThinkingLevelForModelSwitch(e){return e!==void 0?e:this.supportsThinking()?this.thinkingLevel:this.settingsManager.getDefaultThinkingLevel()??ue}_clampThinkingLevel(e,t){return this.model?ee(this.model,e):"off"}setSteeringMode(e){this.agent.steeringMode=e,this.settingsManager.setSteeringMode(e)}setFollowUpMode(e){this.agent.followUpMode=e,this.settingsManager.setFollowUpMode(e)}async compact(e){this._disconnectFromAgent(),await this.abort(),this._compactionAbortController=new AbortController,this._emit({type:"compaction_start",reason:"manual"});try{if(!this.model)throw new Error(H());const{apiKey:t,headers:s}=await this._getCompactionRequestAuth(this.model),n=this.sessionManager.getBranch(),i=this.settingsManager.getCompactionSettings(),o=W(n,i);if(!o)throw n[n.length-1]?.type==="compaction"?new Error("Already compacted"):new Error("Nothing to compact (session too small)");let r,a=!1;if(this._extensionRunner.hasHandlers("session_before_compact")){const g=await this._extensionRunner.emit({type:"session_before_compact",preparation:o,branchEntries:n,customInstructions:e,signal:this._compactionAbortController.signal});if(g?.cancel)throw new Error("Compaction cancelled");g?.compaction&&(r=g.compaction,a=!0)}let l,c,h,u;if(r)l=r.summary,c=r.firstKeptEntryId,h=r.tokensBefore,u=r.details;else{const g=await j(o,this.model,t,s,e,this._compactionAbortController.signal,this.thinkingLevel,this.agent.streamFn);l=g.summary,c=g.firstKeptEntryId,h=g.tokensBefore,u=g.details}if(this._compactionAbortController.signal.aborted)throw new Error("Compaction cancelled");this.sessionManager.appendCompaction(l,c,h,u,a);const m=this.sessionManager.getEntries(),w=this.sessionManager.buildSessionContext();this.agent.state.messages=w.messages;const d=m.find(g=>g.type==="compaction"&&g.summary===l);this._extensionRunner&&d&&await this._extensionRunner.emit({type:"session_compact",compactionEntry:d,fromExtension:a});const b={summary:l,firstKeptEntryId:c,tokensBefore:h,details:u};return this._emit({type:"compaction_end",reason:"manual",result:b,aborted:!1,willRetry:!1}),b}catch(t){const s=t instanceof Error?t.message:String(t),n=s==="Compaction cancelled"||t instanceof Error&&t.name==="AbortError";throw this._emit({type:"compaction_end",reason:"manual",result:void 0,aborted:n,willRetry:!1,errorMessage:n?void 0:`Compaction failed: ${s}`}),t}finally{this._compactionAbortController=void 0,this._reconnectToAgent()}}abortCompaction(){this._compactionAbortController?.abort(),this._autoCompactionAbortController?.abort()}abortBranchSummary(){this._branchSummaryAbortController?.abort()}abortOverseer(){this._overseerAbortController?.abort()}abortRecap(){this._recapAbortController?.abort()}async _checkCompaction(e,t=!0){const s=this.settingsManager.getCompactionSettings();if(!s.enabled||t&&e.stopReason==="aborted")return!1;const n=this.model?.contextWindow??0,i=this.model&&e.provider===this.model.provider&&e.model===this.model.id,o=V(this.sessionManager.getBranch());if(o!==null&&e.timestamp<=new Date(o.timestamp).getTime())return!1;if(i&&$(e,n)){if(this._overflowRecoveryAttempted)return this._emit({type:"compaction_end",reason:"overflow",result:void 0,aborted:!1,willRetry:!1,errorMessage:"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."}),!1;this._overflowRecoveryAttempted=!0;const l=this.agent.state.messages;return l.length>0&&l[l.length-1].role==="assistant"&&(this.agent.state.messages=l.slice(0,-1)),await this._runAutoCompaction("overflow",!0)}let a;if(e.stopReason==="error"){const l=this.agent.state.messages,c=G(l);if(c.lastUsageIndex===null)return!1;const h=l[c.lastUsageIndex];if(o&&h.role==="assistant"&&h.timestamp<=new Date(o.timestamp).getTime())return!1;a=c.tokens}else a=z(e.usage);return de(a,n,s)?await this._runAutoCompaction("threshold",!1):!1}async _runAutoCompaction(e,t){const s=this.settingsManager.getCompactionSettings();this._emit({type:"compaction_start",reason:e}),this._autoCompactionAbortController=new AbortController;try{if(!this.model)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;let n,i;if(this.agent.streamFn===B){const p=await this._modelRegistry.getApiKeyAndHeaders(this.model);if(!p.ok||!p.apiKey)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;n=p.apiKey,i=p.headers}else({apiKey:n,headers:i}=await this._getCompactionRequestAuth(this.model));const o=this.sessionManager.getBranch(),r=W(o,s);if(!r)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;let a,l=!1;if(this._extensionRunner.hasHandlers("session_before_compact")){const p=await this._extensionRunner.emit({type:"session_before_compact",preparation:r,branchEntries:o,customInstructions:void 0,signal:this._autoCompactionAbortController.signal});if(p?.cancel)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!0,willRetry:!1}),!1;p?.compaction&&(a=p.compaction,l=!0)}let c,h,u,m;if(a)c=a.summary,h=a.firstKeptEntryId,u=a.tokensBefore,m=a.details;else{const p=await j(r,this.model,n,i,void 0,this._autoCompactionAbortController.signal,this.thinkingLevel,this.agent.streamFn);c=p.summary,h=p.firstKeptEntryId,u=p.tokensBefore,m=p.details}if(this._autoCompactionAbortController.signal.aborted)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!0,willRetry:!1}),!1;this.sessionManager.appendCompaction(c,h,u,m,l);const w=this.sessionManager.getEntries(),d=this.sessionManager.buildSessionContext();this.agent.state.messages=d.messages;const b=w.find(p=>p.type==="compaction"&&p.summary===c);this._extensionRunner&&b&&await this._extensionRunner.emit({type:"session_compact",compactionEntry:b,fromExtension:l});const g={summary:c,firstKeptEntryId:h,tokensBefore:u,details:m};if(this._emit({type:"compaction_end",reason:e,result:g,aborted:!1,willRetry:t}),t){const p=this.agent.state.messages,_=p[p.length-1];return _?.role==="assistant"&&_.stopReason==="error"&&(this.agent.state.messages=p.slice(0,-1)),!0}return this.agent.hasQueuedMessages()}catch(n){const i=n instanceof Error?n.message:"compaction failed";return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1,errorMessage:e==="overflow"?`Context overflow recovery failed: ${i}`:`Auto-compaction failed: ${i}`}),!1}finally{this._autoCompactionAbortController=void 0}}setAutoCompactionEnabled(e){this.settingsManager.setCompactionEnabled(e)}get autoCompactionEnabled(){return this.settingsManager.getCompactionEnabled()}async bindExtensions(e){e.uiContext!==void 0&&(this._extensionUIContext=e.uiContext),e.mode!==void 0&&(this._extensionMode=e.mode),e.commandContextActions!==void 0&&(this._extensionCommandContextActions=e.commandContextActions),e.abortHandler!==void 0&&(this._extensionAbortHandler=e.abortHandler),e.shutdownHandler!==void 0&&(this._extensionShutdownHandler=e.shutdownHandler),e.onError!==void 0&&(this._extensionErrorListener=e.onError),this._applyExtensionBindings(this._extensionRunner),await this._extensionRunner.emit(this._sessionStartEvent),await this.extendResourcesFromExtensions(this._sessionStartEvent.reason==="reload"?"reload":"startup")}async extendResourcesFromExtensions(e){if(!this._extensionRunner.hasHandlers("resources_discover"))return;const{skillPaths:t,promptPaths:s,themePaths:n}=await this._extensionRunner.emitResourcesDiscover(this._cwd,e);if(t.length===0&&s.length===0&&n.length===0)return;const i={skillPaths:this.buildExtensionResourcePaths(t),promptPaths:this.buildExtensionResourcePaths(s),themePaths:this.buildExtensionResourcePaths(n)};this._resourceLoader.extendResources(i),this._baseSystemPrompt=this._rebuildSystemPrompt(this.getActiveToolNames()),this.agent.state.systemPrompt=this._baseSystemPrompt}buildExtensionResourcePaths(e){return e.map(t=>{const s=this.getExtensionSourceLabel(t.extensionPath),n=t.extensionPath.startsWith("<")?void 0:U(t.extensionPath);return{path:t.path,metadata:{source:s,scope:"temporary",origin:"top-level",baseDir:n}}})}getExtensionSourceLabel(e){return e.startsWith("<")?`extension:${e.replace(/[<>]/g,"")}`:`extension:${Z(e).replace(/\.(ts|js)$/,"")}`}_applyExtensionBindings(e){e.setUIContext(this._extensionUIContext,this._extensionMode),e.bindCommandContext(this._extensionCommandContextActions),this._extensionErrorUnsubscriber?.(),this._extensionErrorUnsubscriber=this._extensionErrorListener?e.onError(this._extensionErrorListener):void 0}_refreshCurrentModelFromRegistry(){const e=this.model;if(!e)return;const t=this._modelRegistry.find(e.provider,e.id);!t||t===e||(this.agent.state.model=t)}_bindExtensionCore(e){const t=f(()=>{const s=e.getRegisteredCommands().map(o=>({name:o.invocationName,description:o.description,source:"extension",sourceInfo:o.sourceInfo})),n=this.promptTemplates.map(o=>({name:o.name,description:o.description,source:"prompt",sourceInfo:o.sourceInfo})),i=this._resourceLoader.getSkills().skills.map(o=>({name:`skill:${o.name}`,description:o.description,source:"skill",sourceInfo:o.sourceInfo}));return[...s,...n,...i]},"getCommands");e.bindCore({sendMessage:f((s,n)=>{this.sendCustomMessage(s,n).catch(i=>{e.emitError({extensionPath:"<runtime>",event:"send_message",error:i instanceof Error?i.message:String(i)})})},"sendMessage"),sendUserMessage:f((s,n)=>{this.sendUserMessage(s,n).catch(i=>{e.emitError({extensionPath:"<runtime>",event:"send_user_message",error:i instanceof Error?i.message:String(i)})})},"sendUserMessage"),appendEntry:f((s,n)=>{this.sessionManager.appendCustomEntry(s,n)},"appendEntry"),setSessionName:f(s=>{this.setSessionName(s)},"setSessionName"),getSessionName:f(()=>this.sessionManager.getSessionName(),"getSessionName"),setLabel:f((s,n)=>{this.sessionManager.appendLabelChange(s,n)},"setLabel"),getActiveTools:f(()=>this.getActiveToolNames(),"getActiveTools"),getAllTools:f(()=>this.getAllTools(),"getAllTools"),setActiveTools:f(s=>this.setActiveToolsByName(s),"setActiveTools"),refreshTools:f(()=>this._refreshToolRegistry(),"refreshTools"),getCommands:t,setModel:f(async s=>this.modelRegistry.hasConfiguredAuth(s)?(await this.setModel(s),!0):!1,"setModel"),getThinkingLevel:f(()=>this.thinkingLevel,"getThinkingLevel"),setThinkingLevel:f(s=>this.setThinkingLevel(s),"setThinkingLevel")},{getModel:f(()=>this.model,"getModel"),isIdle:f(()=>!this.isStreaming,"isIdle"),getSignal:f(()=>this.agent.signal,"getSignal"),abort:f(()=>{if(this._extensionAbortHandler){this._extensionAbortHandler();return}this.abort()},"abort"),hasPendingMessages:f(()=>this.pendingMessageCount>0,"hasPendingMessages"),shutdown:f(()=>{this._extensionShutdownHandler?.()},"shutdown"),getContextUsage:f(()=>this.getContextUsage(),"getContextUsage"),compact:f(s=>{(async()=>{try{const n=await this.compact(s?.customInstructions);s?.onComplete?.(n)}catch(n){const i=n instanceof Error?n:new Error(String(n));s?.onError?.(i)}})()},"compact"),getSystemPrompt:f(()=>this.systemPrompt,"getSystemPrompt"),getSystemPromptOptions:f(()=>this._baseSystemPromptOptions,"getSystemPromptOptions")},{registerProvider:f((s,n)=>{this._modelRegistry.registerProvider(s,n),this._refreshCurrentModelFromRegistry()},"registerProvider"),unregisterProvider:f(s=>{this._modelRegistry.unregisterProvider(s),this._refreshCurrentModelFromRegistry()},"unregisterProvider")})}_refreshToolRegistry(e){const t=new Set(this._toolRegistry.keys()),s=this.getActiveToolNames(),n=this._allowedToolNames,i=this._excludedToolNames,o=f(d=>(!n||n.has(d))&&!i?.has(d),"isAllowedTool"),a=[...this._extensionRunner.getAllRegisteredTools(),...this._customTools.map(d=>({definition:d,sourceInfo:I(`<sdk:${d.name}>`,{source:"sdk"})}))].filter(d=>o(d.definition.name)),l=new Map(Array.from(this._baseToolDefinitions.entries()).filter(([d])=>o(d)).map(([d,b])=>[d,{definition:b,sourceInfo:I(`<builtin:${d}>`,{source:"builtin"})}]));for(const d of a)l.set(d.definition.name,{definition:d.definition,sourceInfo:d.sourceInfo});this._toolDefinitions=l,this._toolPromptSnippets=new Map(Array.from(l.values()).map(({definition:d})=>{const b=this._normalizePromptSnippet(d.promptSnippet);return b?[d.name,b]:void 0}).filter(d=>d!==void 0)),this._toolPromptGuidelines=new Map(Array.from(l.values()).map(({definition:d})=>{const b=this._normalizePromptGuidelines(d.promptGuidelines);return b.length>0?[d.name,b]:void 0}).filter(d=>d!==void 0));const c=this._extensionRunner,h=q(a,c),u=q(Array.from(this._baseToolDefinitions.values()).filter(d=>o(d.name)).map(d=>({definition:d,sourceInfo:I(`<builtin:${d.name}>`,{source:"builtin"})})),c),m=new Map(u.map(d=>[d.name,d]));for(const d of h)m.set(d.name,d);this._toolRegistry=m;const w=(e?.activeToolNames?[...e.activeToolNames]:[...s]).filter(d=>o(d));if(n)for(const d of this._toolRegistry.keys())n.has(d)&&w.push(d);else if(e?.includeAllExtensionTools)for(const d of h)w.push(d.name);else if(!e?.activeToolNames)for(const d of this._toolRegistry.keys())t.has(d)||w.push(d);this.setActiveToolsByName([...new Set(w)])}_buildRuntime(e){const t=this.settingsManager.getImageAutoResize(),s=this.settingsManager.getShellCommandPrefix(),n=this.settingsManager.getShellPath(),i=this._baseToolsOverride?Object.fromEntries(Object.entries(this._baseToolsOverride).map(([l,c])=>[l,ve(c)])):xe(this._cwd,{read:{autoResizeImages:t},bash:{commandPrefix:s,shellPath:n}});this._baseToolDefinitions=new Map(Object.entries(i).map(([l,c])=>[l,c]));const o=this._resourceLoader.getExtensions();if(e.flagValues)for(const[l,c]of e.flagValues)o.runtime.flagValues.set(l,c);this._extensionRunner=new fe(o.extensions,o.runtime,this._cwd,this.sessionManager,this._modelRegistry),this._extensionRunnerRef&&(this._extensionRunnerRef.current=this._extensionRunner),this._bindExtensionCore(this._extensionRunner),this._applyExtensionBindings(this._extensionRunner);const r=this._baseToolsOverride?Object.keys(this._baseToolsOverride):["read","bash","edit","write"],a=e.activeToolNames??r;this._refreshToolRegistry({activeToolNames:a,includeAllExtensionTools:e.includeAllExtensionTools})}async reload(){const e=this._extensionRunner.getFlagValues();await _e(this._extensionRunner,{type:"session_shutdown",reason:"reload"}),await this.settingsManager.reload(),ne(),await this._resourceLoader.reload(),this._buildRuntime({activeToolNames:this.getActiveToolNames(),flagValues:e,includeAllExtensionTools:!0}),(this._extensionUIContext||this._extensionCommandContextActions||this._extensionShutdownHandler||this._extensionErrorListener)&&(await this._extensionRunner.emit({type:"session_start",reason:"reload"}),await this.extendResourcesFromExtensions("reload"))}_isNonRetryableProviderLimitError(e){return/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(e)}_isRateLimited(e){return e.stopReason==="error"&&!!e.errorMessage&&/rate.?limit|429|too many requests|retry delay|quota/i.test(e.errorMessage)}_suggest(e,t){process.env.KODA_TIPS!=="0"&&(this._suggestedThisTask||this._suggestShown.has(e)||(this._suggestShown.add(e),this._suggestedThisTask=!0,this._emit({type:"suggest_feature",id:e,message:t})))}_isRetryableError(e){if(e.stopReason!=="error"||!e.errorMessage)return!1;const t=this.model?.contextWindow??0;if($(e,t))return!1;const s=e.errorMessage;return this._isNonRetryableProviderLimitError(s)?!1:/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(s)}async _prepareRetry(e){const t=this.settingsManager.getRetrySettings();if(!t.enabled)return!1;if(this._retryAttempt++,this._retryAttempt>t.maxRetries)return this._retryAttempt--,!1;const s=t.baseDelayMs*2**(this._retryAttempt-1);this._emit({type:"auto_retry_start",attempt:this._retryAttempt,maxAttempts:t.maxRetries,delayMs:s,errorMessage:e.errorMessage||"Unknown error"});const n=this.agent.state.messages;n.length>0&&n[n.length-1].role==="assistant"&&(this.agent.state.messages=n.slice(0,-1)),this._retryAbortController=new AbortController;try{await ae(s,this._retryAbortController.signal)}catch{const i=this._retryAttempt;return this._retryAttempt=0,this._emit({type:"auto_retry_end",success:!1,attempt:i,finalError:"Retry cancelled"}),!1}finally{this._retryAbortController=void 0}return!0}async _failoverModel(e){if(e.stopReason!=="error"||$(e,this.model?.contextWindow??0))return!1;const t=this.agent.state.model;this._failoverTried.add(`${t.provider}/${t.id}`);const s=(process.env.KODA_FAILOVER||"openadapter/glm-5.1,openadapter/GLM-5,openadapter/Kimi-K2.6,openadapter/Kimi-K2.5,openadapter/MiniMax-M2.7,openadapter/DeepSeek-V4-Pro").split(",").map(a=>a.trim()).filter(Boolean),n=[];for(const a of s){const l=a.indexOf("/"),[c,h]=l>=0?[a.slice(0,l),a.slice(l+1)]:[t.provider,a];if(this._failoverTried.has(`${c}/${h}`))continue;const u=this._modelRegistry.find(c,h);!u||S(u,t)||n.push({key:`${c}/${h}`,model:u})}if(n.length===0)return!1;const i=await this._fetchModelHealth(n[0].model);if(i){const a=f(c=>c?.health!=null&&c.health<=3,"isHealthy"),l=f(c=>c?.latency??6,"latKey");n.sort((c,h)=>{const u=i[c.model.id],m=i[h.model.id],w=a(u)?1:0,d=a(m)?1:0;return w!==d?d-w:l(u)-l(m)})}const o=n[0].model;this._failoverTried.add(n[0].key);const r=this.agent.state.messages;return r.length>0&&r[r.length-1].role==="assistant"&&(this.agent.state.messages=r.slice(0,-1)),this.agent.state.model=o,this._retryAttempt=0,this.sessionManager.appendModelChange(o.provider,o.id),this._emitModelSelect(o,t,"cycle"),this._trace("failover",`${t.id} \u2192 ${o.id} (provider error)`),this._emit({type:"model_switch_notice",message:`\u2191 ${t.id} hit a wall \u2014 Koda beefed up to ${o.id}`}),!0}async _fetchModelHealth(e){try{if(!e.baseUrl)return null;const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(!t?.apiKey)return null;const s=new AbortController,n=setTimeout(()=>s.abort(),2500);try{const i=await fetch(`${e.baseUrl.replace(/\/$/,"")}/model-health`,{headers:{Authorization:`Bearer ${t.apiKey}`,...t.headers||{}},signal:s.signal});return i.ok?(await i.json())?.models??null:null}finally{clearTimeout(n)}}catch{return null}}async _zenitsuGetHealth(){const e=Date.now();if(this._zenitsuHealthCache&&e-this._zenitsuHealthCache.at<3e4)return this._zenitsuHealthCache.models;const t=this._zenitsuPreferred||this.agent.state.model,s=await this._fetchModelHealth(t);return s&&(this._zenitsuHealthCache={at:e,models:s}),this._zenitsuHealthCache?.models}async _zenitsuPickModel(){this._zenitsuPreferred||(this._zenitsuPreferred=this.agent.state.model);const e=this._zenitsuPreferred,t=e.provider,s=Date.now(),n=await this._zenitsuGetHealth()||{},i=f(m=>this._zenitsuCooldown.get(`${t}/${m}`)??0,"cooldownOf"),o=(process.env.KODA_ZENITSU_LADDER||"glm-5.1,Kimi-K2.6,MiniMax-M2.7,GLM-4.7,GLM-5,Kimi-K2.5,DeepSeek-V4-Pro").split(",").map(m=>m.trim()).filter(Boolean),r=[],a=new Set;for(const m of[e.id,...o]){if(a.has(m)||(a.add(m),i(m)>s))continue;const w=this._modelRegistry.find(t,m);if(!w)continue;const d=n[m]||{};r.push({id:m,model:w,healthy:d.health!=null&&d.health<=3,lat:d.latency??6})}if(r.length===0)return e;const l=i(e.id)<=s,c=r.find(m=>m.id===e.id),h=n[e.id]?.health!=null&&n[e.id].health>3;if(l&&c&&!h){const w=c.lat<=5?r.filter(d=>d.id!==e.id&&d.healthy&&d.lat<c.lat).sort((d,b)=>d.lat-b.lat)[0]:void 0;return w?w.model:e}const u=r.filter(m=>m.healthy).sort((m,w)=>m.lat-w.lat);return u.length?u[0].model:r.sort((m,w)=>m.lat-w.lat)[0].model}_messageText(e){const t=e?.content;return typeof t=="string"?t:Array.isArray(t)?t.filter(s=>s?.type==="text").map(s=>s.text).join(`
13
+ ${n}`:l}catch(o){return this._extensionRunner.emitError({extensionPath:i.filePath,event:"skill_expansion",error:o instanceof Error?o.message:String(o)}),e}}async steer(e,t){e.startsWith("/")&&this._throwIfExtensionCommand(e);let s=this._expandSkillCommand(e);s=I(s,[...this.promptTemplates]),await this._queueSteer(s,t)}async followUp(e,t){e.startsWith("/")&&this._throwIfExtensionCommand(e);let s=this._expandSkillCommand(e);s=I(s,[...this.promptTemplates]),await this._queueFollowUp(s,t)}async _queueSteer(e,t){this._steeringMessages.push(e),this._emitQueueUpdate();const s=[{type:"text",text:e}];t&&s.push(...t),this.agent.steer({role:"user",content:s,timestamp:Date.now()})}async _queueFollowUp(e,t){this._followUpMessages.push(e),this._emitQueueUpdate();const s=[{type:"text",text:e}];t&&s.push(...t),this.agent.followUp({role:"user",content:s,timestamp:Date.now()})}_throwIfExtensionCommand(e){const t=e.indexOf(" "),s=t===-1?e.slice(1):e.slice(1,t);if(this._extensionRunner.getCommand(s))throw new Error(`Extension command "/${s}" cannot be queued. Use prompt() or execute the command when not streaming.`)}async sendCustomMessage(e,t){const s={role:"custom",customType:e.customType,content:e.content,display:e.display,details:e.details,timestamp:Date.now()};t?.deliverAs==="nextTurn"?this._pendingNextTurnMessages.push(s):this.isStreaming?t?.deliverAs==="followUp"?this.agent.followUp(s):this.agent.steer(s):t?.triggerTurn?await this._runAgentPrompt(s):(this.agent.state.messages.push(s),this.sessionManager.appendCustomMessageEntry(e.customType,e.content,e.display,e.details),this._emit({type:"message_start",message:s}),this._emit({type:"message_end",message:s}))}async sendUserMessage(e,t){let s,n;if(typeof e=="string")s=e;else{const i=[];n=[];for(const o of e)o.type==="text"?i.push(o.text):n.push(o);s=i.join(`
14
+ `),n.length===0&&(n=void 0)}await this.prompt(s,{expandPromptTemplates:!1,streamingBehavior:t?.deliverAs,images:n,source:"extension"})}clearQueue(){const e=[...this._steeringMessages],t=[...this._followUpMessages];return this._steeringMessages=[],this._followUpMessages=[],this.agent.clearAllQueues(),this._emitQueueUpdate(),{steering:e,followUp:t}}get pendingMessageCount(){return this._steeringMessages.length+this._followUpMessages.length}getSteeringMessages(){return this._steeringMessages}getFollowUpMessages(){return this._followUpMessages}get resourceLoader(){return this._resourceLoader}async abort(){this.abortRetry(),this.agent.abort(),await this.agent.waitForIdle()}async _emitModelSelect(e,t,s){T(t,e)||await this._extensionRunner.emit({type:"model_select",model:e,previousModel:t,source:s})}async setModel(e){if(!this._modelRegistry.hasConfiguredAuth(e))throw new Error(`No API key for ${e.provider}/${e.id}`);const t=this.model,s=this._getThinkingLevelForModelSwitch();this.agent.state.model=e,this._homeModel=e,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(e.provider,e.id),this.settingsManager.setDefaultModelAndProvider(e.provider,e.id),this.setThinkingLevel(s),await this._emitModelSelect(e,t,"set")}async cycleModel(e="forward"){return this._scopedModels.length>0?this._cycleScopedModel(e):this._cycleAvailableModel(e)}async _cycleScopedModel(e){const t=this._scopedModels.filter(a=>this._modelRegistry.hasConfiguredAuth(a.model));if(t.length<=1)return;const s=this.model;let n=t.findIndex(a=>T(a.model,s));n===-1&&(n=0);const i=t.length,o=e==="forward"?(n+1)%i:(n-1+i)%i,r=t[o],l=this._getThinkingLevelForModelSwitch(r.thinkingLevel);return this.agent.state.model=r.model,this._homeModel=r.model,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(r.model.provider,r.model.id),this.settingsManager.setDefaultModelAndProvider(r.model.provider,r.model.id),this.setThinkingLevel(l),await this._emitModelSelect(r.model,s,"cycle"),{model:r.model,thinkingLevel:this.thinkingLevel,isScoped:!0}}async _cycleAvailableModel(e){const t=await this._modelRegistry.getAvailable();if(t.length<=1)return;const s=this.model;let n=t.findIndex(a=>T(a,s));n===-1&&(n=0);const i=t.length,o=e==="forward"?(n+1)%i:(n-1+i)%i,r=t[o],l=this._getThinkingLevelForModelSwitch();return this.agent.state.model=r,this._homeModel=r,this._stickyModel=void 0,this._sessionEscalations=0,this.sessionManager.appendModelChange(r.provider,r.id),this.settingsManager.setDefaultModelAndProvider(r.provider,r.id),this.setThinkingLevel(l),await this._emitModelSelect(r,s,"cycle"),{model:r,thinkingLevel:this.thinkingLevel,isScoped:!1}}setThinkingLevel(e){const t=this.getAvailableThinkingLevels(),s=t.includes(e)?e:this._clampThinkingLevel(e,t),n=this.agent.state.thinkingLevel,i=s!==n;this.agent.state.thinkingLevel=s,i&&(this.sessionManager.appendThinkingLevelChange(s),(this.supportsThinking()||s!=="off")&&this.settingsManager.setDefaultThinkingLevel(s),this._emit({type:"thinking_level_changed",level:s}),this._extensionRunner.emit({type:"thinking_level_select",level:s,previousLevel:n}))}cycleThinkingLevel(){if(!this.supportsThinking())return;const e=this.getAvailableThinkingLevels(),s=(e.indexOf(this.thinkingLevel)+1)%e.length,n=e[s];return this.setThinkingLevel(n),n}getAvailableThinkingLevels(){return this.model?ie(this.model):ke}supportsThinking(){return!!this.model?.reasoning}_getThinkingLevelForModelSwitch(e){return e!==void 0?e:this.supportsThinking()?this.thinkingLevel:this.settingsManager.getDefaultThinkingLevel()??ge}_clampThinkingLevel(e,t){return this.model?se(this.model,e):"off"}setSteeringMode(e){this.agent.steeringMode=e,this.settingsManager.setSteeringMode(e)}setFollowUpMode(e){this.agent.followUpMode=e,this.settingsManager.setFollowUpMode(e)}async compact(e){this._disconnectFromAgent(),await this.abort(),this._compactionAbortController=new AbortController,this._emit({type:"compaction_start",reason:"manual"});try{if(!this.model)throw new Error(j());const{apiKey:t,headers:s}=await this._getCompactionRequestAuth(this.model),n=this.sessionManager.getBranch(),i=this.settingsManager.getCompactionSettings(),o=V(n,i);if(!o)throw n[n.length-1]?.type==="compaction"?new Error("Already compacted"):new Error("Nothing to compact (session too small)");let r,l=!1;if(this._extensionRunner.hasHandlers("session_before_compact")){const m=await this._extensionRunner.emit({type:"session_before_compact",preparation:o,branchEntries:n,customInstructions:e,signal:this._compactionAbortController.signal});if(m?.cancel)throw new Error("Compaction cancelled");m?.compaction&&(r=m.compaction,l=!0)}let a,c,d,u;if(r)a=r.summary,c=r.firstKeptEntryId,d=r.tokensBefore,u=r.details;else{const m=await W(o,this.model,t,s,e,this._compactionAbortController.signal,this.thinkingLevel,this.agent.streamFn);a=m.summary,c=m.firstKeptEntryId,d=m.tokensBefore,u=m.details}if(this._compactionAbortController.signal.aborted)throw new Error("Compaction cancelled");this.sessionManager.appendCompaction(a,c,d,u,l);const g=this.sessionManager.getEntries(),x=this.sessionManager.buildSessionContext();this.agent.state.messages=x.messages;const h=g.find(m=>m.type==="compaction"&&m.summary===a);this._extensionRunner&&h&&await this._extensionRunner.emit({type:"session_compact",compactionEntry:h,fromExtension:l});const b={summary:a,firstKeptEntryId:c,tokensBefore:d,details:u};return this._emit({type:"compaction_end",reason:"manual",result:b,aborted:!1,willRetry:!1}),b}catch(t){const s=t instanceof Error?t.message:String(t),n=s==="Compaction cancelled"||t instanceof Error&&t.name==="AbortError";throw this._emit({type:"compaction_end",reason:"manual",result:void 0,aborted:n,willRetry:!1,errorMessage:n?void 0:`Compaction failed: ${s}`}),t}finally{this._compactionAbortController=void 0,this._reconnectToAgent()}}abortCompaction(){this._compactionAbortController?.abort(),this._autoCompactionAbortController?.abort()}abortBranchSummary(){this._branchSummaryAbortController?.abort()}abortOverseer(){this._overseerAbortController?.abort()}abortRecap(){this._recapAbortController?.abort()}async _checkCompaction(e,t=!0){const s=this.settingsManager.getCompactionSettings();if(!s.enabled||t&&e.stopReason==="aborted")return!1;const n=this.model?.contextWindow??0,i=this.model&&e.provider===this.model.provider&&e.model===this.model.id,o=Q(this.sessionManager.getBranch());if(o!==null&&e.timestamp<=new Date(o.timestamp).getTime())return!1;if(i&&$(e,n)){if(this._overflowRecoveryAttempted)return this._emit({type:"compaction_end",reason:"overflow",result:void 0,aborted:!1,willRetry:!1,errorMessage:"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."}),!1;this._overflowRecoveryAttempted=!0;const a=this.agent.state.messages;return a.length>0&&a[a.length-1].role==="assistant"&&(this.agent.state.messages=a.slice(0,-1)),await this._runAutoCompaction("overflow",!0)}let l;if(e.stopReason==="error"){const a=this.agent.state.messages,c=q(a);if(c.lastUsageIndex===null)return!1;const d=a[c.lastUsageIndex];if(o&&d.role==="assistant"&&d.timestamp<=new Date(o.timestamp).getTime())return!1;l=c.tokens}else l=G(e.usage);return me(l,n,s)?await this._runAutoCompaction("threshold",!1):!1}async _runAutoCompaction(e,t){const s=this.settingsManager.getCompactionSettings();this._emit({type:"compaction_start",reason:e}),this._autoCompactionAbortController=new AbortController;try{if(!this.model)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;let n,i;if(this.agent.streamFn===H){const p=await this._modelRegistry.getApiKeyAndHeaders(this.model);if(!p.ok||!p.apiKey)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;n=p.apiKey,i=p.headers}else({apiKey:n,headers:i}=await this._getCompactionRequestAuth(this.model));const o=this.sessionManager.getBranch(),r=V(o,s);if(!r)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1}),!1;let l,a=!1;if(this._extensionRunner.hasHandlers("session_before_compact")){const p=await this._extensionRunner.emit({type:"session_before_compact",preparation:r,branchEntries:o,customInstructions:void 0,signal:this._autoCompactionAbortController.signal});if(p?.cancel)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!0,willRetry:!1}),!1;p?.compaction&&(l=p.compaction,a=!0)}let c,d,u,g;if(l)c=l.summary,d=l.firstKeptEntryId,u=l.tokensBefore,g=l.details;else{const p=await W(r,this.model,n,i,void 0,this._autoCompactionAbortController.signal,this.thinkingLevel,this.agent.streamFn);c=p.summary,d=p.firstKeptEntryId,u=p.tokensBefore,g=p.details}if(this._autoCompactionAbortController.signal.aborted)return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!0,willRetry:!1}),!1;this.sessionManager.appendCompaction(c,d,u,g,a);const x=this.sessionManager.getEntries(),h=this.sessionManager.buildSessionContext();this.agent.state.messages=h.messages;const b=x.find(p=>p.type==="compaction"&&p.summary===c);this._extensionRunner&&b&&await this._extensionRunner.emit({type:"session_compact",compactionEntry:b,fromExtension:a});const m={summary:c,firstKeptEntryId:d,tokensBefore:u,details:g};if(this._emit({type:"compaction_end",reason:e,result:m,aborted:!1,willRetry:t}),t){const p=this.agent.state.messages,y=p[p.length-1];return y?.role==="assistant"&&y.stopReason==="error"&&(this.agent.state.messages=p.slice(0,-1)),!0}return this.agent.hasQueuedMessages()}catch(n){const i=n instanceof Error?n.message:"compaction failed";return this._emit({type:"compaction_end",reason:e,result:void 0,aborted:!1,willRetry:!1,errorMessage:e==="overflow"?`Context overflow recovery failed: ${i}`:`Auto-compaction failed: ${i}`}),!1}finally{this._autoCompactionAbortController=void 0}}setAutoCompactionEnabled(e){this.settingsManager.setCompactionEnabled(e)}get autoCompactionEnabled(){return this.settingsManager.getCompactionEnabled()}async bindExtensions(e){e.uiContext!==void 0&&(this._extensionUIContext=e.uiContext),e.mode!==void 0&&(this._extensionMode=e.mode),e.commandContextActions!==void 0&&(this._extensionCommandContextActions=e.commandContextActions),e.abortHandler!==void 0&&(this._extensionAbortHandler=e.abortHandler),e.shutdownHandler!==void 0&&(this._extensionShutdownHandler=e.shutdownHandler),e.onError!==void 0&&(this._extensionErrorListener=e.onError),this._applyExtensionBindings(this._extensionRunner),await this._extensionRunner.emit(this._sessionStartEvent),await this.extendResourcesFromExtensions(this._sessionStartEvent.reason==="reload"?"reload":"startup")}async extendResourcesFromExtensions(e){if(!this._extensionRunner.hasHandlers("resources_discover"))return;const{skillPaths:t,promptPaths:s,themePaths:n}=await this._extensionRunner.emitResourcesDiscover(this._cwd,e);if(t.length===0&&s.length===0&&n.length===0)return;const i={skillPaths:this.buildExtensionResourcePaths(t),promptPaths:this.buildExtensionResourcePaths(s),themePaths:this.buildExtensionResourcePaths(n)};this._resourceLoader.extendResources(i),this._baseSystemPrompt=this._rebuildSystemPrompt(this.getActiveToolNames()),this.agent.state.systemPrompt=this._baseSystemPrompt}buildExtensionResourcePaths(e){return e.map(t=>{const s=this.getExtensionSourceLabel(t.extensionPath),n=t.extensionPath.startsWith("<")?void 0:B(t.extensionPath);return{path:t.path,metadata:{source:s,scope:"temporary",origin:"top-level",baseDir:n}}})}getExtensionSourceLabel(e){return e.startsWith("<")?`extension:${e.replace(/[<>]/g,"")}`:`extension:${ee(e).replace(/\.(ts|js)$/,"")}`}_applyExtensionBindings(e){e.setUIContext(this._extensionUIContext,this._extensionMode),e.bindCommandContext(this._extensionCommandContextActions),this._extensionErrorUnsubscriber?.(),this._extensionErrorUnsubscriber=this._extensionErrorListener?e.onError(this._extensionErrorListener):void 0}_refreshCurrentModelFromRegistry(){const e=this.model;if(!e)return;const t=this._modelRegistry.find(e.provider,e.id);!t||t===e||(this.agent.state.model=t)}_bindExtensionCore(e){const t=w(()=>{const s=e.getRegisteredCommands().map(o=>({name:o.invocationName,description:o.description,source:"extension",sourceInfo:o.sourceInfo})),n=this.promptTemplates.map(o=>({name:o.name,description:o.description,source:"prompt",sourceInfo:o.sourceInfo})),i=this._resourceLoader.getSkills().skills.map(o=>({name:`skill:${o.name}`,description:o.description,source:"skill",sourceInfo:o.sourceInfo}));return[...s,...n,...i]},"getCommands");e.bindCore({sendMessage:w((s,n)=>{this.sendCustomMessage(s,n).catch(i=>{e.emitError({extensionPath:"<runtime>",event:"send_message",error:i instanceof Error?i.message:String(i)})})},"sendMessage"),sendUserMessage:w((s,n)=>{this.sendUserMessage(s,n).catch(i=>{e.emitError({extensionPath:"<runtime>",event:"send_user_message",error:i instanceof Error?i.message:String(i)})})},"sendUserMessage"),appendEntry:w((s,n)=>{this.sessionManager.appendCustomEntry(s,n)},"appendEntry"),setSessionName:w(s=>{this.setSessionName(s)},"setSessionName"),getSessionName:w(()=>this.sessionManager.getSessionName(),"getSessionName"),setLabel:w((s,n)=>{this.sessionManager.appendLabelChange(s,n)},"setLabel"),getActiveTools:w(()=>this.getActiveToolNames(),"getActiveTools"),getAllTools:w(()=>this.getAllTools(),"getAllTools"),setActiveTools:w(s=>this.setActiveToolsByName(s),"setActiveTools"),refreshTools:w(()=>this._refreshToolRegistry(),"refreshTools"),getCommands:t,setModel:w(async s=>this.modelRegistry.hasConfiguredAuth(s)?(await this.setModel(s),!0):!1,"setModel"),getThinkingLevel:w(()=>this.thinkingLevel,"getThinkingLevel"),setThinkingLevel:w(s=>this.setThinkingLevel(s),"setThinkingLevel")},{getModel:w(()=>this.model,"getModel"),isIdle:w(()=>!this.isStreaming,"isIdle"),getSignal:w(()=>this.agent.signal,"getSignal"),abort:w(()=>{if(this._extensionAbortHandler){this._extensionAbortHandler();return}this.abort()},"abort"),hasPendingMessages:w(()=>this.pendingMessageCount>0,"hasPendingMessages"),shutdown:w(()=>{this._extensionShutdownHandler?.()},"shutdown"),getContextUsage:w(()=>this.getContextUsage(),"getContextUsage"),compact:w(s=>{(async()=>{try{const n=await this.compact(s?.customInstructions);s?.onComplete?.(n)}catch(n){const i=n instanceof Error?n:new Error(String(n));s?.onError?.(i)}})()},"compact"),getSystemPrompt:w(()=>this.systemPrompt,"getSystemPrompt"),getSystemPromptOptions:w(()=>this._baseSystemPromptOptions,"getSystemPromptOptions")},{registerProvider:w((s,n)=>{this._modelRegistry.registerProvider(s,n),this._refreshCurrentModelFromRegistry()},"registerProvider"),unregisterProvider:w(s=>{this._modelRegistry.unregisterProvider(s),this._refreshCurrentModelFromRegistry()},"unregisterProvider")})}_refreshToolRegistry(e){const t=new Set(this._toolRegistry.keys()),s=this.getActiveToolNames(),n=this._allowedToolNames,i=this._excludedToolNames,o=w(h=>(!n||n.has(h))&&!i?.has(h),"isAllowedTool"),l=[...this._extensionRunner.getAllRegisteredTools(),...this._customTools.map(h=>({definition:h,sourceInfo:N(`<sdk:${h.name}>`,{source:"sdk"})}))].filter(h=>o(h.definition.name)),a=new Map(Array.from(this._baseToolDefinitions.entries()).filter(([h])=>o(h)).map(([h,b])=>[h,{definition:b,sourceInfo:N(`<builtin:${h}>`,{source:"builtin"})}]));for(const h of l)a.set(h.definition.name,{definition:h.definition,sourceInfo:h.sourceInfo});this._toolDefinitions=a,this._toolPromptSnippets=new Map(Array.from(a.values()).map(({definition:h})=>{const b=this._normalizePromptSnippet(h.promptSnippet);return b?[h.name,b]:void 0}).filter(h=>h!==void 0)),this._toolPromptGuidelines=new Map(Array.from(a.values()).map(({definition:h})=>{const b=this._normalizePromptGuidelines(h.promptGuidelines);return b.length>0?[h.name,b]:void 0}).filter(h=>h!==void 0));const c=this._extensionRunner,d=J(l,c),u=J(Array.from(this._baseToolDefinitions.values()).filter(h=>o(h.name)).map(h=>({definition:h,sourceInfo:N(`<builtin:${h.name}>`,{source:"builtin"})})),c),g=new Map(u.map(h=>[h.name,h]));for(const h of d)g.set(h.name,h);this._toolRegistry=g;const x=(e?.activeToolNames?[...e.activeToolNames]:[...s]).filter(h=>o(h));if(n)for(const h of this._toolRegistry.keys())n.has(h)&&x.push(h);else if(e?.includeAllExtensionTools)for(const h of d)x.push(h.name);else if(!e?.activeToolNames)for(const h of this._toolRegistry.keys())t.has(h)||x.push(h);this.setActiveToolsByName([...new Set(x)])}_buildRuntime(e){const t=this.settingsManager.getImageAutoResize(),s=this.settingsManager.getShellCommandPrefix(),n=this.settingsManager.getShellPath(),i=this._baseToolsOverride?Object.fromEntries(Object.entries(this._baseToolsOverride).map(([a,c])=>[a,Me(c)])):Ae(this._cwd,{read:{autoResizeImages:t},bash:{commandPrefix:s,shellPath:n}});this._baseToolDefinitions=new Map(Object.entries(i).map(([a,c])=>[a,c]));const o=this._resourceLoader.getExtensions();if(e.flagValues)for(const[a,c]of e.flagValues)o.runtime.flagValues.set(a,c);this._extensionRunner=new ye(o.extensions,o.runtime,this._cwd,this.sessionManager,this._modelRegistry),this._extensionRunnerRef&&(this._extensionRunnerRef.current=this._extensionRunner),this._bindExtensionCore(this._extensionRunner),this._applyExtensionBindings(this._extensionRunner);const r=this._baseToolsOverride?Object.keys(this._baseToolsOverride):["read","bash","edit","write"],l=e.activeToolNames??r;this._refreshToolRegistry({activeToolNames:l,includeAllExtensionTools:e.includeAllExtensionTools})}async reload(){const e=this._extensionRunner.getFlagValues();await we(this._extensionRunner,{type:"session_shutdown",reason:"reload"}),await this.settingsManager.reload(),re(),await this._resourceLoader.reload(),this._buildRuntime({activeToolNames:this.getActiveToolNames(),flagValues:e,includeAllExtensionTools:!0}),(this._extensionUIContext||this._extensionCommandContextActions||this._extensionShutdownHandler||this._extensionErrorListener)&&(await this._extensionRunner.emit({type:"session_start",reason:"reload"}),await this.extendResourcesFromExtensions("reload"))}_isNonRetryableProviderLimitError(e){return/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(e)}_isRateLimited(e){return e.stopReason==="error"&&!!e.errorMessage&&/rate.?limit|429|too many requests|retry delay|quota/i.test(e.errorMessage)}_suggest(e,t){process.env.KODA_TIPS!=="0"&&(this._suggestedThisTask||this._suggestShown.has(e)||(this._suggestShown.add(e),this._suggestedThisTask=!0,this._emit({type:"suggest_feature",id:e,message:t})))}_isRetryableError(e){if(e.stopReason!=="error"||!e.errorMessage)return!1;const t=this.model?.contextWindow??0;if($(e,t))return!1;const s=e.errorMessage;return this._isNonRetryableProviderLimitError(s)?!1:/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(s)}async _prepareRetry(e){const t=this.settingsManager.getRetrySettings();if(!t.enabled)return!1;if(this._retryAttempt++,this._retryAttempt>t.maxRetries)return this._retryAttempt--,!1;const s=t.baseDelayMs*2**(this._retryAttempt-1);this._emit({type:"auto_retry_start",attempt:this._retryAttempt,maxAttempts:t.maxRetries,delayMs:s,errorMessage:e.errorMessage||"Unknown error"});const n=this.agent.state.messages;n.length>0&&n[n.length-1].role==="assistant"&&(this.agent.state.messages=n.slice(0,-1)),this._retryAbortController=new AbortController;try{await ce(s,this._retryAbortController.signal)}catch{const i=this._retryAttempt;return this._retryAttempt=0,this._emit({type:"auto_retry_end",success:!1,attempt:i,finalError:"Retry cancelled"}),!1}finally{this._retryAbortController=void 0}return!0}async _failoverModel(e){if(e.stopReason!=="error"||$(e,this.model?.contextWindow??0))return!1;const t=this.agent.state.model;this._failoverTried.add(`${t.provider}/${t.id}`);const s=(process.env.KODA_FAILOVER||"openadapter/glm-5.1,openadapter/GLM-5,openadapter/Kimi-K2.6,openadapter/Kimi-K2.5,openadapter/MiniMax-M2.7,openadapter/DeepSeek-V4-Pro").split(",").map(f=>f.trim()).filter(Boolean),n=await this._fetchModelHealth(t),i=w(f=>({max:4,pro:3,lite:2,freemium:1})[(f||"").toLowerCase()]??0,"tierRank"),o=w(f=>f?.health!=null&&f.health<=3,"isHealthy"),r=w(f=>f?.latency??6,"latKey"),l=new Set,a=[],c=w((f,M,_)=>{const A=`${f}/${M}`;if(l.has(A)||this._failoverTried.has(A))return;const k=this._modelRegistry.find(f,M);!k||T(k,t)||(l.add(A),a.push({key:A,model:k,chainIdx:_}))},"consider");if(s.forEach((f,M)=>{const _=f.indexOf("/"),[A,k]=_>=0?[f.slice(0,_),f.slice(_+1)]:[t.provider,f];c(A,k,M)}),n)for(const f of Object.keys(n))f.includes("/")||f.includes("->")||c(t.provider,f,Number.POSITIVE_INFINITY);if(a.length===0)return!1;const d=w((f,M)=>{const _=n?.[f.model.id],A=n?.[M.model.id],k=o(_)?1:0,R=o(A)?1:0;if(k!==R)return R-k;const D=i(_?.tier),U=i(A?.tier);return D!==U?U-D:f.chainIdx!==M.chainIdx?f.chainIdx-M.chainIdx:r(_)-r(A)},"order"),u=i(n?.[t.id]?.tier),g=this._isRateLimited(e),x=a.filter(f=>i(n?.[f.model.id]?.tier)===u),h=a.filter(f=>i(n?.[f.model.id]?.tier)<u);let b;g&&u>0&&h.length?b=h:!g&&u>0&&x.length?b=x:b=a,b.sort(d);const m=b[0].model;this._failoverTried.add(b[0].key);const p=this.agent.state.messages;p.length>0&&p[p.length-1].role==="assistant"&&(this.agent.state.messages=p.slice(0,-1)),this.agent.state.model=m,this._retryAttempt=0,this.sessionManager.appendModelChange(m.provider,m.id),this._emitModelSelect(m,t,"cycle"),this._trace("failover",`${t.id} \u2192 ${m.id} (${g?"rate-limited":"provider error"})`);const y=i(n?.[m.id]?.tier);let v;return u&&y&&y<u?v=`\u2193 ${t.id} ${g?"rate-limited":"unavailable"} \u2014 Koda dropped to ${m.id} (lower tier) to keep going`:u&&y&&y>u?v=`\u2191 ${t.id} hit a wall \u2014 Koda beefed up to ${m.id}`:v=`\xBB ${t.id} ${g?"rate-limited":"hit a wall"} \u2014 Koda switched to ${m.id}`,this._emit({type:"model_switch_notice",message:v}),!0}async _fetchModelHealth(e){try{if(!e.baseUrl)return null;const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(!t?.apiKey)return null;const s=new AbortController,n=setTimeout(()=>s.abort(),2500);try{const i=await fetch(`${e.baseUrl.replace(/\/$/,"")}/model-health`,{headers:{Authorization:`Bearer ${t.apiKey}`,...t.headers||{}},signal:s.signal});return i.ok?(await i.json())?.models??null:null}finally{clearTimeout(n)}}catch{return null}}async _zenitsuGetHealth(){const e=Date.now();if(this._zenitsuHealthCache&&e-this._zenitsuHealthCache.at<3e4)return this._zenitsuHealthCache.models;const t=this._zenitsuPreferred||this.agent.state.model,s=await this._fetchModelHealth(t);return s&&(this._zenitsuHealthCache={at:e,models:s}),this._zenitsuHealthCache?.models}async _zenitsuPickModel(){this._zenitsuPreferred||(this._zenitsuPreferred=this.agent.state.model);const e=this._zenitsuPreferred,t=e.provider,s=Date.now(),n=await this._zenitsuGetHealth()||{},i=w(g=>this._zenitsuCooldown.get(`${t}/${g}`)??0,"cooldownOf"),o=(process.env.KODA_ZENITSU_LADDER||"glm-5.1,Kimi-K2.6,MiniMax-M2.7,GLM-4.7,GLM-5,Kimi-K2.5,DeepSeek-V4-Pro").split(",").map(g=>g.trim()).filter(Boolean),r=[],l=new Set;for(const g of[e.id,...o]){if(l.has(g)||(l.add(g),i(g)>s))continue;const x=this._modelRegistry.find(t,g);if(!x)continue;const h=n[g]||{};r.push({id:g,model:x,healthy:h.health!=null&&h.health<=3,lat:h.latency??6})}if(r.length===0)return e;const a=i(e.id)<=s,c=r.find(g=>g.id===e.id),d=n[e.id]?.health!=null&&n[e.id].health>3;if(a&&c&&!d){const x=c.lat<=5?r.filter(h=>h.id!==e.id&&h.healthy&&h.lat<c.lat).sort((h,b)=>h.lat-b.lat)[0]:void 0;return x?x.model:e}const u=r.filter(g=>g.healthy).sort((g,x)=>g.lat-x.lat);return u.length?u[0].model:r.sort((g,x)=>g.lat-x.lat)[0].model}_messageText(e){const t=e?.content;return typeof t=="string"?t:Array.isArray(t)?t.filter(s=>s?.type==="text").map(s=>s.text).join(`
15
15
  `):""}_goalFromMessages(e){const t=Array.isArray(e)?e:[e];for(let s=t.length-1;s>=0;s--)if(t[s]?.role==="user"){const n=this._messageText(t[s]).trim();if(n)return n}}_overseerContext(){const e=[];for(const t of this.agent.state.messages.slice(-12)){const s=t?.role;if(s==="assistant"){const n=this._messageText(t).trim();n&&e.push(`assistant: ${n.slice(0,600)}`);for(const i of Array.isArray(t.content)?t.content:[])(i?.type==="toolUse"||i?.type==="tool_use")&&e.push(` \u2192 tool ${i.name}(${JSON.stringify(i.input??i.args??{}).slice(0,160)})`)}else(s==="toolResult"||s==="tool")&&e.push(` result(${t.toolName??""}): ${this._messageText(t).slice(0,300)}`)}return e.join(`
16
- `).slice(-4e3)}async _getModelTier(e){if(!this._tierCache){this._tierCache=new Map;try{if(e.baseUrl){const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(t?.apiKey){const s=new AbortController,n=setTimeout(()=>s.abort(),3e3);try{const i=await fetch(`${e.baseUrl.replace(/\/$/,"")}/models`,{headers:{Authorization:`Bearer ${t.apiKey}`,...t.headers||{}},signal:s.signal});if(i.ok){const o=await i.json();for(const r of o.data??[])r.tier&&this._tierCache.set(r.id,r.tier)}}finally{clearTimeout(n)}}}}catch{}}return this._tierCache.get(e.id)}_isDegenerateOutput(e){const t=this._messageText(e);if(t.length<200)return!1;const s=t.match(/(.{3,60}?)\1{6,}/s);if(s&&s[0].length>150)return!0;const n=t.split(/\s+/).filter(Boolean);return n.length>60&&new Set(n).size/n.length<.15}async _escalate(e,t){if(this._escalationsDone>=2)return!1;const s=this.agent.state.model,n=(process.env.KODA_ESCALATE||"GLM-4.7,glm-5.1").split(",").map(r=>r.trim()).filter(Boolean);let i;for(const r of n){if(r===s.id||this._escalateTried.has(r))continue;const a=this._modelRegistry.find(s.provider,r);if(a){i=a;break}}if(!i)return!1;if(this._escalationsDone++,this._sessionEscalations++,this._escalateTried.add(i.id),this._sessionEscalations>=2&&(this._stickyModel=i),t?.stripLast){const r=this.agent.state.messages;r.length>0&&r[r.length-1].role==="assistant"&&(this.agent.state.messages=r.slice(0,-1))}this.agent.state.model=i,this._retryAttempt=0,this._overseerRounds=0,this.sessionManager.appendModelChange(i.provider,i.id),this._emitModelSelect(i,s,"cycle");const o=this._stickyModel?" (staying on it \u2014 this session needs the muscle)":"";return this._trace("escalate",`${s.id} \u2192 ${i.id}${this._stickyModel?" [sticky]":""} \u2014 ${e}`),this._emit({type:"model_switch_notice",message:`\u25B2 Koda escalated to ${i.id} \u2014 ${e}${o}`}),!0}listSkillNames(){return this._resourceLoader.getSkills().skills.map(e=>e.name)}getPinnedSkills(){return[...this._pinnedSkills.keys()]}pinSkill(e){const t=this._resourceLoader.getSkills().skills.find(s=>s.name===e);if(!t)return!1;try{const s=F(T(t.filePath,"utf-8")).trim();return this._pinnedSkills.set(e,`<active_skill name="${t.name}" location="${t.filePath}">
16
+ `).slice(-4e3)}async _getModelTier(e){if(!this._tierCache){this._tierCache=new Map;try{if(e.baseUrl){const t=await this._modelRegistry.getApiKeyAndHeaders(e);if(t?.apiKey){const s=new AbortController,n=setTimeout(()=>s.abort(),3e3);try{const i=await fetch(`${e.baseUrl.replace(/\/$/,"")}/models`,{headers:{Authorization:`Bearer ${t.apiKey}`,...t.headers||{}},signal:s.signal});if(i.ok){const o=await i.json();for(const r of o.data??[])r.tier&&this._tierCache.set(r.id,r.tier)}}finally{clearTimeout(n)}}}}catch{}}return this._tierCache.get(e.id)}_isDegenerateOutput(e){const t=this._messageText(e);if(t.length<200)return!1;const s=t.match(/(.{3,60}?)\1{6,}/s);if(s&&s[0].length>150)return!0;const n=t.split(/\s+/).filter(Boolean);return n.length>60&&new Set(n).size/n.length<.15}async _escalate(e,t){if(this._escalationsDone>=2)return!1;const s=this.agent.state.model,n=(process.env.KODA_ESCALATE||"GLM-4.7,glm-5.1").split(",").map(r=>r.trim()).filter(Boolean);let i;for(const r of n){if(r===s.id||this._escalateTried.has(r))continue;const l=this._modelRegistry.find(s.provider,r);if(l){i=l;break}}if(!i)return!1;if(this._escalationsDone++,this._sessionEscalations++,this._escalateTried.add(i.id),this._sessionEscalations>=2&&(this._stickyModel=i),t?.stripLast){const r=this.agent.state.messages;r.length>0&&r[r.length-1].role==="assistant"&&(this.agent.state.messages=r.slice(0,-1))}this.agent.state.model=i,this._retryAttempt=0,this._overseerRounds=0,this.sessionManager.appendModelChange(i.provider,i.id),this._emitModelSelect(i,s,"cycle");const o=this._stickyModel?" (staying on it \u2014 this session needs the muscle)":"";return this._trace("escalate",`${s.id} \u2192 ${i.id}${this._stickyModel?" [sticky]":""} \u2014 ${e}`),this._emit({type:"model_switch_notice",message:`\u25B2 Koda escalated to ${i.id} \u2014 ${e}${o}`}),!0}listSkillNames(){return this._resourceLoader.getSkills().skills.map(e=>e.name)}getPinnedSkills(){return[...this._pinnedSkills.keys()]}pinSkill(e){const t=this._resourceLoader.getSkills().skills.find(s=>s.name===e);if(!t)return!1;try{const s=z(E(t.filePath,"utf-8")).trim();return this._pinnedSkills.set(e,`<active_skill name="${t.name}" location="${t.filePath}">
17
17
  References are relative to ${t.baseDir}. Follow these instructions for this session:
18
18
 
19
19
  ${s}
20
- </active_skill>`),this._refreshSystemPrompt(),!0}catch{return!1}}unpinSkill(e){const t=this._pinnedSkills.delete(e);return t&&this._refreshSystemPrompt(),t}_refreshSystemPrompt(){this._baseSystemPrompt=this._rebuildSystemPrompt(this.getActiveToolNames()),this.agent.state.systemPrompt=this._baseSystemPrompt}getSessionGoal(){return this._sessionGoal}clearSessionGoal(){this._sessionGoal=void 0,this._emit({type:"goal_cleared"})}async setSessionGoal(e){const t=e.trim();if(!t)return;const s=await this._generateMilestones(t);this._sessionGoal={text:t,milestones:s.map(n=>({text:n,done:!1})),achieved:!1},this._emit({type:"goal_set",text:t,milestones:this._sessionGoal.milestones})}async _generateMilestones(e){const t=[e];try{const s=this.agent.state.model,n=this._modelRegistry.find(s.provider,process.env.KODA_OVERSEER_MODEL||"GLM-4.7")||s;if(!n.baseUrl)return t;const i=await this._modelRegistry.getApiKeyAndHeaders(n);if(!i?.apiKey)return t;const o=await fetch(`${n.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${i.apiKey}`,"Content-Type":"application/json",...i.headers||{}},body:JSON.stringify({model:n.id,temperature:0,messages:[{role:"system",content:"Break the user's session goal into 3-6 concrete, verifiable milestones (short imperative phrases, in order). Reply with ONLY a JSON array of strings."},{role:"user",content:e}]})});if(!o.ok)return t;let a=((await o.json())?.choices?.[0]?.message?.content??"").trim();a=a.replace(/<think>[\s\S]*?<\/think>/gi,"");const l=a.match(/\[[\s\S]*\]/);l&&(a=l[0]);const c=JSON.parse(a),h=Array.isArray(c)?c.filter(u=>typeof u=="string"&&u.trim()).map(u=>u.trim()).slice(0,6):[];return h.length?h:t}catch{return t}}_trace(e,t){this._harnessTrace.push({ts:Date.now(),kind:e,detail:t}),this._harnessTrace.length>60&&this._harnessTrace.shift()}getHarnessTrace(){return[...this._harnessTrace]}_gatewayOpen(){return Date.now()<this._gatewayCircuit.openUntil}_gatewayResult(e){e?this._gatewayCircuit.failures=0:++this._gatewayCircuit.failures>=3&&(this._gatewayCircuit.openUntil=Date.now()+3e4,this._gatewayCircuit.failures=0,this._trace("circuit","gateway calls paused 30s after repeated failures"))}_detectVerify(){if(this._verifyCmd!==void 0)return this._verifyCmd;const e=f(s=>(this._verifyCmd=s,s),"set"),t=process.env.KODA_VERIFY;if(t==="0")return e(null);if(t)return e(t);try{const s=this._cwd,n=C(s,"package.json");if(k(n)){const i=JSON.parse(T(n,"utf-8")),o=i.scripts||{},r=Object.keys({...i.dependencies||{},...i.devDependencies||{}}).join(" ");if(/\b(react|vue|svelte|next|nuxt|vite|@angular\/core|solid-js|preact|astro)\b/i.test(r)){const l=[];if(o.typecheck?l.push("npm run typecheck"):o["type-check"]?l.push("npm run type-check"):k(C(s,"tsconfig.json"))&&l.push("npx tsc --noEmit"),o.lint&&l.push("npm run lint"),l.length)return e(l.join(" && "));if(o.build)return e("npm run build")}for(const l of["typecheck","type-check","build","lint"])if(o[l])return e(`npm run ${l}`)}if(k(C(s,"tsconfig.json")))return e("npx tsc --noEmit");if(k(C(s,"go.mod")))return e("go build ./...");if(k(C(s,"Cargo.toml")))return e("cargo check")}catch{}return e(null)}async _runVerification(e,t){try{const s=await me("sh",["-c",e],this._cwd,{timeout:12e4,signal:t}),n=`${s.stdout}
21
- ${s.stderr}`.trim();return{passed:s.code===0&&!s.killed,output:n.slice(-1500)}}catch(s){return{passed:!0,output:`(verify could not run: ${s.message})`}}}async _overseerCheck(e){if(e.stopReason==="error")return!1;const t=this.agent.state.model,s=this._sessionGoal,n=!!s&&!s.achieved,i=await this._getModelTier(t),o=process.env.KODA_OVERSEER!=="0"&&this._overseerRounds<2&&!!this._currentGoal&&!!i&&i!=="max",r=this._codeChangedThisTask?this._detectVerify():null;if(!r&&!o&&!n)return!1;this._emit({type:"overseer_start",model:t.id}),this._overseerAbortController=new AbortController;const a=this._overseerAbortController.signal;try{let l;if(r){const h=await this._runVerification(r,a);if(a.aborted)return this._trace("overseer","cancelled during verify"),this._emit({type:"overseer_end",outcome:"skipped",detail:"cancelled"}),!1;if(this._trace("verify",`${r} \u2192 ${h.passed?"pass":"FAIL"}`),l=h.passed,!h.passed){const u=this._overseerRounds>=1&&await this._escalate("build/tests still failing");return u||this._overseerRounds++,this.agent.followUp({role:"user",content:[{type:"text",text:`Not done \u2014 \`${r}\` failed. Fix the root cause and re-run it until it passes:
20
+ </active_skill>`),this._refreshSystemPrompt(),!0}catch{return!1}}unpinSkill(e){const t=this._pinnedSkills.delete(e);return t&&this._refreshSystemPrompt(),t}_refreshSystemPrompt(){this._baseSystemPrompt=this._rebuildSystemPrompt(this.getActiveToolNames()),this.agent.state.systemPrompt=this._baseSystemPrompt}getSessionGoal(){return this._sessionGoal}clearSessionGoal(){this._sessionGoal=void 0,this._emit({type:"goal_cleared"})}async setSessionGoal(e){const t=e.trim();if(!t)return;const s=await this._generateMilestones(t);this._sessionGoal={text:t,milestones:s.map(n=>({text:n,done:!1})),achieved:!1},this._emit({type:"goal_set",text:t,milestones:this._sessionGoal.milestones})}async _generateMilestones(e){const t=[e];try{const s=this.agent.state.model,n=this._modelRegistry.find(s.provider,process.env.KODA_OVERSEER_MODEL||"GLM-4.7")||s;if(!n.baseUrl)return t;const i=await this._modelRegistry.getApiKeyAndHeaders(n);if(!i?.apiKey)return t;const o=await fetch(`${n.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${i.apiKey}`,"Content-Type":"application/json",...i.headers||{}},body:JSON.stringify({model:n.id,temperature:0,messages:[{role:"system",content:"Break the user's session goal into 3-6 concrete, verifiable milestones (short imperative phrases, in order). Reply with ONLY a JSON array of strings."},{role:"user",content:e}]})});if(!o.ok)return t;let l=((await o.json())?.choices?.[0]?.message?.content??"").trim();l=l.replace(/<think>[\s\S]*?<\/think>/gi,"");const a=l.match(/\[[\s\S]*\]/);a&&(l=a[0]);const c=JSON.parse(l),d=Array.isArray(c)?c.filter(u=>typeof u=="string"&&u.trim()).map(u=>u.trim()).slice(0,6):[];return d.length?d:t}catch{return t}}_trace(e,t){this._harnessTrace.push({ts:Date.now(),kind:e,detail:t}),this._harnessTrace.length>60&&this._harnessTrace.shift()}getHarnessTrace(){return[...this._harnessTrace]}_gatewayOpen(){return Date.now()<this._gatewayCircuit.openUntil}_gatewayResult(e){e?this._gatewayCircuit.failures=0:++this._gatewayCircuit.failures>=3&&(this._gatewayCircuit.openUntil=Date.now()+3e4,this._gatewayCircuit.failures=0,this._trace("circuit","gateway calls paused 30s after repeated failures"))}_detectVerify(){if(this._verifyCmd!==void 0)return this._verifyCmd;const e=w(s=>(this._verifyCmd=s,s),"set"),t=process.env.KODA_VERIFY;if(t==="0")return e(null);if(t)return e(t);try{const s=this._cwd,n=S(s,"package.json");if(C(n)){const i=JSON.parse(E(n,"utf-8")),o=i.scripts||{},r=Object.keys({...i.dependencies||{},...i.devDependencies||{}}).join(" ");if(/\b(react|vue|svelte|next|nuxt|vite|@angular\/core|solid-js|preact|astro)\b/i.test(r)){const a=[];if(o.typecheck?a.push("npm run typecheck"):o["type-check"]?a.push("npm run type-check"):C(S(s,"tsconfig.json"))&&a.push("npx tsc --noEmit"),o.lint&&a.push("npm run lint"),a.length)return e(a.join(" && "));if(o.build)return e("npm run build")}for(const a of["typecheck","type-check","build","lint"])if(o[a])return e(`npm run ${a}`)}if(C(S(s,"tsconfig.json")))return e("npx tsc --noEmit");if(C(S(s,"go.mod")))return e("go build ./...");if(C(S(s,"Cargo.toml")))return e("cargo check")}catch{}return e(null)}async _runVerification(e,t){try{const s=await pe("sh",["-c",e],this._cwd,{timeout:12e4,signal:t}),n=`${s.stdout}
21
+ ${s.stderr}`.trim();return{passed:s.code===0&&!s.killed,output:n.slice(-1500)}}catch(s){return{passed:!0,output:`(verify could not run: ${s.message})`}}}async _overseerCheck(e){if(e.stopReason==="error")return!1;const t=this.agent.state.model,s=this._sessionGoal,n=!!s&&!s.achieved,i=await this._getModelTier(t),o=process.env.KODA_OVERSEER!=="0"&&this._overseerRounds<2&&!!this._currentGoal&&!!i&&i!=="max",r=this._codeChangedThisTask?this._detectVerify():null;if(!r&&!o&&!n)return!1;this._emit({type:"overseer_start",model:t.id}),this._overseerAbortController=new AbortController;const l=this._overseerAbortController.signal;try{let a;if(r){const d=await this._runVerification(r,l);if(l.aborted)return this._trace("overseer","cancelled during verify"),this._emit({type:"overseer_end",outcome:"skipped",detail:"cancelled"}),!1;if(this._trace("verify",`${r} \u2192 ${d.passed?"pass":"FAIL"}`),a=d.passed,!d.passed){const u=this._overseerRounds>=1&&await this._escalate("build/tests still failing");return u||this._overseerRounds++,this.agent.followUp({role:"user",content:[{type:"text",text:`Not done \u2014 \`${r}\` failed. Fix the root cause and re-run it until it passes:
22
22
 
23
- ${h.output}`}],timestamp:Date.now()}),this._emit({type:"overseer_end",outcome:"nudging",detail:u?"escalating: verification failing":`verification failed: ${r}`}),!0}}if(!o&&!n)return this._emit({type:"overseer_end",outcome:"complete"}),!1;const c=await this._taskEndReview(o?this._currentGoal:void 0,this._overseerContext(),this.getActiveToolNames(),n?s:void 0,l,a);if(a.aborted)return this._trace("overseer","cancelled during review"),this._emit({type:"overseer_end",outcome:"skipped",detail:"cancelled"}),!1;if(!c)return this._emit({type:"overseer_end",outcome:"skipped",detail:this._overseerLastError||"unknown"}),!1;if(n&&s&&c.goal&&(c.goal.milestonesDone.forEach((h,u)=>{s.milestones[u]&&h&&(s.milestones[u].done=!0)}),s.achieved=c.goal.achieved||s.milestones.every(h=>h.done),this._emit({type:"goal_progress",text:s.text,milestones:s.milestones.map(h=>({...h})),achieved:s.achieved,drift:c.goal.drift||void 0})),o&&c.task&&!c.task.done){const h=this._overseerRounds>=1&&await this._escalate(`stuck \u2014 ${c.task.missing}`);h||this._overseerRounds++;const u=n&&c.goal?.drift?` You've also drifted from the session goal: ${c.goal.drift}.`:"";return this.agent.followUp({role:"user",content:[{type:"text",text:`Not done yet \u2014 keep going. ${c.task.missing}${u} Do exactly that, then verify it works before you stop. If you're unsure how, call ask_for_help to get a step-by-step plan from a senior model and follow it.`}],timestamp:Date.now()}),this._emit({type:"overseer_end",outcome:"nudging",detail:h?`escalating: ${c.task.missing}`:c.task.missing}),!0}return this._emit({type:"overseer_end",outcome:"complete"}),!1}finally{this._overseerAbortController=void 0}}async _taskEndReview(e,t,s,n,i,o){try{if(this._gatewayOpen())return this._overseerLastError="gateway circuit open (recent failures)",null;const r=this.agent.state.model,a=[process.env.KODA_OVERSEER_MODEL||"GLM-4.7","GLM-5","glm-5.1"],l=a.map(c=>this._modelRegistry.find(r.provider,c)).filter(c=>!!c&&c.id!==r.id);if(l.length===0)return this._overseerLastError=`no judge model registered (tried ${a.join(", ")})`,null;for(const c of l){if(o?.aborted)return this._overseerLastError="cancelled",null;const h=await this._reviewOnce(c,e,t,s,n,i,o);if(this._gatewayResult(!h.error||h.error.startsWith("unparseable")),h.review)return h.review;this._overseerLastError=`${c.id}: ${h.error}`}return null}catch(r){return this._overseerLastError=r?.message?.slice(0,80)||"error",null}}async _reviewOnce(e,t,s,n,i,o,r){try{if(r?.aborted)return{error:"cancelled"};if(!e.baseUrl)return{error:"no baseUrl"};const a=await this._modelRegistry.getApiKeyAndHeaders(e);if(!a?.apiKey)return{error:"no api key"};const l=["You are a meticulous reviewer for a coding agent. Ground every judgement in the WORK LOG (a record of tool calls and their results); be skeptical of any claim with no matching evidence."],c=[];if(t&&(l.push("TASK REVIEW \u2014 decide whether EVERY explicit deliverable of the REQUEST is present AND works. Judge only its explicit asks. If it says to run/test/verify, it is done only if the log shows that ran and succeeded. Completion requires EVIDENCE in the work log \u2014 files actually written, commands that actually ran and succeeded. The assistant's reasoning, plans, intentions, or confident claims are NOT evidence; if the log shows only thinking or failed/partial attempts with no successful results producing the deliverables, it is NOT done. "+(o===!0?"NOTE: the project's build/tests were just run by the harness and PASSED, so the code works \u2014 judge only whether the request's scope is fully addressed. ":"")+`The assistant has these TOOLS: ${n.join(", ")||"(none)"}; if it guessed or gave up without using a tool that could have found the answer, it is NOT done \u2014 name the tool to call. "taskMissing" must name ONLY what is left (imperative; exact file/function/command/tool), never restate done work.`),c.push('"taskDone": true|false, "taskMissing": "<only what is left; empty if done>"')),i){const w=i.milestones.map((d,b)=>`${b}: ${d.text}`).join("; ");l.push(`GOAL REVIEW \u2014 the session goal is: "${i.text}". Milestones by index: ${w}. From the WORK LOG, return "milestonesDone" as an array of booleans (one per index; true only if clearly accomplished in the log). Set "goalAchieved" true only if the whole goal is genuinely met. If recent work clearly wandered onto something unrelated to the goal, put a one-sentence note in "drift" (else "").`),c.push('"milestonesDone": [booleans], "goalAchieved": true|false, "drift": "<note or empty>"')}l.push(`Output ONLY a JSON object, no prose or fences: { ${c.join(", ")} }.`);const h=new AbortController,u=setTimeout(()=>h.abort(),45e3),m=f(()=>h.abort(),"onAbort");r?.addEventListener("abort",m,{once:!0});try{const w=await fetch(`${e.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${a.apiKey}`,"Content-Type":"application/json",...a.headers||{}},body:JSON.stringify({model:e.id,temperature:0,messages:[{role:"system",content:l.join(`
23
+ ${d.output}`}],timestamp:Date.now()}),this._emit({type:"overseer_end",outcome:"nudging",detail:u?"escalating: verification failing":`verification failed: ${r}`}),!0}}if(!o&&!n)return this._emit({type:"overseer_end",outcome:"complete"}),!1;const c=await this._taskEndReview(o?this._currentGoal:void 0,this._overseerContext(),this.getActiveToolNames(),n?s:void 0,a,l);if(l.aborted)return this._trace("overseer","cancelled during review"),this._emit({type:"overseer_end",outcome:"skipped",detail:"cancelled"}),!1;if(!c)return this._emit({type:"overseer_end",outcome:"skipped",detail:this._overseerLastError||"unknown"}),!1;if(n&&s&&c.goal&&(c.goal.milestonesDone.forEach((d,u)=>{s.milestones[u]&&d&&(s.milestones[u].done=!0)}),s.achieved=c.goal.achieved||s.milestones.every(d=>d.done),this._emit({type:"goal_progress",text:s.text,milestones:s.milestones.map(d=>({...d})),achieved:s.achieved,drift:c.goal.drift||void 0})),o&&c.task&&!c.task.done){const d=this._overseerRounds>=1&&await this._escalate(`stuck \u2014 ${c.task.missing}`);d||this._overseerRounds++;const u=n&&c.goal?.drift?` You've also drifted from the session goal: ${c.goal.drift}.`:"";return this.agent.followUp({role:"user",content:[{type:"text",text:`Not done yet \u2014 keep going. ${c.task.missing}${u} Do exactly that, then verify it works before you stop. If you're unsure how, call ask_for_help to get a step-by-step plan from a senior model and follow it.`}],timestamp:Date.now()}),this._emit({type:"overseer_end",outcome:"nudging",detail:d?`escalating: ${c.task.missing}`:c.task.missing}),!0}return this._emit({type:"overseer_end",outcome:"complete"}),!1}finally{this._overseerAbortController=void 0}}async _taskEndReview(e,t,s,n,i,o){try{if(this._gatewayOpen())return this._overseerLastError="gateway circuit open (recent failures)",null;const r=this.agent.state.model,l=[process.env.KODA_OVERSEER_MODEL||"GLM-4.7","GLM-5","glm-5.1"],a=l.map(c=>this._modelRegistry.find(r.provider,c)).filter(c=>!!c&&c.id!==r.id);if(a.length===0)return this._overseerLastError=`no judge model registered (tried ${l.join(", ")})`,null;for(const c of a){if(o?.aborted)return this._overseerLastError="cancelled",null;const d=await this._reviewOnce(c,e,t,s,n,i,o);if(this._gatewayResult(!d.error||d.error.startsWith("unparseable")),d.review)return d.review;this._overseerLastError=`${c.id}: ${d.error}`}return null}catch(r){return this._overseerLastError=r?.message?.slice(0,80)||"error",null}}async _reviewOnce(e,t,s,n,i,o,r){try{if(r?.aborted)return{error:"cancelled"};if(!e.baseUrl)return{error:"no baseUrl"};const l=await this._modelRegistry.getApiKeyAndHeaders(e);if(!l?.apiKey)return{error:"no api key"};const a=["You are a meticulous reviewer for a coding agent. Ground every judgement in the WORK LOG (a record of tool calls and their results); be skeptical of any claim with no matching evidence."],c=[];if(t&&(a.push("TASK REVIEW \u2014 decide whether EVERY explicit deliverable of the REQUEST is present AND works. Judge only its explicit asks. If it says to run/test/verify, it is done only if the log shows that ran and succeeded. Completion requires EVIDENCE in the work log \u2014 files actually written, commands that actually ran and succeeded. The assistant's reasoning, plans, intentions, or confident claims are NOT evidence; if the log shows only thinking or failed/partial attempts with no successful results producing the deliverables, it is NOT done. "+(o===!0?"NOTE: the project's build/tests were just run by the harness and PASSED, so the code works \u2014 judge only whether the request's scope is fully addressed. ":"")+`The assistant has these TOOLS: ${n.join(", ")||"(none)"}; if it guessed or gave up without using a tool that could have found the answer, it is NOT done \u2014 name the tool to call. "taskMissing" must name ONLY what is left (imperative; exact file/function/command/tool), never restate done work.`),c.push('"taskDone": true|false, "taskMissing": "<only what is left; empty if done>"')),i){const x=i.milestones.map((h,b)=>`${b}: ${h.text}`).join("; ");a.push(`GOAL REVIEW \u2014 the session goal is: "${i.text}". Milestones by index: ${x}. From the WORK LOG, return "milestonesDone" as an array of booleans (one per index; true only if clearly accomplished in the log). Set "goalAchieved" true only if the whole goal is genuinely met. If recent work clearly wandered onto something unrelated to the goal, put a one-sentence note in "drift" (else "").`),c.push('"milestonesDone": [booleans], "goalAchieved": true|false, "drift": "<note or empty>"')}a.push(`Output ONLY a JSON object, no prose or fences: { ${c.join(", ")} }.`);const d=new AbortController,u=setTimeout(()=>d.abort(),45e3),g=w(()=>d.abort(),"onAbort");r?.addEventListener("abort",g,{once:!0});try{const x=await fetch(`${e.baseUrl.replace(/\/$/,"")}/chat/completions`,{method:"POST",headers:{Authorization:`Bearer ${l.apiKey}`,"Content-Type":"application/json",...l.headers||{}},body:JSON.stringify({model:e.id,temperature:0,messages:[{role:"system",content:a.join(`
24
24
 
25
25
  `)},{role:"user",content:`${t?`REQUEST:
26
26
  ${t}
27
27
 
28
28
  `:""}WORK LOG:
29
- ${s}`}]}),signal:h.signal});if(!w.ok)return{error:`HTTP ${w.status}`};const d=await w.json();let b=(d?.choices?.[0]?.message?.content??d?.choices?.[0]?.message?.reasoning_content??"").trim();b=b.replace(/<think>[\s\S]*?<\/think>/gi,"").replace(/^```(?:json)?/i,"").replace(/```\s*$/,"").trim();const g=b.match(/\{[\s\S]*\}/);if(g&&(b=g[0]),!b)return{error:"empty response"};let p;try{p=JSON.parse(b)}catch{return{error:"unparseable JSON"}}const _={};if(t){const x=p?.taskDone===!0||p?.taskDone==="true",v=String(Array.isArray(p?.taskMissing)?p.taskMissing.join("; "):p?.taskMissing??"").trim();_.task=!x&&!v?{done:!0,missing:""}:{done:x,missing:v}}return i&&(_.goal={milestonesDone:Array.isArray(p?.milestonesDone)?p.milestonesDone.map(x=>x===!0||x==="true"):[],achieved:p?.goalAchieved===!0||p?.goalAchieved==="true",drift:String(p?.drift??"").trim()}),{review:_}}finally{clearTimeout(u),r?.removeEventListener("abort",m)}}catch(a){return{error:a?.name==="AbortError"?"timeout 45s":(a?.message||"error").slice(0,80)}}}_isMutatingBash(e){return/>>?\s*(?!\/dev\/null|&?\d)/.test(e)?!0:/\b(rm|rmdir|mv|cp|mkdir|touch|truncate|dd|chmod|chown|ln|tee|patch|sed\s+-i|install|npm\s+(i|install|add|remove|uninstall|ci)|pnpm\s+(install|add|remove)|yarn\s+(add|remove|install)|pip\d?\s+install|cargo\s+(add|install)|go\s+install|apt(-get)?\s+(install|remove)|brew\s+(install|uninstall)|git\s+(commit|add|push|checkout|reset|merge|rebase|clean|stash|apply|rm|mv))\b/.test(e)}_parseRetrySeconds(e){const t=/(?:retry|again)[^0-9]{0,16}?(\d+(?:\.\d+)?)\s*(ms|s\b|sec|second)/i.exec(e);if(t){const s=parseFloat(t[1]);return/ms/i.test(t[2])?Math.max(1,Math.ceil(s/1e3)):Math.ceil(s)}}async _zenitsuHandle429(e){if(e.stopReason!=="error"||!e.errorMessage||!/rate.?limit|429|too many requests|retry delay|quota/i.test(e.errorMessage))return!1;const t=this.agent.state.model,s=this._parseRetrySeconds(e.errorMessage)??30;this._zenitsuCooldown.set(`${t.provider}/${t.id}`,Date.now()+s*1e3);const n=await this._zenitsuPickModel();if(!n||S(n,t))return!1;const i=this.agent.state.messages;return i.length>0&&i[i.length-1].role==="assistant"&&(this.agent.state.messages=i.slice(0,-1)),this.agent.state.model=n,this._retryAttempt=0,this.sessionManager.appendModelChange(n.provider,n.id),this._emitModelSelect(n,t,"cycle"),this._trace("zenitsu",`${t.id} \u2192 ${n.id} (429)`),this._emit({type:"model_switch_notice",message:`\xBB ${t.id} rate-limited \u2014 Koda zipped over to ${n.id} (no waiting)`}),!0}_maybeReturnHome(){const e=this._stickyModel??this._homeModel;if(!e)return!1;const t=this.agent.state.model;return t.id===e.id&&t.provider===e.provider||!this._modelRegistry.hasConfiguredAuth(e)||(this._zenitsuCooldown.get(`${e.provider}/${e.id}`)??0)>Date.now()?!1:(this.agent.state.model=e,this._zenitsuPreferred=e,this._retryAttempt=0,this.sessionManager.appendModelChange(e.provider,e.id),this._emitModelSelect(e,t,"cycle"),this._trace("revert",`\u2192 ${e.id} (preferred available)`),this._emit({type:"model_switch_notice",message:`\u2190 Back on your pick \u2014 ${e.id}`}),!0)}abortRetry(){this._retryAbortController?.abort()}get isRetrying(){return this._retryAbortController!==void 0}get autoRetryEnabled(){return this.settingsManager.getRetryEnabled()}setAutoRetryEnabled(e){this.settingsManager.setRetryEnabled(e)}async executeBash(e,t,s){this._bashAbortController=new AbortController;const n=this.settingsManager.getShellCommandPrefix(),i=this.settingsManager.getShellPath(),o=n?`${n}
30
- ${e}`:e;try{const r=await le(o,this.sessionManager.getCwd(),s?.operations??be({shellPath:i}),{onChunk:t,signal:this._bashAbortController.signal});return this.recordBashResult(e,r,s),r}finally{this._bashAbortController=void 0}}recordBashResult(e,t,s){const n={role:"bashExecution",command:e,output:t.output,exitCode:t.exitCode,cancelled:t.cancelled,truncated:t.truncated,fullOutputPath:t.fullOutputPath,timestamp:Date.now(),excludeFromContext:s?.excludeFromContext};this.isStreaming?this._pendingBashMessages.push(n):(this.agent.state.messages.push(n),this.sessionManager.appendMessage(n))}abortBash(){this._bashAbortController?.abort()}get isBashRunning(){return this._bashAbortController!==void 0}get hasPendingBashMessages(){return this._pendingBashMessages.length>0}_flushPendingBashMessages(){if(this._pendingBashMessages.length!==0){for(const e of this._pendingBashMessages)this.agent.state.messages.push(e),this.sessionManager.appendMessage(e);this._pendingBashMessages=[]}}setSessionName(e){this.sessionManager.appendSessionInfo(e),this._emit({type:"session_info_changed",name:this.sessionManager.getSessionName()})}async navigateTree(e,t={}){const s=this.sessionManager.getLeafId();if(e===s)return{cancelled:!1};if(t.summarize&&!this.model)throw new Error("No model available for summarization");const n=this.sessionManager.getEntry(e);if(!n)throw new Error(`Entry ${e} not found`);const{entries:i,commonAncestorId:o}=ce(this.sessionManager,s,e);let r=t.customInstructions,a=t.replaceInstructions,l=t.label;const c={targetId:e,oldLeafId:s,commonAncestorId:o,entriesToSummarize:i,userWantsSummary:t.summarize??!1,customInstructions:r,replaceInstructions:a,label:l};this._branchSummaryAbortController=new AbortController;try{let h,u=!1;if(this._extensionRunner.hasHandlers("session_before_tree")){const _=await this._extensionRunner.emit({type:"session_before_tree",preparation:c,signal:this._branchSummaryAbortController.signal});if(_?.cancel)return{cancelled:!0};_?.summary&&t.summarize&&(h=_.summary,u=!0),_?.customInstructions!==void 0&&(r=_.customInstructions),_?.replaceInstructions!==void 0&&(a=_.replaceInstructions),_?.label!==void 0&&(l=_.label)}let m,w;if(t.summarize&&i.length>0&&!h){const _=this.model,{apiKey:x,headers:v}=await this._getRequiredRequestAuth(_),M=this.settingsManager.getBranchSummarySettings(),y=await he(i,{model:_,apiKey:x,headers:v,signal:this._branchSummaryAbortController.signal,customInstructions:r,replaceInstructions:a,reserveTokens:M.reserveTokens,streamFn:this.agent.streamFn});if(y.aborted)return{cancelled:!0,aborted:!0};if(y.error)throw new Error(y.error);m=y.summary,w={readFiles:y.readFiles||[],modifiedFiles:y.modifiedFiles||[]}}else h&&(m=h.summary,w=h.details);let d,b;n.type==="message"&&n.message.role==="user"?(d=n.parentId,b=this._extractUserMessageText(n.message.content)):n.type==="custom_message"?(d=n.parentId,b=typeof n.content=="string"?n.content:n.content.filter(_=>_.type==="text").map(_=>_.text).join("")):d=e;let g;if(m){const _=this.sessionManager.branchWithSummary(d,m,w,u);g=this.sessionManager.getEntry(_),l&&this.sessionManager.appendLabelChange(_,l)}else d===null?this.sessionManager.resetLeaf():this.sessionManager.branch(d);l&&!m&&this.sessionManager.appendLabelChange(e,l);const p=this.sessionManager.buildSessionContext();return this.agent.state.messages=p.messages,await this._extensionRunner.emit({type:"session_tree",newLeafId:this.sessionManager.getLeafId(),oldLeafId:s,summaryEntry:g,fromExtension:m?u:void 0}),{editorText:b,cancelled:!1,summaryEntry:g}}finally{this._branchSummaryAbortController=void 0}}getUserMessagesForForking(){const e=this.sessionManager.getEntries(),t=[];for(const s of e){if(s.type!=="message"||s.message.role!=="user")continue;const n=this._extractUserMessageText(s.message.content);n&&t.push({entryId:s.id,text:n})}return t}_extractUserMessageText(e){return typeof e=="string"?e:Array.isArray(e)?e.filter(t=>t.type==="text").map(t=>t.text).join(""):""}getSessionStats(){const e=this.state,t=e.messages.filter(h=>h.role==="user").length,s=e.messages.filter(h=>h.role==="assistant").length,n=e.messages.filter(h=>h.role==="toolResult").length;let i=0,o=0,r=0,a=0,l=0,c=0;for(const h of e.messages)if(h.role==="assistant"){const u=h;i+=u.content.filter(m=>m.type==="toolCall").length,o+=u.usage.input,r+=u.usage.output,a+=u.usage.cacheRead,l+=u.usage.cacheWrite,c+=u.usage.cost.total}return{sessionFile:this.sessionFile,sessionId:this.sessionId,userMessages:t,assistantMessages:s,toolCalls:i,toolResults:n,totalMessages:e.messages.length,tokens:{input:o,output:r,cacheRead:a,cacheWrite:l,total:o+r+a+l},cost:c,contextUsage:this.getContextUsage()}}getContextUsage(){const e=this.model;if(!e)return;const t=e.contextWindow??0;if(t<=0)return;const s=this.sessionManager.getBranch(),n=V(s);if(n){const r=s.lastIndexOf(n);let a=!1;for(let l=s.length-1;l>r;l--){const c=s[l];if(c.type==="message"&&c.message.role==="assistant"){const h=c.message;if(h.stopReason!=="aborted"&&h.stopReason!=="error"){z(h.usage)>0&&(a=!0);break}}}if(!a)return{tokens:null,contextWindow:t,percent:null}}const i=G(this.messages),o=i.tokens/t*100;return{tokens:i.tokens,contextWindow:t,percent:o}}async exportToHtml(e){const t=this.settingsManager.getTheme(),s=pe({getToolDefinition:f(n=>this.getToolDefinition(n),"getToolDefinition"),theme:ie,cwd:this.sessionManager.getCwd()});return await ge(this.sessionManager,this.state,{outputPath:e,themeName:t,toolRenderer:s})}exportToJsonl(e){const t=oe(e??`session-${new Date().toISOString().replace(/[:.]/g,"-")}.jsonl`,process.cwd()),s=U(t);k(s)||D(s,{recursive:!0});const n={type:"session",version:ye,id:this.sessionManager.getSessionId(),timestamp:new Date().toISOString(),cwd:this.sessionManager.getCwd()},i=this.sessionManager.getBranch(),o=[JSON.stringify(n)];let r=null;for(const a of i){const l={...a,parentId:r};o.push(JSON.stringify(l)),r=a.id}return L(t,`${o.join(`
29
+ ${s}`}]}),signal:d.signal});if(!x.ok)return{error:`HTTP ${x.status}`};const h=await x.json();let b=(h?.choices?.[0]?.message?.content??h?.choices?.[0]?.message?.reasoning_content??"").trim();b=b.replace(/<think>[\s\S]*?<\/think>/gi,"").replace(/^```(?:json)?/i,"").replace(/```\s*$/,"").trim();const m=b.match(/\{[\s\S]*\}/);if(m&&(b=m[0]),!b)return{error:"empty response"};let p;try{p=JSON.parse(b)}catch{return{error:"unparseable JSON"}}const y={};if(t){const v=p?.taskDone===!0||p?.taskDone==="true",f=String(Array.isArray(p?.taskMissing)?p.taskMissing.join("; "):p?.taskMissing??"").trim();y.task=!v&&!f?{done:!0,missing:""}:{done:v,missing:f}}return i&&(y.goal={milestonesDone:Array.isArray(p?.milestonesDone)?p.milestonesDone.map(v=>v===!0||v==="true"):[],achieved:p?.goalAchieved===!0||p?.goalAchieved==="true",drift:String(p?.drift??"").trim()}),{review:y}}finally{clearTimeout(u),r?.removeEventListener("abort",g)}}catch(l){return{error:l?.name==="AbortError"?"timeout 45s":(l?.message||"error").slice(0,80)}}}_isMutatingBash(e){return/>>?\s*(?!\/dev\/null|&?\d)/.test(e)?!0:/\b(rm|rmdir|mv|cp|mkdir|touch|truncate|dd|chmod|chown|ln|tee|patch|sed\s+-i|install|npm\s+(i|install|add|remove|uninstall|ci)|pnpm\s+(install|add|remove)|yarn\s+(add|remove|install)|pip\d?\s+install|cargo\s+(add|install)|go\s+install|apt(-get)?\s+(install|remove)|brew\s+(install|uninstall)|git\s+(commit|add|push|checkout|reset|merge|rebase|clean|stash|apply|rm|mv))\b/.test(e)}_parseRetrySeconds(e){const t=/(?:retry|again)[^0-9]{0,16}?(\d+(?:\.\d+)?)\s*(ms|s\b|sec|second)/i.exec(e);if(t){const s=parseFloat(t[1]);return/ms/i.test(t[2])?Math.max(1,Math.ceil(s/1e3)):Math.ceil(s)}}async _zenitsuHandle429(e){if(e.stopReason!=="error"||!e.errorMessage||!/rate.?limit|429|too many requests|retry delay|quota/i.test(e.errorMessage))return!1;const t=this.agent.state.model,s=this._parseRetrySeconds(e.errorMessage)??30;this._zenitsuCooldown.set(`${t.provider}/${t.id}`,Date.now()+s*1e3);const n=await this._zenitsuPickModel();if(!n||T(n,t))return!1;const i=this.agent.state.messages;return i.length>0&&i[i.length-1].role==="assistant"&&(this.agent.state.messages=i.slice(0,-1)),this.agent.state.model=n,this._retryAttempt=0,this.sessionManager.appendModelChange(n.provider,n.id),this._emitModelSelect(n,t,"cycle"),this._trace("zenitsu",`${t.id} \u2192 ${n.id} (429)`),this._emit({type:"model_switch_notice",message:`\xBB ${t.id} rate-limited \u2014 Koda zipped over to ${n.id} (no waiting)`}),!0}_maybeReturnHome(){const e=this._stickyModel??this._homeModel;if(!e)return!1;const t=this.agent.state.model;return t.id===e.id&&t.provider===e.provider||!this._modelRegistry.hasConfiguredAuth(e)||(this._zenitsuCooldown.get(`${e.provider}/${e.id}`)??0)>Date.now()?!1:(this.agent.state.model=e,this._zenitsuPreferred=e,this._retryAttempt=0,this.sessionManager.appendModelChange(e.provider,e.id),this._emitModelSelect(e,t,"cycle"),this._trace("revert",`\u2192 ${e.id} (preferred available)`),this._emit({type:"model_switch_notice",message:`\u2190 Back on your pick \u2014 ${e.id}`}),!0)}abortRetry(){this._retryAbortController?.abort()}get isRetrying(){return this._retryAbortController!==void 0}get autoRetryEnabled(){return this.settingsManager.getRetryEnabled()}setAutoRetryEnabled(e){this.settingsManager.setRetryEnabled(e)}async executeBash(e,t,s){this._bashAbortController=new AbortController;const n=this.settingsManager.getShellCommandPrefix(),i=this.settingsManager.getShellPath(),o=n?`${n}
30
+ ${e}`:e;try{const r=await he(o,this.sessionManager.getCwd(),s?.operations??ve({shellPath:i}),{onChunk:t,signal:this._bashAbortController.signal});return this.recordBashResult(e,r,s),r}finally{this._bashAbortController=void 0}}recordBashResult(e,t,s){const n={role:"bashExecution",command:e,output:t.output,exitCode:t.exitCode,cancelled:t.cancelled,truncated:t.truncated,fullOutputPath:t.fullOutputPath,timestamp:Date.now(),excludeFromContext:s?.excludeFromContext};this.isStreaming?this._pendingBashMessages.push(n):(this.agent.state.messages.push(n),this.sessionManager.appendMessage(n))}abortBash(){this._bashAbortController?.abort()}get isBashRunning(){return this._bashAbortController!==void 0}get hasPendingBashMessages(){return this._pendingBashMessages.length>0}_flushPendingBashMessages(){if(this._pendingBashMessages.length!==0){for(const e of this._pendingBashMessages)this.agent.state.messages.push(e),this.sessionManager.appendMessage(e);this._pendingBashMessages=[]}}setSessionName(e){this.sessionManager.appendSessionInfo(e),this._emit({type:"session_info_changed",name:this.sessionManager.getSessionName()})}async navigateTree(e,t={}){const s=this.sessionManager.getLeafId();if(e===s)return{cancelled:!1};if(t.summarize&&!this.model)throw new Error("No model available for summarization");const n=this.sessionManager.getEntry(e);if(!n)throw new Error(`Entry ${e} not found`);const{entries:i,commonAncestorId:o}=de(this.sessionManager,s,e);let r=t.customInstructions,l=t.replaceInstructions,a=t.label;const c={targetId:e,oldLeafId:s,commonAncestorId:o,entriesToSummarize:i,userWantsSummary:t.summarize??!1,customInstructions:r,replaceInstructions:l,label:a};this._branchSummaryAbortController=new AbortController;try{let d,u=!1;if(this._extensionRunner.hasHandlers("session_before_tree")){const y=await this._extensionRunner.emit({type:"session_before_tree",preparation:c,signal:this._branchSummaryAbortController.signal});if(y?.cancel)return{cancelled:!0};y?.summary&&t.summarize&&(d=y.summary,u=!0),y?.customInstructions!==void 0&&(r=y.customInstructions),y?.replaceInstructions!==void 0&&(l=y.replaceInstructions),y?.label!==void 0&&(a=y.label)}let g,x;if(t.summarize&&i.length>0&&!d){const y=this.model,{apiKey:v,headers:f}=await this._getRequiredRequestAuth(y),M=this.settingsManager.getBranchSummarySettings(),_=await ue(i,{model:y,apiKey:v,headers:f,signal:this._branchSummaryAbortController.signal,customInstructions:r,replaceInstructions:l,reserveTokens:M.reserveTokens,streamFn:this.agent.streamFn});if(_.aborted)return{cancelled:!0,aborted:!0};if(_.error)throw new Error(_.error);g=_.summary,x={readFiles:_.readFiles||[],modifiedFiles:_.modifiedFiles||[]}}else d&&(g=d.summary,x=d.details);let h,b;n.type==="message"&&n.message.role==="user"?(h=n.parentId,b=this._extractUserMessageText(n.message.content)):n.type==="custom_message"?(h=n.parentId,b=typeof n.content=="string"?n.content:n.content.filter(y=>y.type==="text").map(y=>y.text).join("")):h=e;let m;if(g){const y=this.sessionManager.branchWithSummary(h,g,x,u);m=this.sessionManager.getEntry(y),a&&this.sessionManager.appendLabelChange(y,a)}else h===null?this.sessionManager.resetLeaf():this.sessionManager.branch(h);a&&!g&&this.sessionManager.appendLabelChange(e,a);const p=this.sessionManager.buildSessionContext();return this.agent.state.messages=p.messages,await this._extensionRunner.emit({type:"session_tree",newLeafId:this.sessionManager.getLeafId(),oldLeafId:s,summaryEntry:m,fromExtension:g?u:void 0}),{editorText:b,cancelled:!1,summaryEntry:m}}finally{this._branchSummaryAbortController=void 0}}getUserMessagesForForking(){const e=this.sessionManager.getEntries(),t=[];for(const s of e){if(s.type!=="message"||s.message.role!=="user")continue;const n=this._extractUserMessageText(s.message.content);n&&t.push({entryId:s.id,text:n})}return t}_extractUserMessageText(e){return typeof e=="string"?e:Array.isArray(e)?e.filter(t=>t.type==="text").map(t=>t.text).join(""):""}getSessionStats(){const e=this.state,t=e.messages.filter(d=>d.role==="user").length,s=e.messages.filter(d=>d.role==="assistant").length,n=e.messages.filter(d=>d.role==="toolResult").length;let i=0,o=0,r=0,l=0,a=0,c=0;for(const d of e.messages)if(d.role==="assistant"){const u=d;i+=u.content.filter(g=>g.type==="toolCall").length,o+=u.usage.input,r+=u.usage.output,l+=u.usage.cacheRead,a+=u.usage.cacheWrite,c+=u.usage.cost.total}return{sessionFile:this.sessionFile,sessionId:this.sessionId,userMessages:t,assistantMessages:s,toolCalls:i,toolResults:n,totalMessages:e.messages.length,tokens:{input:o,output:r,cacheRead:l,cacheWrite:a,total:o+r+l+a},cost:c,contextUsage:this.getContextUsage()}}getContextUsage(){const e=this.model;if(!e)return;const t=e.contextWindow??0;if(t<=0)return;const s=this.sessionManager.getBranch(),n=Q(s);if(n){const r=s.lastIndexOf(n);let l=!1;for(let a=s.length-1;a>r;a--){const c=s[a];if(c.type==="message"&&c.message.role==="assistant"){const d=c.message;if(d.stopReason!=="aborted"&&d.stopReason!=="error"){G(d.usage)>0&&(l=!0);break}}}if(!l)return{tokens:null,contextWindow:t,percent:null}}const i=q(this.messages),o=i.tokens/t*100;return{tokens:i.tokens,contextWindow:t,percent:o}}async exportToHtml(e){const t=this.settingsManager.getTheme(),s=_e({getToolDefinition:w(n=>this.getToolDefinition(n),"getToolDefinition"),theme:oe,cwd:this.sessionManager.getCwd()});return await fe(this.sessionManager,this.state,{outputPath:e,themeName:t,toolRenderer:s})}exportToJsonl(e){const t=le(e??`session-${new Date().toISOString().replace(/[:.]/g,"-")}.jsonl`,process.cwd()),s=B(t);C(s)||K(s,{recursive:!0});const n={type:"session",version:be,id:this.sessionManager.getSessionId(),timestamp:new Date().toISOString(),cwd:this.sessionManager.getCwd()},i=this.sessionManager.getBranch(),o=[JSON.stringify(n)];let r=null;for(const l of i){const a={...l,parentId:r};o.push(JSON.stringify(a)),r=l.id}return L(t,`${o.join(`
31
31
  `)}
32
- `),t}getLastAssistantText(){const e=this.messages.slice().reverse().find(s=>{if(s.role!=="assistant")return!1;const n=s;return!(n.stopReason==="aborted"&&n.content.length===0)});if(!e)return;let t="";for(const s of e.content)s.type==="text"&&(t+=s.text);return t.trim()||void 0}createReplacedSessionContext(){const e=Object.defineProperties({},Object.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext()));return e.sendMessage=(t,s)=>this.sendCustomMessage(t,s),e.sendUserMessage=(t,s)=>this.sendUserMessage(t,s),e}hasExtensionHandlers(e){return this._extensionRunner.hasHandlers(e)}get extensionRunner(){return this._extensionRunner}}export{Ye as AgentSession,Qe as parseSkillBlock};
32
+ `),t}getLastAssistantText(){const e=this.messages.slice().reverse().find(s=>{if(s.role!=="assistant")return!1;const n=s;return!(n.stopReason==="aborted"&&n.content.length===0)});if(!e)return;let t="";for(const s of e.content)s.type==="text"&&(t+=s.text);return t.trim()||void 0}createReplacedSessionContext(){const e=Object.defineProperties({},Object.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext()));return e.sendMessage=(t,s)=>this.sendCustomMessage(t,s),e.sendUserMessage=(t,s)=>this.sendUserMessage(t,s),e}hasExtensionHandlers(e){return this._extensionRunner.hasHandlers(e)}get extensionRunner(){return this._extensionRunner}}export{Xe as AgentSession,Ze as parseSkillBlock};
@@ -88,6 +88,7 @@ export declare class InteractiveMode {
88
88
  private overseerLoader;
89
89
  private recapLoader;
90
90
  private updateCheckTimer;
91
+ private modelRefreshTimer;
91
92
  private lastNotifiedUpdateVersion;
92
93
  private retryCountdown;
93
94
  private retryEscapeHandler?;
@@ -338,11 +339,7 @@ export declare class InteractiveMode {
338
339
  showWarning(warningMessage: string): void;
339
340
  private showResumeHintForCwd;
340
341
  private runImportFromSettings;
341
- /**
342
- * Check for a newer release; on a new one, notify and (unless disabled) auto-update.
343
- * Runs at startup and on a periodic timer, so long-running sessions still update.
344
- * Notifies at most once per version so the timer never re-pops the same box.
345
- */
342
+ private _refreshModelsWhenIdle;
346
343
  private _checkForUpdate;
347
344
  showNewVersionNotification(release: LatestPiRelease, autoUpdating?: boolean): void;
348
345
  showPackageUpdateNotification(packages: string[]): void;