open-multi-agent-kit 0.96.2 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +4 -2
- package/dist/core/agent-session.d.ts +9 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +84 -7
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/attachment-store.d.ts +39 -0
- package/dist/core/attachment-store.d.ts.map +1 -0
- package/dist/core/attachment-store.js +81 -0
- package/dist/core/attachment-store.js.map +1 -0
- package/dist/core/command-safety.d.ts +8 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +140 -14
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +35 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/context-budget-governor-v2.d.ts +1 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +1 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +2 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +3 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +2 -1
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-scoring.d.ts +17 -0
- package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
- package/dist/core/context-budget-v2-scoring.js +25 -8
- package/dist/core/context-budget-v2-scoring.js.map +1 -1
- package/dist/core/context-budget-v2-selection.d.ts +2 -2
- package/dist/core/context-budget-v2-selection.d.ts.map +1 -1
- package/dist/core/context-budget-v2-selection.js +18 -1
- package/dist/core/context-budget-v2-selection.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +17 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js +15 -0
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts +3 -1
- package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
- package/dist/core/extensions/builtin/command-safety-gate.js +7 -0
- package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
- package/dist/core/extensions/types.d.ts +12 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts +8 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +71 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-doctor-source-scan.d.ts +9 -0
- package/dist/core/package-doctor-source-scan.d.ts.map +1 -1
- package/dist/core/package-doctor-source-scan.js +77 -4
- package/dist/core/package-doctor-source-scan.js.map +1 -1
- package/dist/core/prompt-attachment.d.ts +51 -0
- package/dist/core/prompt-attachment.d.ts.map +1 -0
- package/dist/core/prompt-attachment.js +186 -0
- package/dist/core/prompt-attachment.js.map +1 -0
- package/dist/core/provider-resilience.d.ts +28 -0
- package/dist/core/provider-resilience.d.ts.map +1 -1
- package/dist/core/provider-resilience.js +50 -0
- package/dist/core/provider-resilience.js.map +1 -1
- package/dist/core/provider-retry.d.ts +1 -1
- package/dist/core/provider-retry.d.ts.map +1 -1
- package/dist/core/provider-retry.js +1 -1
- package/dist/core/provider-retry.js.map +1 -1
- package/dist/core/provider-usage.d.ts.map +1 -1
- package/dist/core/provider-usage.js +68 -2
- package/dist/core/provider-usage.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +84 -0
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -0
- package/dist/core/reasoning-router-promotion.js +92 -0
- package/dist/core/reasoning-router-promotion.js.map +1 -0
- package/dist/core/session-bash-service.d.ts.map +1 -1
- package/dist/core/session-bash-service.js +4 -3
- package/dist/core/session-bash-service.js.map +1 -1
- package/dist/core/session-termination.d.ts +1 -1
- package/dist/core/session-termination.d.ts.map +1 -1
- package/dist/core/session-termination.js +11 -2
- package/dist/core/session-termination.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +3 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +13 -48
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -34
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/attachment-strip.d.ts +21 -0
- package/dist/modes/interactive/components/attachment-strip.d.ts.map +1 -0
- package/dist/modes/interactive/components/attachment-strip.js +136 -0
- package/dist/modes/interactive/components/attachment-strip.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts +6 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +26 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
- package/dist/modes/interactive/components/status-sidebar.js +12 -1
- package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +23 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +174 -53
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/extensions.md +12 -3
- package/docs/index.md +4 -1
- package/docs/metrics.md +74 -2
- package/docs/provider-resilience.md +11 -0
- package/docs/usage.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +7 -7
- package/dist/core/tools/omp-pure-seams.d.ts +0 -41
- package/dist/core/tools/omp-pure-seams.d.ts.map +0 -1
- package/dist/core/tools/omp-pure-seams.js +0 -142
- package/dist/core/tools/omp-pure-seams.js.map +0 -1
- package/dist/core/tools/omp-seam-runtime.d.ts +0 -100
- package/dist/core/tools/omp-seam-runtime.d.ts.map +0 -1
- package/dist/core/tools/omp-seam-runtime.js +0 -40
- package/dist/core/tools/omp-seam-runtime.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../src/core/agent-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * AgentSession - Core abstraction for agent lifecycle and session management.\n *\n * This class is shared between all run modes (interactive, print, rpc).\n * It encapsulates:\n * - Agent state access\n * - Event subscription with automatic session persistence\n * - Model and thinking level management\n * - Compaction (manual and auto)\n * - Bash execution\n * - Session switching and branching\n *\n * Modes use this class and add their own I/O layer on top.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join as joinPath } from \"node:path\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from \"omk-agent-core\";\nimport { getVisionRouteModel } from \"omk-agent-core\";\nimport type { Api, AssistantMessage, ImageContent, Message, Model, TextContent } from \"omk-ai\";\nimport {\n\tclampThinkingLevel,\n\tcleanupSessionResources,\n\tderiveContextPromptCacheKey,\n\tgetSupportedThinkingLevels,\n\tisContextOverflow,\n\tmodelsAreEqual,\n\ttype RetryCallbacks,\n\tresetApiProviders,\n\tstreamSimple,\n} from \"omk-ai\";\nimport { APP_NAME, VERSION } from \"../config.ts\";\nimport type { ReplayLedgerManager } from \"../guardrails/evidence-system.ts\";\nimport type { VerifiedEvidenceExecutor } from \"../guardrails/verified-executor.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport type { ReplayEventType } from \"../types/evidence.ts\";\nimport { stripFrontmatter } from \"../utils/frontmatter.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { getShellConfig } from \"../utils/shell.ts\";\nimport { sleep } from \"../utils/sleep.ts\";\nimport { type AdaptorchBridge, type AdaptorchConsultPayload, createAdaptorchBridge } from \"./adaptorch-bridge.ts\";\nimport {\n\tapplyCategoryTimeoutDefaults,\n\tresolveAgentToolSettings,\n\tresolveToolTimeoutCategory,\n} from \"./agent-tool-settings.ts\";\nimport { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport { parseBangInvocation } from \"./bang-skill-invocation.ts\";\nimport type { BashResult } from \"./bash-executor.ts\";\nimport { type CompactionSettings, getCompactionHeadroomThreshold } from \"./compaction/compaction.ts\";\nimport {\n\ttype CompactionHysteresisState,\n\tcreateCompactionHysteresisConfig,\n\tcreateCompactionHysteresisState,\n\tstepCompactionHysteresis,\n} from \"./compaction/hysteresis.ts\";\nimport {\n\ttype CompactionResult,\n\tcalculateContextTokens,\n\tcollectEntriesForBranchSummary,\n\tcompact,\n\testimateContextTokens,\n\testimateProjectedContextTokens,\n\tgenerateBranchSummary,\n\tprepareCompaction,\n\tresolveCompactionModel,\n} from \"./compaction/index.ts\";\nimport { compactionEmitWillRetry } from \"./compaction/resume-policy.ts\";\nimport { isSessionModelOverflow, shouldSkipCompactionCheck } from \"./compaction-gate.ts\";\nimport {\n\testimateToolResultReserve,\n\ttype ToolResultClass,\n\ttype ToolResultReserveRequest,\n} from \"./context-budget-reserved-tokens.ts\";\nimport {\n\tcreateDiskContextBudgetCacheProviderV2,\n\tDiskContextBudgetCacheProviderV2,\n} from \"./context-budget-v2-cache-disk.ts\";\nimport {\n\tapplyContextCacheInvalidation,\n\ttype ContextCacheInvalidationEvent,\n\ttype ContextCacheInvalidationSnapshot,\n\tcreateContextCacheInvalidationSnapshot,\n} from \"./context-budget-v2-cache-invalidation.ts\";\nimport { createMemoryContextBudgetCacheProviderV2 } from \"./context-budget-v2-cache-provider.ts\";\nimport type { ContextBudgetCacheProviderV2 } from \"./context-budget-v2-types.ts\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport {\n\ttype ContextUsage,\n\ttype ExtensionCommandContextActions,\n\ttype ExtensionErrorListener,\n\ttype ExtensionMode,\n\tExtensionRunner,\n\ttype ExtensionUIContext,\n\ttype InputSource,\n\ttype MessageEndEvent,\n\ttype MessageStartEvent,\n\ttype MessageUpdateEvent,\n\ttype ReplacedSessionContext,\n\ttype SessionBeforeCompactResult,\n\ttype SessionBeforeTreeResult,\n\ttype SessionStartEvent,\n\ttype ShutdownHandler,\n\ttype ToolDefinition,\n\ttype ToolExecutionEndEvent,\n\ttype ToolExecutionStartEvent,\n\ttype ToolExecutionUpdateEvent,\n\ttype ToolInfo,\n\ttype TreePreparation,\n\ttype TurnEndEvent,\n\ttype TurnStartEvent,\n\twrapRegisteredTools,\n} from \"./extensions/index.ts\";\nimport { emitSessionShutdownEvent } from \"./extensions/runner.ts\";\nimport { assertTextChatModelForCompletion } from \"./grok-harness.ts\";\nimport { grokPlaybookAppendForProvider } from \"./grok-playbook.ts\";\nimport { captureHostResourceSnapshot, type HostResourceSnapshot } from \"./host-resource-snapshot.ts\";\nimport { decideLoadoutAccess, type LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { loadMcpServerConfigs } from \"./mcp/config.ts\";\nimport { McpManager, type McpServerConfig, type McpServerStatus } from \"./mcp/manager.ts\";\nimport { buildRuntimeProvenance } from \"./runtime-provenance.ts\";\nimport { type ToolCallMetric, TurnMetricsSink } from \"./turn-metrics.ts\";\n\n/** Values that disable a session-level feature through its environment variable. */\nconst DISABLED_ENV_VALUES = new Set([\"0\", \"false\", \"off\", \"disable\", \"disabled\"]);\nconst MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;\nconst OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;\nconst MAX_RESOURCE_OBSERVATION_JOURNALS = 32;\n\nfunction isDisabledEnvValue(value: string | undefined): boolean {\n\treturn value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());\n}\n\n/** In-flight metric accumulation for the current turn. */\ninterface TurnMetricsState {\n\treadonly startedAtEpochMs: number;\n\treadonly toolCalls: ToolCallMetric[];\n\treadonly toolStarts: Map<string, number>;\n\t/** `provider/model` the turn was requested with, captured at turn start. */\n\treadonly requestedModel: string | null;\n\t/** `\"auto\"` in auto mode, else the thinking level captured at turn start. */\n\treadonly requestedThinking: string;\n}\n\ntype RouterAutoDecision = Pick<\n\tRouterFeedbackRecord,\n\t\"routerVersion\" | \"laneType\" | \"predictedClass\" | \"resolvedLevel\" | \"lenBucket\" | \"hadFence\" | \"hadDiff\"\n>;\n\n/** Extract the first text block of a tool result, for failure classification only. */\nfunction firstTextContent(result: unknown): string | undefined {\n\tconst content = (result as { content?: unknown } | undefined)?.content;\n\tif (!Array.isArray(content)) return undefined;\n\tfor (const block of content) {\n\t\tif (block && typeof block === \"object\" && (block as { type?: unknown }).type === \"text\") {\n\t\t\tconst text = (block as { text?: unknown }).text;\n\t\t\tif (typeof text === \"string\") return text;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nimport { redactCredentialShapedContent } from \"./compaction/transaction.ts\";\nimport type { CustomMessage } from \"./messages.ts\";\nimport { selectContextFilesForModel } from \"./model-prompt-policy.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { findExactModelReferenceMatch } from \"./model-resolver.ts\";\nimport { computePromptTokenBudget } from \"./prompt-budget.ts\";\nimport { classifyPromptCacheTransition } from \"./prompt-cache.ts\";\nimport {\n\tcreatePromptSettlementState,\n\ttype PromptSettledEvent,\n\ttype PromptSettlementOutcome,\n\treducePromptSettlement,\n\tsettlePromptIfReady,\n} from \"./prompt-settlement.ts\";\nimport { expandPromptTemplate, type PromptTemplate } from \"./prompt-templates.ts\";\nimport {\n\tisQuotaExhaustionMessage,\n\tisStickySafetyModel,\n\tpickFailoverCandidate,\n\tresolveProviderResilience,\n\tshouldEjectStickySafetyModel,\n\tshouldHonorSafetyFailover,\n\tstickySafetyBlockMessage,\n} from \"./provider-resilience.ts\";\nimport {\n\tcomputeRetryDelayMs,\n\tfailoverModelKey,\n\tisFailoverTriggerError,\n\tisRetryableAssistantError,\n\tnextRetryAttempt,\n\tretryBudgetForAssistantError,\n} from \"./provider-retry.ts\";\nimport { getBiasStepsForCell, parseRouterBiasSnapshot, type RouterBiasSnapshot } from \"./reasoning-router-bias.ts\";\nimport {\n\tclassifyTaskV4,\n\tderiveRouterFeedbackFeaturesV4,\n\ttype RouterFeedbackFeaturesV4,\n\tresolveThinkingLevelV4WithUncertainty,\n\tTASK_CLASS_THINKING_LEVELS_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4.ts\";\nimport { redactSensitiveText, redactSensitiveTextForced } from \"./redaction.ts\";\nimport { getRepositoryRouterLearningPaths, type RepositoryRouterLearningPaths } from \"./repository-learning-scope.ts\";\nimport { decideResourceAdmission, type ResourceAdmissionDecision } from \"./resource-admission.ts\";\nimport { resolveResourceGovernorSettings } from \"./resource-governor-settings.ts\";\nimport type { ResourceExtensionPaths, ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tadmissionObservationFacts,\n\tclassificationObservationFacts,\n\tResourceObservationJournal,\n\tsettledObservationFacts,\n\tsnapshotObservationFacts,\n\tsoundObservationFacts,\n} from \"./resource-observation-journal.ts\";\nimport { decideResourceSafetyGate, RESOURCE_PRESSURE_REQUIRED_ACTION } from \"./resource-safety-gate.ts\";\nimport {\n\tappendRouterFeedbackRecord,\n\tROUTER_FEEDBACK_LEVELS,\n\ttype RouterFeedbackRecord,\n} from \"./router-feedback-collector.ts\";\nimport type { RunJournalAuditDetails, RunJournalAuditEvent, RunJournalRecord } from \"./run-journal.ts\";\nimport { type RunJournalQuarantineReport, RunJournalStore } from \"./run-journal-store.ts\";\nimport { type RunResourceLease, RunResourceLeaseController } from \"./run-resource-lease.ts\";\nimport { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport { type BashResourcePermitGrant, SessionBashService } from \"./session-bash-service.ts\";\nimport { SessionCompactionService } from \"./session-compaction-service.ts\";\nimport type { BranchSummaryEntry, SessionManager } from \"./session-manager.ts\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.ts\";\nimport { acquireSessionOwnerLeaseSync, type SessionOwnerLease } from \"./session-owner-lease.ts\";\nimport { assembleSessionSystemPrompt } from \"./session-system-prompt.ts\";\nimport {\n\tclassifySessionTermination,\n\ttype SessionProcessSignal,\n\ttype SessionTermination,\n\ttype SessionTerminationCause,\n} from \"./session-termination.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport type { SlashCommandInfo } from \"./slash-commands.ts\";\nimport { createSyntheticSourceInfo, type SourceInfo } from \"./source-info.ts\";\nimport { type BuildSystemPromptOptions, buildSystemPromptPlan } from \"./system-prompt.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\nimport { createAllToolDefinitions } from \"./tools/index.ts\";\nimport { createToolDefinitionFromAgentTool } from \"./tools/tool-definition-wrapper.ts\";\nimport { createVerifiedBashOperations } from \"./verified-bash-adapter.ts\";\nimport { resolveSessionWorkspaceScope } from \"./verified-bash-runtime.ts\";\nimport { classifyWorkloadCommand } from \"./workload-classifier.ts\";\nimport { WorkloadPermitError, WorkloadPermitPool, type WorkloadPermitPoolSnapshot } from \"./workload-permit-pool.ts\";\n\n// ============================================================================\n// Skill Block Parsing\n// ============================================================================\n\n/** Parsed skill block from a user message */\nexport interface ParsedSkillBlock {\n\tname: string;\n\tlocation: string;\n\tcontent: string;\n\tuserMessage: string | undefined;\n}\n\n/**\n * Parse a skill block from message text.\n * Returns null if the text doesn't contain a skill block.\n */\nexport function parseSkillBlock(text: string): ParsedSkillBlock | null {\n\tconst match = text.match(/^<skill name=\"([^\"]+)\" location=\"([^\"]+)\">\\n([\\s\\S]*?)\\n<\\/skill>(?:\\n\\n([\\s\\S]+))?$/);\n\tif (!match) return null;\n\treturn {\n\t\tname: match[1],\n\t\tlocation: match[2],\n\t\tcontent: match[3],\n\t\tuserMessage: match[4]?.trim() || undefined,\n\t};\n}\n\n/**\n * Thinking mode: \"manual\" keeps the user-selected level; \"auto\" resolves the\n * level per turn via the reasoning router. Manual `/think <level>` always wins\n * because the router only runs in auto mode.\n */\nexport type ThinkingMode = \"manual\" | \"auto\";\n\n/** Session-specific events that extend the core AgentEvent */\nexport type AgentSessionEvent =\n\t| Exclude<AgentEvent, { type: \"agent_end\" }>\n\t| {\n\t\t\ttype: \"agent_end\";\n\t\t\tmessages: AgentMessage[];\n\t\t\twillRetry: boolean;\n\t }\n\t| PromptSettledEvent\n\t| {\n\t\t\ttype: \"queue_update\";\n\t\t\tsteering: readonly string[];\n\t\t\tfollowUp: readonly string[];\n\t }\n\t| { type: \"compaction_start\"; reason: \"manual\" | \"threshold\" | \"overflow\" }\n\t| { type: \"session_info_changed\"; name: string | undefined }\n\t| { type: \"thinking_level_changed\"; level: ThinkingLevel }\n\t| {\n\t\t\ttype: \"compaction_end\";\n\t\t\treason: \"manual\" | \"threshold\" | \"overflow\";\n\t\t\tresult: CompactionResult | undefined;\n\t\t\taborted: boolean;\n\t\t\twillRetry: boolean;\n\t\t\terrorMessage?: string;\n\t }\n\t| { type: \"auto_retry_start\"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }\n\t| { type: \"auto_retry_end\"; success: boolean; attempt: number; finalError?: string }\n\t| { type: \"bash_execution_update\"; id?: string; delta: string }\n\t| {\n\t\t\ttype: \"summarization_retry_scheduled\";\n\t\t\tattempt: number;\n\t\t\tmaxAttempts: number;\n\t\t\tdelayMs: number;\n\t\t\terrorMessage: string;\n\t }\n\t| { type: \"summarization_retry_attempt_start\"; source: \"branchSummary\" }\n\t| {\n\t\t\ttype: \"summarization_retry_attempt_start\";\n\t\t\tsource: \"compaction\";\n\t\t\treason: \"manual\" | \"threshold\" | \"overflow\";\n\t }\n\t| { type: \"summarization_retry_finished\" }\n\t| { type: \"session_termination\"; termination: SessionTermination }\n\t| {\n\t\t\t/**\n\t\t\t * A late-settling potentially-writing tool may have mutated the\n\t\t\t * workspace after its terminal result was committed. Evidence\n\t\t\t * freshness consumers must treat affected scopes as stale\n\t\t\t * (empty `paths` means the whole workspace root).\n\t\t\t */\n\t\t\ttype: \"workspace_mutation\";\n\t\t\tsource: \"tool_late_settlement\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\tpayload: { root: string; paths: readonly string[] };\n\t };\n\n/** Listener function for agent session events */\nexport type AgentSessionEventListener = (event: AgentSessionEvent) => void;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AgentSessionConfig {\n\tagent: Agent;\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tcwd: string;\n\t/** Models to cycle through with Ctrl+P (from --models flag) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\t/** Resource loader for skills, prompts, themes, context files, system prompt */\n\tresourceLoader: ResourceLoader;\n\t/** SDK custom tools registered outside extensions */\n\tcustomTools?: ToolDefinition[];\n\t/** Model registry for API key resolution and model discovery */\n\tmodelRegistry: ModelRegistry;\n\t/** Initial active built-in tool names. Default: [read, bash, edit, write] */\n\tinitialActiveToolNames?: string[];\n\t/** Optional allowlist of tool names. When provided, only these tool names are exposed. */\n\tallowedToolNames?: string[];\n\t/** Optional denylist of tool names. When provided, these tool names are not exposed. */\n\texcludedToolNames?: string[];\n\t/**\n\t * Override base tools (useful for custom runtimes).\n\t *\n\t * These are synthesized into minimal ToolDefinitions internally so AgentSession can keep\n\t * a definition-first registry even when callers provide plain AgentTool instances.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\t/** Trusted sandbox preflight used for built-in local bash execution. Never sourced from RPC command payloads. */\n\tbashSandboxPreflight?: BashSandboxPreflight;\n\t/** Optional immutable loadout policy used to lock active tools and scope built-in tool access. */\n\tloadoutAccessPolicy?: LoadoutAccessPolicy;\n\t/** Mutable ref used by Agent to access the current ExtensionRunner */\n\textensionRunnerRef?: { current?: ExtensionRunner };\n\t/** Session start event metadata emitted when extensions bind to this runtime. */\n\tsessionStartEvent?: SessionStartEvent;\n\t/** Transcript repair applied by the SDK while opening/resuming this session (ALG001-A). */\n\ttranscriptRepair?: SessionTranscriptRepair;\n\t/** Optional shared replay ledger used by evidence receipts and runtime mutation audits. */\n\treplayLedger?: ReplayLedgerManager;\n\t/** Goal binding for replay events. Defaults to the ledger's own goal id. */\n\treplayGoalId?: string;\n\t/** Optional lane binding for replay events. */\n\treplayLaneId?: string;\n\t/** True when the user pinned `--model` / `--provider` for this process. */\n\tmodelPinned?: boolean;\n}\n\n/** Summary of a missing-only transcript auto-repair applied on session open/resume. */\nexport interface SessionTranscriptRepair {\n\treadonly insertedToolCallIds: readonly string[];\n\treadonly reason: \"resume\";\n}\n\nexport interface ExtensionBindings {\n\tuiContext?: ExtensionUIContext;\n\tmode?: ExtensionMode;\n\tcommandContextActions?: ExtensionCommandContextActions;\n\tabortHandler?: () => void;\n\tshutdownHandler?: ShutdownHandler;\n\tonError?: ExtensionErrorListener;\n}\n\ninterface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Options for AgentSession.prompt() */\nexport interface PromptOptions {\n\t/** Whether to expand file-based prompt templates (default: true) */\n\texpandPromptTemplates?: boolean;\n\t/** Image attachments */\n\timages?: ImageContent[];\n\t/** When streaming, how to queue the message: \"steer\" (interrupt) or \"followUp\" (wait). Required if streaming. */\n\tstreamingBehavior?: \"steer\" | \"followUp\";\n\t/** Source of input for extension input event handlers. Defaults to \"interactive\". */\n\tsource?: InputSource;\n\tactiveSkillNames?: readonly string[];\n\tactiveSkillSource?: string;\n\t/** Internal hook used by RPC mode to observe prompt preflight acceptance or rejection. */\n\tpreflightResult?: (success: boolean) => void;\n}\n\nfunction mergePromptActiveSkillNames(first: readonly string[], second: readonly string[]): string[] {\n\tconst names: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const name of [...first, ...second]) {\n\t\tif (!seen.has(name)) {\n\t\t\tseen.add(name);\n\t\t\tnames.push(name);\n\t\t}\n\t}\n\treturn names;\n}\n\n/** Result from cycleModel() */\nexport interface ModelCycleResult {\n\tmodel: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\t/** Whether cycling through scoped models (--models flag) or all available */\n\tisScoped: boolean;\n}\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tpromptCache: {\n\t\tproviderEligibleInputTokens: number;\n\t\tproviderHitRate: number;\n\t\tkeyChanges: number;\n\t\tboundaryBypasses: number;\n\t\tstablePrefixCharacters: number;\n\t\tlastBreakReason?: string;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\ninterface ToolDefinitionEntry {\n\tdefinition: ToolDefinition;\n\tsourceInfo: SourceInfo;\n}\n\ntype BegunCompaction = import(\"./session-compaction-service.ts\").BegunCompaction;\ntype CommittedCompaction = import(\"./session-compaction-service.ts\").CommittedCompaction;\n\nconst PENDING_TOOL_RESULT_TOKENS = {\n\ttext: 1024,\n\timage: 4096,\n\t\"large-output\": 16_384,\n} as const satisfies Record<ToolResultClass, number>;\n\nconst TEXT_RESULT_TOOLS = new Set([\"edit\", \"find\", \"grep\", \"ls\", \"read\", \"write\"]);\nconst IMAGE_PATH_PATTERN = /\\.(?:avif|gif|jpe?g|png|webp)$/iu;\n\nfunction pendingToolResultClass(name: string, args: unknown): ToolResultClass {\n\tif (\n\t\tname === \"read\" &&\n\t\ttypeof args === \"object\" &&\n\t\targs !== null &&\n\t\ttypeof Reflect.get(args, \"path\") === \"string\" &&\n\t\tIMAGE_PATH_PATTERN.test(Reflect.get(args, \"path\"))\n\t) {\n\t\treturn \"image\";\n\t}\n\treturn TEXT_RESULT_TOOLS.has(name) ? \"text\" : \"large-output\";\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Standard thinking levels */\nconst THINKING_LEVELS: ThinkingLevel[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\"];\n\nfunction normalizeToolNames(toolNames: readonly string[]): string[] {\n\treturn [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== \"\"))].sort((left, right) => {\n\t\tif (left < right) return -1;\n\t\tif (left > right) return 1;\n\t\treturn 0;\n\t});\n}\n\nfunction toolNameSetsEqual(left: readonly string[], right: readonly string[]): boolean {\n\tconst normalizedLeft = normalizeToolNames(left);\n\tconst normalizedRight = normalizeToolNames(right);\n\treturn (\n\t\tnormalizedLeft.length === normalizedRight.length &&\n\t\tnormalizedLeft.every((toolName, index) => toolName === normalizedRight[index])\n\t);\n}\n\nfunction snapshotContractError(reason: string): Error {\n\treturn new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);\n}\n\n/**\n * Clone a finalized message replacement into the JSON-compatible snapshot\n * persisted by SessionManager. Undefined remains allowed as ordinary optional\n * message data: JSON omits object properties with undefined values and writes\n * undefined array elements as null.\n */\nfunction clonePlainSnapshot(\n\tvalue: unknown,\n\tancestors = new WeakSet<object>(),\n\tcopies = new WeakMap<object, unknown>(),\n): unknown {\n\tif (value === null) return value;\n\n\tswitch (typeof value) {\n\t\tcase \"string\":\n\t\tcase \"boolean\":\n\t\tcase \"undefined\":\n\t\t\treturn value;\n\t\tcase \"number\":\n\t\t\tif (!Number.isFinite(value)) {\n\t\t\t\tthrow snapshotContractError(\"non-finite number values are not allowed\");\n\t\t\t}\n\t\t\treturn value;\n\t\tcase \"bigint\":\n\t\t\tthrow snapshotContractError(\"bigint values are not allowed\");\n\t\tcase \"function\":\n\t\tcase \"symbol\":\n\t\t\tthrow snapshotContractError(`${typeof value} values are not allowed`);\n\t\tcase \"object\":\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow snapshotContractError(`${typeof value} values are not allowed`);\n\t}\n\n\tif (ancestors.has(value)) {\n\t\tthrow snapshotContractError(\"cyclic values are not allowed\");\n\t}\n\tif (copies.has(value)) {\n\t\treturn copies.get(value);\n\t}\n\tancestors.add(value);\n\n\ttry {\n\t\tif (Array.isArray(value)) {\n\t\t\tconst copy: unknown[] = [];\n\t\t\tcopy.length = value.length;\n\t\t\tcopies.set(value, copy);\n\t\t\tfor (const key of Reflect.ownKeys(value)) {\n\t\t\t\tif (key === \"length\") continue;\n\t\t\t\tif (typeof key !== \"string\" || !/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= value.length) {\n\t\t\t\t\tthrow snapshotContractError(\"arrays may only contain indexed values\");\n\t\t\t\t}\n\t\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\t\t\tif (!descriptor || !(\"value\" in descriptor) || !descriptor.enumerable) {\n\t\t\t\t\tthrow snapshotContractError(\"accessor or non-enumerable properties are not allowed\");\n\t\t\t\t}\n\t\t\t\tcopy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);\n\t\t\t}\n\t\t\treturn copy;\n\t\t}\n\n\t\tconst prototype = Object.getPrototypeOf(value);\n\t\tif (prototype !== Object.prototype && prototype !== null) {\n\t\t\tthrow snapshotContractError(\"non-plain objects are not allowed\");\n\t\t}\n\n\t\tconst copy = Object.create(prototype) as Record<string, unknown>;\n\t\tcopies.set(value, copy);\n\t\tfor (const key of Reflect.ownKeys(value)) {\n\t\t\tif (typeof key !== \"string\") {\n\t\t\t\tthrow snapshotContractError(\"symbol-keyed properties are not allowed\");\n\t\t\t}\n\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\t\tif (!descriptor || !(\"value\" in descriptor) || !descriptor.enumerable) {\n\t\t\t\tthrow snapshotContractError(\"accessor or non-enumerable properties are not allowed\");\n\t\t\t}\n\t\t\tObject.defineProperty(copy, key, {\n\t\t\t\tvalue: clonePlainSnapshot(descriptor.value, ancestors, copies),\n\t\t\t\tenumerable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t\twritable: true,\n\t\t\t});\n\t\t}\n\t\treturn copy;\n\t} finally {\n\t\tancestors.delete(value);\n\t}\n}\n\nfunction freezeSnapshot<T>(value: T, seen = new WeakSet<object>()): T {\n\tif (value === null || typeof value !== \"object\" || seen.has(value)) return value;\n\tseen.add(value);\n\tfor (const child of Object.values(value as Record<string, unknown>)) {\n\t\tfreezeSnapshot(child, seen);\n\t}\n\treturn Object.freeze(value);\n}\n\nfunction createImmutableMessageSnapshot(message: AgentMessage): AgentMessage {\n\treturn freezeSnapshot(clonePlainSnapshot(message) as AgentMessage);\n}\n\n// ============================================================================\n// AgentSession Class\n// ============================================================================\n\nexport class AgentSession {\n\treadonly agent: Agent;\n\treadonly sessionManager: SessionManager;\n\treadonly settingsManager: SettingsManager;\n\n\tprivate _scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Thinking mode (default \"manual\"). Persists across turns within the session;\n\t * auto-resolved levels are never written to the user's persisted settings.\n\t */\n\tprivate _thinkingMode: ThinkingMode = \"manual\";\n\n\t/**\n\t * N=8 ring buffer of recent auto-turn task classes (newest first), feeding\n\t * v4's multi-turn prior feature. Never persisted to settings.\n\t */\n\tprivate _taskClassHistory: TaskClassV4[] = [];\n\n\t/**\n\t * Compiled reasoning-router bias snapshot for the opt-in v4 learning path\n\t * (Goal 010 Lane I). Loaded at most once per session and pinned thereafter:\n\t * `_reasoningRouterBiasSnapshotLoaded` flips to `true` on the first v4\n\t * auto-turn that has learning enabled, and `_reasoningRouterBiasSnapshot`\n\t * then stays fixed for the rest of the session (even across later settings\n\t * reloads, and even if the on-disk file changes or the load failed/was\n\t * invalid, in which case it stays `null`). See `_getReasoningRouterBiasSnapshot`.\n\t */\n\tprivate _reasoningRouterBiasSnapshot: RouterBiasSnapshot | null = null;\n\tprivate _reasoningRouterBiasSnapshotLoaded = false;\n\tprivate _lastAutoRouterDecision: RouterAutoDecision | undefined;\n\n\t/**\n\t * AdaptOrch advisory bridge (default-off, global-only opt-in via\n\t * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily\n\t * constructed on the first v4 auto-turn when enabled; stays `null`\n\t * otherwise. The bridge is advisory-only: its hint is fused into the\n\t * resolver as a bounded ±2 step nudge, never as an override.\n\t */\n\tprivate _adaptorchBridge: AdaptorchBridge | null = null;\n\tprivate _adaptorchBridgeInitAttempted = false;\n\n\t// Event subscription state\n\tprivate _unsubscribeAgent?: () => void;\n\tprivate _eventListeners: AgentSessionEventListener[] = [];\n\tprivate _messageEndReplacements = new WeakMap<Extract<AgentEvent, { type: \"message_end\" }>, AgentMessage>();\n\n\t/** Tracks pending steering messages for UI display. Removed when delivered. */\n\tprivate _steeringMessages: string[] = [];\n\t/** Tracks pending follow-up messages for UI display. Removed when delivered. */\n\tprivate _followUpMessages: string[] = [];\n\t/** Messages queued to be included with the next user prompt as context (\"asides\"). */\n\tprivate _pendingNextTurnMessages: CustomMessage[] = [];\n\n\t// Compaction state\n\tprivate _compactionAbortController: AbortController | undefined = undefined;\n\tprivate _autoCompactionAbortController: AbortController | undefined = undefined;\n\tprivate _overflowRecoveryAttempts = 0;\n\tprivate _thresholdCompactionEmergency = false;\n\tprivate _compactionHysteresisState: CompactionHysteresisState = createCompactionHysteresisState();\n\n\t// Branch summarization state\n\tprivate _branchSummaryAbortController: AbortController | undefined = undefined;\n\n\t// Retry state\n\tprivate _retryAbortController: AbortController | undefined = undefined;\n\tprivate _retryAttempt = 0;\n\t// v10.3-Ω: models that already refused/failed this turn — failover must advance,\n\t// not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).\n\tprivate _refusedModels = new Set<string>();\n\t/** True when `--model` / `--provider` pinned this process to one model. */\n\tprivate readonly _modelPinned: boolean;\n\n\t// Bash execution state\n\tprivate readonly _bashService: SessionBashService;\n\tprivate readonly _compactionService: SessionCompactionService;\n\n\t// Extension system\n\tprivate _extensionRunner!: ExtensionRunner;\n\tprivate _turnIndex = 0;\n\n\tprivate _resourceLoader: ResourceLoader;\n\tprivate _customTools: ToolDefinition[];\n\tprivate _baseToolDefinitions: Map<string, ToolDefinition> = new Map();\n\tprivate _mcpManager: McpManager | undefined;\n\tprivate _mcpToolNames: Set<string> = new Set();\n\tprivate _turnMetricsSink: TurnMetricsSink | undefined;\n\tprivate _turnMetricsState: TurnMetricsState | undefined;\n\tprivate _cwd: string;\n\tprivate _repositoryRouterLearningPaths: RepositoryRouterLearningPaths | undefined;\n\tprivate _extensionRunnerRef?: { current?: ExtensionRunner };\n\tprivate _initialActiveToolNames?: string[];\n\tprivate _allowedToolNames?: Set<string>;\n\tprivate _excludedToolNames?: Set<string>;\n\tprivate _baseToolsOverride?: Record<string, AgentTool>;\n\tprivate _loadoutAccessPolicy?: LoadoutAccessPolicy;\n\tprivate _sessionStartEvent: SessionStartEvent;\n\tprivate _extensionUIContext?: ExtensionUIContext;\n\tprivate _extensionMode: ExtensionMode = \"print\";\n\tprivate _extensionCommandContextActions?: ExtensionCommandContextActions;\n\tprivate _extensionAbortHandler?: () => void;\n\tprivate _extensionShutdownHandler?: ShutdownHandler;\n\tprivate _extensionErrorListener?: ExtensionErrorListener;\n\tprivate _extensionErrorUnsubscriber?: () => void;\n\n\t// Required durable run/audit chain, persisted next to the session file.\n\tprivate readonly _runJournalStore: RunJournalStore;\n\tprivate _ownedSessionOwnerLease: SessionOwnerLease | undefined;\n\tprivate _activeRunId: string | null = null;\n\t// Resource governor (roadmap M2): lease controller owns the temporary tool\n\t// cap for governed runs; the last decision backs the §19.4 read-only API.\n\tprivate _resourceLeaseController: RunResourceLeaseController | undefined;\n\tprivate _resourceObservations: ResourceObservationJournal | null = null;\n\tprivate readonly _resourceObservationJournals = new Map<string, ResourceObservationJournal>();\n\tprivate _latestResourcePromptRunId: string | null = null;\n\tprivate _lastResourceAdmission: ResourceAdmissionDecision | null = null;\n\tprivate _workloadPermitPool: WorkloadPermitPool | undefined;\n\tprivate _pendingRuntimeTerminationCause: SessionTerminationCause | undefined;\n\tprivate _activeRunToolTermination:\n\t\t| { toolCallId: string; toolName: string; timeoutMs?: number; executionStarted: boolean }\n\t\t| undefined;\n\tprivate _lastTermination: SessionTermination | undefined;\n\tprivate _userAbortRequested = false;\n\tprivate readonly _replayLedger: ReplayLedgerManager | undefined;\n\tprivate readonly _replayGoalId: string | undefined;\n\tprivate readonly _replayLaneId: string | undefined;\n\tprivate readonly _bashRuntime: SessionBashRuntime;\n\tprivate _transcriptRepair: SessionTranscriptRepair | undefined;\n\tprivate _sessionRiskLevel: \"normal\" | \"elevated\" = \"normal\";\n\tprivate _workspaceMutationCount = 0;\n\n\t// Model registry for API key resolution\n\tprivate _modelRegistry: ModelRegistry;\n\t/** Lazy, in-memory only; never shared across sessions or persisted. */\n\tprivate _contextBudgetCacheProvider: ContextBudgetCacheProviderV2 | undefined;\n\tprivate _contextCacheInvalidationSnapshot: ContextCacheInvalidationSnapshot;\n\n\t// Tool registry for extension getTools/setTools\n\tprivate _toolRegistry: Map<string, AgentTool> = new Map();\n\tprivate _toolDefinitions: Map<string, ToolDefinitionEntry> = new Map();\n\tprivate _toolPromptSnippets: Map<string, string> = new Map();\n\tprivate _toolPromptGuidelines: Map<string, string[]> = new Map();\n\n\t// Base system prompt (without extension appends) - used to apply fresh appends each turn\n\tprivate _baseSystemPrompt = \"\";\n\tprivate _baseSystemPromptCacheBoundary: number | undefined;\n\tprivate _baseSystemPromptOptions!: BuildSystemPromptOptions;\n\tprivate _promptCacheKey: string | undefined;\n\tprivate _promptCacheKeyChanges = 0;\n\tprivate _promptCacheBoundaryBypasses = 0;\n\tprivate _promptCacheStablePrefixCharacters = 0;\n\tprivate _promptCacheLastBreakReason: string | undefined;\n\n\tconstructor(config: AgentSessionConfig) {\n\t\tthis.agent = config.agent;\n\t\tthis.sessionManager = config.sessionManager;\n\t\tthis.settingsManager = config.settingsManager;\n\t\tthis._scopedModels = config.scopedModels ?? [];\n\t\tthis._modelPinned = config.modelPinned === true;\n\t\tthis._resourceLoader = config.resourceLoader;\n\t\tthis._customTools = config.customTools ?? [];\n\t\tthis._cwd = config.cwd;\n\t\tthis._modelRegistry = config.modelRegistry;\n\t\tconst initialModelId = this._contextCacheModelId(this.agent.state.model);\n\t\tthis._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({\n\t\t\tforkId: this.sessionManager.getSessionId(),\n\t\t\tworktreeFingerprint: this._contextCacheWorktreeFingerprint(),\n\t\t\tactiveModelId: initialModelId,\n\t\t\tcompactionModelId: initialModelId,\n\t\t});\n\t\tthis._extensionRunnerRef = config.extensionRunnerRef;\n\t\tthis._initialActiveToolNames = config.initialActiveToolNames;\n\t\tthis._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;\n\t\tthis._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;\n\t\tthis._baseToolsOverride = config.baseToolsOverride;\n\t\tthis._loadoutAccessPolicy = config.loadoutAccessPolicy;\n\n\t\tthis._sessionStartEvent = config.sessionStartEvent ?? { type: \"session_start\", reason: \"startup\" };\n\t\tthis._transcriptRepair = config.transcriptRepair;\n\t\tthis._replayLedger = config.replayLedger;\n\t\tthis._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;\n\t\tthis._replayLaneId = config.replayLaneId;\n\t\tif (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {\n\t\t\tthrow new Error(\"AgentSession replayGoalId does not match the replay ledger goal id\");\n\t\t}\n\t\tthis._bashRuntime = new SessionBashRuntime({\n\t\t\tcwd: this._cwd,\n\t\t\tgetExecutionCwd: () => this.sessionManager.getCwd(),\n\t\t\tgetSessionFile: () => this.sessionManager.getSessionFile(),\n\t\t\treplayLedger: this._replayLedger,\n\t\t\treplayGoalId: this._replayGoalId,\n\t\t\treplayLaneId: this._replayLaneId,\n\t\t\tconfiguredSandboxPreflight: config.bashSandboxPreflight,\n\t\t\tappendReplayEvent: (type, payload) => this._appendReplayEvent(type, payload),\n\t\t});\n\t\tthis._bashService = new SessionBashService({\n\t\t\tsettings: this.settingsManager,\n\t\t\truntime: this._bashRuntime,\n\t\t\tloadoutPolicy: this._loadoutAccessPolicy,\n\t\t\tgetCwd: () => this.sessionManager.getCwd(),\n\t\t\temit: (event) => this._emit(event),\n\t\t\tisStreaming: () => this.isStreaming,\n\t\t\tpushMessage: (message) => this.agent.state.messages.push(message),\n\t\t\tappendMessage: (message) => this.sessionManager.appendMessage(message),\n\t\t\tacquireResourcePermit: (command, signal) => this._acquireBashResourcePermit(command, signal),\n\t\t});\n\t\tthis._compactionService = new SessionCompactionService({\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tpendingToolCallIds: () => this.agent.state.pendingToolCalls,\n\t\t\tgetUserMessageText: (message) => this._getUserMessageText(message),\n\t\t\tcwd: this._cwd,\n\t\t\tinvalidateContextBudget: () => this._invalidateContextBudgetCache({ type: \"transcriptRepair\" }),\n\t\t\trefreshAgentMessages: () => {\n\t\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\t\t\t},\n\t\t\trecordCommit: () => this._recordCompactionCommitForHysteresis(),\n\t\t});\n\t\tconst sessionFile = this.sessionManager.getSessionFile();\n\t\tlet ownerLease = this.sessionManager.getOwnerLease();\n\t\ttry {\n\t\t\tif (sessionFile && !ownerLease) {\n\t\t\t\townerLease = acquireSessionOwnerLeaseSync(sessionFile);\n\t\t\t\tthis._ownedSessionOwnerLease = ownerLease;\n\t\t\t\tthis.sessionManager.setOwnerLease(ownerLease);\n\t\t\t}\n\t\t\tthis._runJournalStore = RunJournalStore.open({\n\t\t\t\t...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),\n\t\t\t\tsessionId: this.sessionManager.getSessionId(),\n\t\t\t\t...(ownerLease ? { ownerLease } : {}),\n\t\t\t});\n\t\t\tconst startupRecords = this._runJournalStore.records;\n\t\t\tconst startupTerminal = startupRecords[startupRecords.length - 1];\n\t\t\tif (startupTerminal?.event === \"run_recovered\") {\n\t\t\t\tthis._lastTermination = startupTerminal.termination;\n\t\t\t}\n\n\t\t\t// Always subscribe to agent events for internal handling\n\t\t\t// (session persistence, extensions, auto-compaction, retry logic)\n\t\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t\t\tthis._installAgentToolHooks();\n\n\t\t\t// Durable transcript-repair audit for a repair applied on open/resume.\n\t\t\tif (this._transcriptRepair) {\n\t\t\t\tthis._invalidateContextBudgetCache({ type: \"transcriptRepair\" });\n\t\t\t\tthis._appendRunJournalAudit(\"transcript_repaired\", {\n\t\t\t\t\tinsertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],\n\t\t\t\t\treason: this._transcriptRepair.reason,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tthis._buildRuntime({\n\t\t\t\tactiveToolNames: this._initialActiveToolNames,\n\t\t\t\tincludeAllExtensionTools: true,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconst ownedLease = this._ownedSessionOwnerLease;\n\t\t\tif (ownedLease) {\n\t\t\t\ttry {\n\t\t\t\t\townedLease.release();\n\t\t\t\t\tif (this.sessionManager.getOwnerLease() === ownedLease) this.sessionManager.setOwnerLease(undefined);\n\t\t\t\t\tthis._ownedSessionOwnerLease = undefined;\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new Error(\"AgentSession initialization cleanup failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/** Model registry for API key resolution and model discovery */\n\tget modelRegistry(): ModelRegistry {\n\t\treturn this._modelRegistry;\n\t}\n\n\t/** Transcript repair applied while this session was opened/resumed, if any. */\n\tget transcriptRepair(): SessionTranscriptRepair | undefined {\n\t\treturn this._transcriptRepair;\n\t}\n\n\t/** Durable lifecycle and audit records appended by this session's run journal. */\n\tget runJournalRecords(): readonly RunJournalRecord[] {\n\t\treturn this._runJournalStore.records;\n\t}\n\n\t/** Most recently observed or inferred termination for this session. */\n\tget lastTermination(): SessionTermination | undefined {\n\t\treturn this._lastTermination;\n\t}\n\n\t/** Exact trailing journal fragment quarantine performed during startup, if any. */\n\tget runJournalQuarantineReport(): RunJournalQuarantineReport | null {\n\t\treturn this._runJournalStore.quarantineReport;\n\t}\n\n\t/** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */\n\tget sessionRiskLevel(): \"normal\" | \"elevated\" {\n\t\treturn this._sessionRiskLevel;\n\t}\n\n\t/** Monotonic count of workspace mutation/invalidation signals emitted by this session. */\n\tget workspaceMutationCount(): number {\n\t\treturn this._workspaceMutationCount;\n\t}\n\n\tget contextCacheInvalidationSnapshot(): ContextCacheInvalidationSnapshot {\n\t\treturn this._contextCacheInvalidationSnapshot;\n\t}\n\n\tprivate _contextCacheModelId(model: Model<any> | undefined): string {\n\t\treturn model\n\t\t\t? `model-${createHash(\"sha256\").update(`${model.provider}\\0${model.id}`, \"utf8\").digest(\"hex\")}`\n\t\t\t: \"unknown\";\n\t}\n\n\tprivate _contextCacheWorktreeFingerprint(): string {\n\t\treturn createHash(\"sha256\").update(`${this._cwd}\\0${this._workspaceMutationCount}`, \"utf8\").digest(\"hex\");\n\t}\n\n\tprivate _invalidateContextBudgetCache(event: ContextCacheInvalidationEvent): void {\n\t\tconst result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);\n\t\tthis._contextCacheInvalidationSnapshot = result.snapshot;\n\t\tif (result.status === \"overflow\") {\n\t\t\tthis._contextBudgetCacheProvider = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);\n\t}\n\n\tprivate _recordEvidenceReceiptInvalidation(customType: string): void {\n\t\tif (customType === \"evidence_receipt\" || customType === \"evidence-receipt\") {\n\t\t\tthis._invalidateContextBudgetCache({ type: \"evidenceReceipt\" });\n\t\t}\n\t}\n\n\tprivate _sessionRevision(): number {\n\t\treturn this.sessionManager.getEntries().length;\n\t}\n\n\tprivate _appendReplayEvent(type: ReplayEventType, payload: unknown): void {\n\t\tif (!this._replayLedger || !this._replayGoalId) return;\n\t\tthis._replayLedger.append({\n\t\t\ttype,\n\t\t\tgoalId: this._replayGoalId,\n\t\t\t...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),\n\t\t\tpayload,\n\t\t});\n\t\tthis._replayLedger.persist();\n\t}\n\n\t/** Lazy verified-bash executor bound to this session's replay ledger (default-on path). */\n\tprivate _getVerifiedEvidenceExecutor(): VerifiedEvidenceExecutor | undefined {\n\t\treturn this._bashRuntime.verifiedEvidenceExecutor();\n\t}\n\n\t/** Append one required durable audit record. Persistence failures propagate. */\n\tprivate _appendRunJournalAudit(event: RunJournalAuditEvent, details: RunJournalAuditDetails): void {\n\t\tthis._runJournalStore.audit({\n\t\t\tevent,\n\t\t\tdetails,\n\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\t\tthis._appendReplayEvent(event, details);\n\t}\n\n\tprivate _terminationMessage(value: string | undefined, fallback: string): string {\n\t\tconst redacted = redactSensitiveText(value?.trim() || fallback)\n\t\t\t.replace(/\\0/g, \"\")\n\t\t\t.slice(0, 512);\n\t\treturn redacted || fallback;\n\t}\n\n\tprivate _providerFailureCause(message: AssistantMessage): SessionTerminationCause {\n\t\tconst text = message.errorMessage ?? \"\";\n\t\tif (isContextOverflow(message, this.model?.contextWindow ?? 0)) {\n\t\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t\t}\n\t\t// Fable/Claude often emit stop_reason=refusal on benign turns (false positive).\n\t\t// Match broadly so we never mislabel these as provider.protocol.\n\t\tif (\n\t\t\t/stop_reason\\s*=\\s*(refusal|sensitive)|content\\/safety stop|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\t\t\ttext,\n\t\t\t)\n\t\t) {\n\t\t\treturn { area: \"provider\", code: \"refusal\" };\n\t\t}\n\t\t// Quota/billing exhaustion is checked BEFORE the generic 401/403 auth\n\t\t// patterns: \"403 ... usage limit for this billing cycle\" is transient per\n\t\t// cycle and must fail over, not terminate the turn as an auth error.\n\t\tif (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t\t}\n\t\tif (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\\/login/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"auth\" };\n\t\t}\n\t\t// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.\n\t\t// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.\n\t\tif (/tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"protocol\" };\n\t\t}\n\t\tif (/tool.+timed? out|tool.+timeout/i.test(text)) return { area: \"tool\", code: \"timeout\" };\n\t\tif (/\\btool\\b/i.test(text) && !/tool_call_id/i.test(text)) return { area: \"tool\", code: \"fatal\" };\n\t\tif (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"network\" };\n\t\t}\n\t\treturn { area: \"provider\", code: \"protocol\" };\n\t}\n\n\tprivate _classifyPreflightCause(message: string): SessionTerminationCause {\n\t\tif (!this.model || /no model|model selected|model is required/i.test(message)) {\n\t\t\treturn { area: \"configuration\", code: \"invalid\" };\n\t\t}\n\t\tif (isQuotaExhaustionMessage(message)) {\n\t\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t\t}\n\t\tif (/auth|api key|unauthori[sz]ed|forbidden|401|403|\\/login/i.test(message)) {\n\t\t\treturn { area: \"provider\", code: \"auth\" };\n\t\t}\n\t\tif (/context.+overflow|context window|too many tokens/i.test(message)) {\n\t\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t\t}\n\t\tif (/duplicate.?result/i.test(message)) return { area: \"transcript\", code: \"duplicate_result\" };\n\t\tif (/orphan.?result/i.test(message)) return { area: \"transcript\", code: \"orphan_result\" };\n\t\tif (/duplicate.?call/i.test(message)) return { area: \"transcript\", code: \"duplicate_call_id\" };\n\t\tif (/transcript|missing.?result/i.test(message)) return { area: \"transcript\", code: \"missing_result\" };\n\t\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\t\tif (/fsync/i.test(message)) return { area: \"persistence\", code: \"fsync_failed\" };\n\t\tif (/lock/i.test(message)) return { area: \"persistence\", code: \"lock_failed\" };\n\t\tif (/append|persist|write/i.test(message)) return { area: \"persistence\", code: \"append_failed\" };\n\t\tif (/tool/i.test(message)) return { area: \"tool\", code: \"fatal\" };\n\t\treturn { area: \"internal\", code: \"unclassified\" };\n\t}\n\n\tprivate _classifyRunTermination(\n\t\trunId: string,\n\t\tevent: Extract<AgentEvent, { type: \"agent_end\" }>,\n\t): SessionTermination {\n\t\tconst timestamp = new Date().toISOString();\n\t\tlet assistant: AssistantMessage | undefined;\n\t\tfor (let index = event.messages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = event.messages[index];\n\t\t\tif (message?.role === \"assistant\") {\n\t\t\t\tassistant = message;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tlet cause: SessionTerminationCause;\n\t\tlet message: string;\n\t\tlet sideEffects: \"none\" | \"possible\" = this._sessionRiskLevel === \"elevated\" ? \"possible\" : \"none\";\n\t\tlet toolCallId: string | undefined;\n\t\tlet toolName: string | undefined;\n\n\t\tif (this._activeRunToolTermination) {\n\t\t\tcause = { area: \"tool\", code: \"timeout\" };\n\t\t\tmessage = `Tool ${this._activeRunToolTermination.toolName} timed out.`;\n\t\t\ttoolCallId = this._activeRunToolTermination.toolCallId;\n\t\t\ttoolName = this._activeRunToolTermination.toolName;\n\t\t\tsideEffects = this._activeRunToolTermination.executionStarted ? \"possible\" : sideEffects;\n\t\t} else if (!assistant) {\n\t\t\tcause = { area: \"internal\", code: \"unclassified\" };\n\t\t\tmessage = \"Agent run ended without an assistant result.\";\n\t\t} else if (assistant.stopReason === \"aborted\") {\n\t\t\tcause = this._userAbortRequested ? { area: \"user\", code: \"abort\" } : { area: \"provider\", code: \"abort\" };\n\t\t\tmessage = this._terminationMessage(\n\t\t\t\tassistant.errorMessage,\n\t\t\t\tthis._userAbortRequested ? \"The user aborted the run.\" : \"The provider aborted the run.\",\n\t\t\t);\n\t\t} else if (assistant.stopReason === \"error\") {\n\t\t\tcause = this._providerFailureCause(assistant);\n\t\t\tmessage = this._terminationMessage(assistant.errorMessage, \"The provider request failed.\");\n\t\t} else {\n\t\t\tcause = { area: \"completed\" };\n\t\t\tmessage = \"Run completed.\";\n\t\t}\n\n\t\tlet provider = this.model?.provider;\n\t\tif (assistant?.provider) provider = assistant.provider;\n\t\tlet model = this.model?.id;\n\t\tif (assistant?.model) model = assistant.model;\n\n\t\treturn classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage,\n\t\t\tcause,\n\t\t\tsideEffects,\n\t\t\t...(provider ? { provider } : {}),\n\t\t\t...(model ? { model } : {}),\n\t\t\t...(toolCallId ? { toolCallId } : {}),\n\t\t\t...(toolName ? { toolName } : {}),\n\t\t});\n\t}\n\n\tprivate _publishTermination(termination: SessionTermination): void {\n\t\tthis._lastTermination = termination;\n\t\tthis._emit({ type: \"session_termination\", termination });\n\t}\n\n\tprivate _runtimeFailureCause(error: unknown): SessionTerminationCause {\n\t\tif (this._pendingRuntimeTerminationCause) return this._pendingRuntimeTerminationCause;\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error ? Reflect.get(error, \"code\") : undefined;\n\t\tif (\n\t\t\ttypeof code === \"string\" &&\n\t\t\tnew Set([\"EACCES\", \"EDQUOT\", \"EFBIG\", \"EIO\", \"EISDIR\", \"EMFILE\", \"ENFILE\", \"ENOSPC\", \"EPERM\", \"EROFS\"]).has(\n\t\t\t\tcode,\n\t\t\t)\n\t\t) {\n\t\t\treturn { area: \"persistence\", code: \"append_failed\" };\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (/compaction.+stale|session changed during compaction/i.test(message)) {\n\t\t\treturn { area: \"compaction\", code: \"stale\" };\n\t\t}\n\t\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\t\treturn { area: \"internal\", code: \"unclassified\" };\n\t}\n\n\tprivate _publishRuntimeFailure(error: unknown): void {\n\t\tconst cause = this._runtimeFailureCause(error);\n\t\tconst runId = this._activeRunId ?? `runtime-${randomUUID()}`;\n\t\tconst timestamp = new Date().toISOString();\n\t\tlet message = \"The AgentSession runtime failed before completing the run.\";\n\t\tif (cause.area === \"persistence\") {\n\t\t\tmessage = \"A required runtime persistence operation failed.\";\n\t\t} else if (cause.area === \"compaction\") {\n\t\t\tmessage = \"Runtime compaction failed.\";\n\t\t}\n\t\tlet termination = classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage,\n\t\t\tcause,\n\t\t\tsideEffects: this._activeRunId === null ? \"none\" : \"possible\",\n\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t});\n\t\tif (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {\n\t\t\ttry {\n\t\t\t\tthis._runJournalStore.finish({\n\t\t\t\t\ttermination,\n\t\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\t\ttimestamp,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\ttermination = classifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: \"The run journal could not persist the runtime termination.\",\n\t\t\t\t\tcause: { area: \"persistence\", code: \"append_failed\" },\n\t\t\t\t\tsideEffects: \"possible\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis._activeRunId = null;\n\t\tthis._activeRunToolTermination = undefined;\n\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\tthis._userAbortRequested = false;\n\t\tthis._publishTermination(termination);\n\t}\n\n\tprivate _handleRunLifecycleEvent(event: AgentEvent): void {\n\t\tif (event.type === \"agent_start\") {\n\t\t\tif (this._activeRunId !== null) throw new Error(\"run journal already has an active AgentSession run\");\n\t\t\tconst runId = randomUUID();\n\t\t\tthis._activeRunId = runId;\n\t\t\tthis._activeRunToolTermination = undefined;\n\t\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\t\tthis._userAbortRequested = false;\n\t\t\ttry {\n\t\t\t\tthis._runJournalStore.start({\n\t\t\t\t\trunId,\n\t\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tthis._pendingRuntimeTerminationCause = { area: \"persistence\", code: \"append_failed\" };\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (event.type !== \"agent_end\") return;\n\t\tif (this._activeRunId === null) throw new Error(\"run journal received agent_end without run_started\");\n\t\tconst termination = this._classifyRunTermination(this._activeRunId, event);\n\t\ttry {\n\t\t\tthis._runJournalStore.finish({\n\t\t\t\ttermination,\n\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\ttimestamp: termination.timestamp,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis._pendingRuntimeTerminationCause = { area: \"persistence\", code: \"append_failed\" };\n\t\t\tthrow error;\n\t\t}\n\t\tthis._activeRunId = null;\n\t\tthis._activeRunToolTermination = undefined;\n\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\tthis._userAbortRequested = false;\n\t\tthis._publishTermination(termination);\n\t}\n\n\t/**\n\t * Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late\n\t * settlement of a potentially-writing tool raises session risk and emits a\n\t * workspace mutation/invalidation signal for evidence freshness consumers.\n\t */\n\tprivate _handleToolAuditEvent(event: AgentEvent): void {\n\t\tif (event.type === \"tool_execution_end\") {\n\t\t\tthis._invalidateContextBudgetCache({ type: \"toolResultDisposition\" });\n\t\t\tconst envelope = (event.result as { details?: { omk?: Record<string, unknown> } } | undefined)?.details?.omk;\n\t\t\tif (envelope && envelope.schema === \"tool-result/v2\" && envelope.disposition === \"timeout\") {\n\t\t\t\tconst timeout = {\n\t\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t...(typeof envelope.timeoutMs === \"number\" ? { timeoutMs: envelope.timeoutMs } : {}),\n\t\t\t\t\texecutionStarted: envelope.executionStarted === true,\n\t\t\t\t};\n\t\t\t\tthis._activeRunToolTermination = timeout;\n\t\t\t\tthis._appendRunJournalAudit(\"tool_timeout\", timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (event.type !== \"tool_execution_late_settlement\") {\n\t\t\treturn;\n\t\t}\n\t\t// Fail closed: anything not classified as a read-category tool may write.\n\t\tconst potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== \"read\";\n\t\tthis._appendRunJournalAudit(\"tool_late_settlement\", {\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\tdisposition: event.disposition,\n\t\t\toutcome: event.outcome,\n\t\t\t...(potentiallyWriting ? { sessionRisk: \"elevated\" as const } : {}),\n\t\t});\n\t\tif (potentiallyWriting) {\n\t\t\tthis._sessionRiskLevel = \"elevated\";\n\t\t\tthis._workspaceMutationCount += 1;\n\t\t\tthis._invalidateContextBudgetCache({\n\t\t\t\ttype: \"worktreeFingerprint\",\n\t\t\t\tvalue: this._contextCacheWorktreeFingerprint(),\n\t\t\t});\n\t\t\tconst payload = { root: this._cwd, paths: [] as readonly string[] };\n\t\t\tthis._appendReplayEvent(\"workspace_mutation\", payload);\n\t\t\tthis._emit({\n\t\t\t\ttype: \"workspace_mutation\",\n\t\t\t\tsource: \"tool_late_settlement\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\tpayload,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _getBashSandboxPreflight(override?: BashSandboxPreflight): BashSandboxPreflight | undefined {\n\t\treturn this._bashRuntime.sandboxPreflight(override);\n\t}\n\n\tprivate async _getRequiredRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tconst result = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\tif (!result.ok) {\n\t\t\tif (result.error.startsWith(\"No API key found\")) {\n\t\t\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t\t\t}\n\t\t\tthrow new Error(result.error);\n\t\t}\n\t\tif (result.apiKey) {\n\t\t\treturn { apiKey: result.apiKey, headers: result.headers };\n\t\t}\n\n\t\tconst isOAuth = this._modelRegistry.isUsingOAuth(model);\n\t\tif (isOAuth) {\n\t\t\tthrow new Error(\n\t\t\t\t`Authentication failed for \"${model.provider}\". ` +\n\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t`Run '/login ${model.provider}' to re-authenticate.`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t}\n\n\tprivate async _getCompactionRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.agent.streamFn === streamSimple) {\n\t\t\treturn this._getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Install tool hooks once on the Agent instance.\n\t *\n\t * The callbacks read `this._extensionRunner` at execution time, so extension reload swaps in the\n\t * new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt\n\t * registered tool execution to the extension context. Tool call and tool result interception now\n\t * happens here instead of in wrappers.\n\t */\n\tprivate _installAgentToolHooks(): void {\n\t\tthis.agent.beforeToolCall = async ({ toolCall, args }) => {\n\t\t\tif (this._loadoutAccessPolicy && !this._loadoutAccessPolicy.activeTools.includes(toolCall.name)) {\n\t\t\t\tthrow new Error(`loadout: inactive tool: ${toolCall.name}`);\n\t\t\t}\n\t\t\tconst runner = this._extensionRunner;\n\t\t\tif (!runner.hasHandlers(\"tool_call\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn await runner.emitToolCall({\n\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`Extension failed, blocking execution: ${String(err)}`);\n\t\t\t}\n\t\t};\n\n\t\tthis.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {\n\t\t\tconst runner = this._extensionRunner;\n\t\t\tif (!runner.hasHandlers(\"tool_result\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst hookResult = await runner.emitToolResult({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\tcontent: result.content,\n\t\t\t\tdetails: result.details,\n\t\t\t\tisError,\n\t\t\t});\n\n\t\t\tif (!hookResult) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: hookResult.content,\n\t\t\t\tdetails: hookResult.details,\n\t\t\t\tisError: hookResult.isError ?? isError,\n\t\t\t};\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Event Subscription\n\t// =========================================================================\n\n\t/** Emit an event to all listeners */\n\tprivate _emit(event: AgentSessionEvent): void {\n\t\tfor (const l of this._eventListeners) {\n\t\t\tl(event);\n\t\t}\n\t}\n\n\tprivate _emitQueueUpdate(): void {\n\t\tthis._emit({\n\t\t\ttype: \"queue_update\",\n\t\t\tsteering: [...this._steeringMessages],\n\t\t\tfollowUp: [...this._followUpMessages],\n\t\t});\n\t}\n\n\t// Track last assistant message for auto-compaction check\n\tprivate _lastAssistantMessage: AssistantMessage | undefined = undefined;\n\n\t/** Internal handler for agent events - shared by subscribe and reconnect */\n\tprivate _handleAgentEvent = async (event: AgentEvent): Promise<void> => {\n\t\t// Required run lifecycle persistence executes before extension/user listeners.\n\t\tthis._handleRunLifecycleEvent(event);\n\n\t\t// When a user message starts, check if it's from either queue and remove it BEFORE emitting\n\t\t// This ensures the UI sees the updated queue state\n\t\tif (event.type === \"message_start\" && event.message.role === \"user\") {\n\t\t\tthis._overflowRecoveryAttempts = 0;\n\t\t\tconst messageText = this._getUserMessageText(event.message);\n\t\t\tif (messageText) {\n\t\t\t\t// Check steering queue first\n\t\t\t\tconst steeringIndex = this._steeringMessages.indexOf(messageText);\n\t\t\t\tif (steeringIndex !== -1) {\n\t\t\t\t\tthis._steeringMessages.splice(steeringIndex, 1);\n\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t} else {\n\t\t\t\t\t// Check follow-up queue\n\t\t\t\t\tconst followUpIndex = this._followUpMessages.indexOf(messageText);\n\t\t\t\t\tif (followUpIndex !== -1) {\n\t\t\t\t\t\tthis._followUpMessages.splice(followUpIndex, 1);\n\t\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Durable tool timeout / late-settlement audits (ALG004-A/B).\n\t\tthis._handleToolAuditEvent(event);\n\n\t\t// Emit to extensions first\n\t\tawait this._emitExtensionEvent(event);\n\t\tconst replacement = event.type === \"message_end\" ? this._messageEndReplacements.get(event) : undefined;\n\t\tif (event.type === \"message_end\") {\n\t\t\tthis._messageEndReplacements.delete(event);\n\t\t}\n\t\tconst finalizedEvent: AgentEvent =\n\t\t\treplacement === undefined ? event : (Object.freeze({ ...event, message: replacement }) as AgentEvent);\n\n\t\t// Notify all listeners\n\t\tthis._emit(\n\t\t\tfinalizedEvent.type === \"agent_end\"\n\t\t\t\t? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }\n\t\t\t\t: finalizedEvent,\n\t\t);\n\n\t\t// Handle session persistence\n\t\tif (finalizedEvent.type === \"message_end\") {\n\t\t\t// Check if this is a custom message from extensions\n\t\t\tif (finalizedEvent.message.role === \"custom\") {\n\t\t\t\t// Persist as CustomMessageEntry\n\t\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\t\tfinalizedEvent.message.customType,\n\t\t\t\t\tfinalizedEvent.message.content,\n\t\t\t\t\tfinalizedEvent.message.display,\n\t\t\t\t\tfinalizedEvent.message.details,\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tfinalizedEvent.message.role === \"user\" ||\n\t\t\t\tfinalizedEvent.message.role === \"assistant\" ||\n\t\t\t\tfinalizedEvent.message.role === \"toolResult\"\n\t\t\t) {\n\t\t\t\t// Regular LLM message - persist as SessionMessageEntry\n\t\t\t\tthis.sessionManager.appendMessage(finalizedEvent.message);\n\t\t\t}\n\t\t\t// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere\n\n\t\t\t// Track assistant message for auto-compaction (checked on agent_end)\n\t\t\tif (finalizedEvent.message.role === \"assistant\") {\n\t\t\t\tthis._lastAssistantMessage = finalizedEvent.message;\n\n\t\t\t\tconst assistantMsg = finalizedEvent.message as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason !== \"error\") {\n\t\t\t\t\tthis._overflowRecoveryAttempts = 0;\n\t\t\t\t}\n\n\t\t\t\t// Reset retry counter immediately on successful assistant response\n\t\t\t\t// This prevents accumulation across multiple LLM calls within a turn\n\t\t\t\tif (assistantMsg.stopReason !== \"error\" && this._retryAttempt > 0) {\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\t\t});\n\t\t\t\t\tthis._retryAttempt = 0;\n\t\t\t\t\tthis._refusedModels.clear();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tprivate _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: \"agent_end\" }>): boolean {\n\t\tconst settings = this.settingsManager.getRetrySettings();\n\t\tif (!settings.enabled) return false;\n\n\t\tfor (let i = event.messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = event.messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t\tconst maxRetries = retryBudgetForAssistantError(assistant, settings.maxRetries);\n\t\t\t\treturn this._retryAttempt < maxRetries && this._isRetryableError(assistant);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Extract text content from a message */\n\tprivate _getUserMessageText(message: Message): string {\n\t\tif (message.role !== \"user\") return \"\";\n\t\tconst content = message.content;\n\t\tif (typeof content === \"string\") return content;\n\t\tconst textBlocks = content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as TextContent).text).join(\"\");\n\t}\n\n\t/** Find the last assistant message in agent state (including aborted ones) */\n\tprivate _findLastAssistantMessage(): AssistantMessage | undefined {\n\t\tconst messages = this.agent.state.messages;\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst msg = messages[i];\n\t\t\tif (msg.role === \"assistant\") {\n\t\t\t\treturn msg as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate _replaceFinalizedMessage(target: AgentMessage, replacement: AgentMessage): AgentMessage {\n\t\tconst messages = this.agent.state.messages;\n\t\tconst matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));\n\t\tif (matchingIndexes.length !== 1) {\n\t\t\tthrow new Error(\n\t\t\t\tmatchingIndexes.length === 0\n\t\t\t\t\t? \"Finalized message was not found in agent state\"\n\t\t\t\t\t: \"Finalized message is ambiguous in agent state\",\n\t\t\t);\n\t\t}\n\n\t\tconst matchingIndex = matchingIndexes[0];\n\t\tif (matchingIndex === undefined) {\n\t\t\tthrow new Error(\"Finalized message was not found in agent state\");\n\t\t}\n\t\tconst finalizedReplacement = createImmutableMessageSnapshot(replacement);\n\t\tconst nextMessages = [...messages];\n\t\tnextMessages[matchingIndex] = finalizedReplacement;\n\t\tthis.agent.state.messages = nextMessages;\n\t\treturn finalizedReplacement;\n\t}\n\n\t/** Emit extension events based on agent events */\n\tprivate async _emitExtensionEvent(event: AgentEvent): Promise<void> {\n\t\tif (event.type === \"agent_start\") {\n\t\t\tthis._turnIndex = 0;\n\t\t\tawait this._extensionRunner.emit({ type: \"agent_start\" });\n\t\t} else if (event.type === \"agent_end\") {\n\t\t\tawait this._extensionRunner.emit({ type: \"agent_end\", messages: event.messages });\n\t\t} else if (event.type === \"turn_start\") {\n\t\t\tthis._turnMetricsState = {\n\t\t\t\tstartedAtEpochMs: Date.now(),\n\t\t\t\ttoolCalls: [],\n\t\t\t\ttoolStarts: new Map(),\n\t\t\t\trequestedModel: this.model ? `${this.model.provider}/${this.model.id}` : null,\n\t\t\t\trequestedThinking: this.thinkingMode === \"auto\" ? \"auto\" : this.thinkingLevel,\n\t\t\t};\n\t\t\tconst extensionEvent: TurnStartEvent = {\n\t\t\t\ttype: \"turn_start\",\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"turn_end\") {\n\t\t\tthis._recordTurnMetrics(event.message);\n\t\t\tconst extensionEvent: TurnEndEvent = {\n\t\t\t\ttype: \"turn_end\",\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\tmessage: event.message,\n\t\t\t\ttoolResults: event.toolResults,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t\tthis._turnIndex++;\n\t\t} else if (event.type === \"message_start\") {\n\t\t\tconst extensionEvent: MessageStartEvent = {\n\t\t\t\ttype: \"message_start\",\n\t\t\t\tmessage: event.message,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"message_update\") {\n\t\t\tconst extensionEvent: MessageUpdateEvent = {\n\t\t\t\ttype: \"message_update\",\n\t\t\t\tmessage: event.message,\n\t\t\t\tassistantMessageEvent: event.assistantMessageEvent,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"message_end\") {\n\t\t\tconst extensionEvent: MessageEndEvent = {\n\t\t\t\ttype: \"message_end\",\n\t\t\t\tmessage: event.message,\n\t\t\t};\n\t\t\tconst replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);\n\t\t\tif (replacement) {\n\t\t\t\tconst finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);\n\t\t\t\tthis._messageEndReplacements.set(event, finalizedReplacement);\n\t\t\t}\n\t\t} else if (event.type === \"tool_execution_start\") {\n\t\t\tthis._turnMetricsState?.toolStarts.set(event.toolCallId, Date.now());\n\t\t\tconst extensionEvent: ToolExecutionStartEvent = {\n\t\t\t\ttype: \"tool_execution_start\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\targs: event.args,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"tool_execution_update\") {\n\t\t\tconst extensionEvent: ToolExecutionUpdateEvent = {\n\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\targs: event.args,\n\t\t\t\tpartialResult: event.partialResult,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"tool_execution_end\") {\n\t\t\tthis._recordToolMetric(event.toolCallId, event.toolName, event.isError === true, event.result);\n\t\t\tconst extensionEvent: ToolExecutionEndEvent = {\n\t\t\t\ttype: \"tool_execution_end\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\tresult: event.result,\n\t\t\t\tisError: event.isError,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t}\n\t}\n\n\t/**\n\t * Turn metrics sink for this session, created on first use.\n\t *\n\t * Writes to `<cwd>/.omk/metrics/`. Set `OMK_TURN_METRICS=0` to disable, or\n\t * `OMK_TURN_METRICS_DIR` to relocate. Returns `undefined` when disabled, so\n\t * every call site is a no-op rather than a branch.\n\t */\n\tprivate _metricsSink(): TurnMetricsSink | undefined {\n\t\tif (isDisabledEnvValue(process.env.OMK_TURN_METRICS)) return undefined;\n\t\tif (!this._turnMetricsSink) {\n\t\t\tconst dir = process.env.OMK_TURN_METRICS_DIR ?? joinPath(this._cwd, \".omk\", \"metrics\");\n\t\t\tthis._turnMetricsSink = new TurnMetricsSink({ dir });\n\t\t}\n\t\treturn this._turnMetricsSink;\n\t}\n\n\tprivate _recordToolMetric(toolCallId: string, toolName: string, isError: boolean, result: unknown): void {\n\t\tconst state = this._turnMetricsState;\n\t\tif (!state) return;\n\t\tconst startedAt = state.toolStarts.get(toolCallId);\n\t\tstate.toolStarts.delete(toolCallId);\n\t\tstate.toolCalls.push({\n\t\t\tname: toolName,\n\t\t\tdurationMs: startedAt === undefined ? 0 : Date.now() - startedAt,\n\t\t\tok: !isError,\n\t\t\t...(isError ? { error: firstTextContent(result) } : {}),\n\t\t});\n\t}\n\n\t/** Persist one turn. Metrics are advisory: a failure here never affects the turn. */\n\tprivate _recordTurnMetrics(message: AgentMessage): void {\n\t\tconst state = this._turnMetricsState;\n\t\tthis._turnMetricsState = undefined;\n\t\tif (!state) return;\n\t\tconst sink = this._metricsSink();\n\t\tif (!sink) return;\n\t\tconst assistant = message.role === \"assistant\" ? message : undefined;\n\t\tconst usage = assistant?.usage;\n\t\tconst selectedModel = this.model ? `${this.model.provider}/${this.model.id}` : null;\n\t\tconst runtimeProvenance = buildRuntimeProvenance({\n\t\t\trequestedModel: state.requestedModel,\n\t\t\tselectedModel,\n\t\t\tresponseModel: assistant?.responseModel ?? (assistant ? `${assistant.provider}/${assistant.model}` : null),\n\t\t\trequestedThinking: state.requestedThinking,\n\t\t\teffectiveThinking: this.thinkingLevel,\n\t\t\tsource: \"session\",\n\t\t});\n\t\ttry {\n\t\t\tsink.record({\n\t\t\t\tsessionId: this.sessionId,\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\tprovider: this.model?.provider,\n\t\t\t\tmodel: this.model?.id,\n\t\t\t\tstartedAtEpochMs: state.startedAtEpochMs,\n\t\t\t\tendedAtEpochMs: Date.now(),\n\t\t\t\tstopReason: assistant?.stopReason,\n\t\t\t\truntimeProvenance,\n\t\t\t\tusage: usage\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tinput: usage.input,\n\t\t\t\t\t\t\toutput: usage.output,\n\t\t\t\t\t\t\tcacheRead: usage.cacheRead,\n\t\t\t\t\t\t\tcacheWrite: usage.cacheWrite,\n\t\t\t\t\t\t\tcostUsd: usage.cost.total,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\ttoolCalls: state.toolCalls,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Never let observability break a turn.\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to agent events.\n\t * Session persistence is handled internally (saves messages on message_end).\n\t * Multiple listeners can be added. Returns unsubscribe function for this listener.\n\t */\n\tsubscribe(listener: AgentSessionEventListener): () => void {\n\t\tthis._eventListeners.push(listener);\n\n\t\t// Return unsubscribe function for this specific listener\n\t\treturn () => {\n\t\t\tconst index = this._eventListeners.indexOf(listener);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis._eventListeners.splice(index, 1);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Temporarily disconnect from agent events.\n\t * User listeners are preserved and will receive events again after resubscribe().\n\t * Used internally during operations that need to pause event processing.\n\t */\n\tprivate _disconnectFromAgent(): void {\n\t\tif (this._unsubscribeAgent) {\n\t\t\tthis._unsubscribeAgent();\n\t\t\tthis._unsubscribeAgent = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Reconnect to agent events after _disconnectFromAgent().\n\t * Preserves all existing listeners.\n\t */\n\tprivate _reconnectToAgent(): void {\n\t\tif (this._unsubscribeAgent) return; // Already connected\n\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t}\n\n\t/**\n\t * Remove all listeners and disconnect from agent.\n\t * Call this when completely done with the session.\n\t */\n\tdispose(): void {\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tthis.abortRetry();\n\t\t\t\tthis.abortCompaction();\n\t\t\t\tthis.abortBranchSummary();\n\t\t\t\tthis.abortBash();\n\t\t\t\tthis.agent.abort();\n\t\t\t} catch {\n\t\t\t\t// Dispose must succeed even if an abort hook throws.\n\t\t\t}\n\n\t\t\tthis._extensionRunner.invalidate(\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured extension API 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().\",\n\t\t\t);\n\t\t\tthis._disconnectFromAgent();\n\t\t\tthis._eventListeners = [];\n\t\t\tif (this._contextBudgetCacheProvider instanceof DiskContextBudgetCacheProviderV2) {\n\t\t\t\t// Persist whatever the session rendered; a failed flush is never fatal.\n\t\t\t\tthis._contextBudgetCacheProvider.close();\n\t\t\t}\n\t\t\tthis._mcpManager?.close();\n\t\t\tthis._mcpManager = undefined;\n\t\t\tcleanupSessionResources(this.sessionId);\n\t\t} finally {\n\t\t\tconst ownerLease = this._ownedSessionOwnerLease;\n\t\t\tif (ownerLease) {\n\t\t\t\townerLease.release();\n\t\t\t\tif (this.sessionManager.getOwnerLease() === ownerLease) this.sessionManager.setOwnerLease(undefined);\n\t\t\t\tthis._ownedSessionOwnerLease = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Build the context-budget cache provider for this session.\n\t *\n\t * Defaults to a workspace-layer provider persisted under `<cwd>/.omk/cache`,\n\t * so a second session reuses representations the first one already rendered\n\t * (representation keys are content-addressed; see\n\t * `context-budget-v2-cache-keys.ts`). Set\n\t * `OMK_CONTEXT_GOVERNOR_CACHE=memory` to opt out, or\n\t * `OMK_CONTEXT_GOVERNOR_CACHE_DIR` to relocate the snapshot.\n\t */\n\tprivate _createContextBudgetCacheProvider(): ContextBudgetCacheProviderV2 {\n\t\tif (process.env.OMK_CONTEXT_GOVERNOR_CACHE === \"memory\") {\n\t\t\treturn createMemoryContextBudgetCacheProviderV2(\"session\");\n\t\t}\n\t\tconst dir =\n\t\t\tprocess.env.OMK_CONTEXT_GOVERNOR_CACHE_DIR ?? joinPath(this._cwd, \".omk\", \"cache\", \"context-budget-v2\");\n\t\ttry {\n\t\t\treturn createDiskContextBudgetCacheProviderV2({ dir, layer: \"workspace\" });\n\t\t} catch {\n\t\t\t// An unusable cache directory must not take the session down.\n\t\t\treturn createMemoryContextBudgetCacheProviderV2(\"session\");\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Read-only State Access\n\t// =========================================================================\n\n\t/** Full agent state */\n\tget state(): AgentState {\n\t\treturn this.agent.state;\n\t}\n\n\t/** Current model (may be undefined if not yet selected) */\n\tget model(): Model<any> | undefined {\n\t\treturn this.agent.state.model;\n\t}\n\n\t/** Current thinking level */\n\tget thinkingLevel(): ThinkingLevel {\n\t\treturn this.agent.state.thinkingLevel;\n\t}\n\n\t/** Current thinking mode (\"manual\" = user-selected level, \"auto\" = per-turn router) */\n\tget thinkingMode(): ThinkingMode {\n\t\treturn this._thinkingMode;\n\t}\n\n\t/** Whether agent is currently streaming a response */\n\tget isStreaming(): boolean {\n\t\treturn this.agent.state.isStreaming;\n\t}\n\n\t/** Current effective system prompt (includes any per-turn extension modifications) */\n\tget systemPrompt(): string {\n\t\treturn this.agent.state.systemPrompt;\n\t}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget retryAttempt(): number {\n\t\treturn this._retryAttempt;\n\t}\n\n\t/**\n\t * Get the names of currently active tools.\n\t * Returns the names of tools currently set on the agent.\n\t */\n\tgetActiveToolNames(): string[] {\n\t\treturn this.agent.state.tools.map((t) => t.name);\n\t}\n\n\t/**\n\t * Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.\n\t */\n\tgetAllTools(): ToolInfo[] {\n\t\treturn Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({\n\t\t\tname: definition.name,\n\t\t\tdescription: definition.description,\n\t\t\tparameters: definition.parameters,\n\t\t\tpromptGuidelines: definition.promptGuidelines,\n\t\t\tsourceInfo,\n\t\t}));\n\t}\n\n\tgetToolDefinition(name: string): ToolDefinition | undefined {\n\t\treturn this._toolDefinitions.get(name)?.definition;\n\t}\n\n\t/**\n\t * Set active tools by name.\n\t * Only tools in the registry can be enabled. Unknown tool names are ignored.\n\t * Also rebuilds the system prompt to reflect the new tool set.\n\t * Changes take effect on the next agent turn.\n\t */\n\tsetActiveToolsByName(toolNames: string[]): void {\n\t\tconst requestedToolNames = normalizeToolNames(toolNames);\n\t\tconst lockedToolNames = this._loadoutAccessPolicy?.activeTools;\n\t\tif (lockedToolNames && !toolNameSetsEqual(requestedToolNames, lockedToolNames)) {\n\t\t\tthrow new Error(\n\t\t\t\t`loadout active tools are locked: expected ${lockedToolNames.join(\", \") || \"(none)\"}, received ${requestedToolNames.join(\", \") || \"(none)\"}`,\n\t\t\t);\n\t\t}\n\n\t\tconst desiredToolNames = lockedToolNames ? [...lockedToolNames] : toolNames;\n\t\tconst tools: AgentTool[] = [];\n\t\tconst validToolNames: string[] = [];\n\t\tfor (const name of desiredToolNames) {\n\t\t\tconst tool = this._toolRegistry.get(name);\n\t\t\tif (tool) {\n\t\t\t\ttools.push(tool);\n\t\t\t\tvalidToolNames.push(name);\n\t\t\t} else if (lockedToolNames?.includes(name)) {\n\t\t\t\tthrow new Error(`loadout locked tool unavailable: ${name}`);\n\t\t\t}\n\t\t}\n\t\tthis.agent.state.tools = tools;\n\t\tthis._applyToolTimeoutCategoryDefaults(validToolNames);\n\n\t\t// Rebuild base system prompt with new tool set\n\t\tthis._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis._recordPromptCachePlan(\"tool-set\");\n\t}\n\n\t/**\n\t * Recompute the Agent's per-name timeout map for the active tool set:\n\t * explicit user/settings entries always win; active tools without an entry\n\t * receive their §6.3 category default (ALG004-C); uncategorized tools fall\n\t * through to the global `toolTimeoutMs`.\n\t */\n\tprivate _applyToolTimeoutCategoryDefaults(activeToolNames: readonly string[]): void {\n\t\ttry {\n\t\t\tconst resolved = resolveAgentToolSettings(this.settingsManager);\n\t\t\tthis.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);\n\t\t} catch {\n\t\t\t// Invalid settings fail closed at session creation; keep current\n\t\t\t// explicit entries and still fill category defaults for active tools.\n\t\t\tthis.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});\n\t\t}\n\t}\n\n\t/** Whether compaction or branch summarization is currently running */\n\tget isCompacting(): boolean {\n\t\treturn (\n\t\t\tthis._autoCompactionAbortController !== undefined ||\n\t\t\tthis._compactionAbortController !== undefined ||\n\t\t\tthis._branchSummaryAbortController !== undefined\n\t\t);\n\t}\n\n\t/** All messages including custom types like BashExecutionMessage */\n\tget messages(): AgentMessage[] {\n\t\treturn this.agent.state.messages;\n\t}\n\n\t/** Current steering mode */\n\tget steeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.agent.steeringMode;\n\t}\n\n\t/** Current follow-up mode */\n\tget followUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.agent.followUpMode;\n\t}\n\n\t/** Current session file path, or undefined if sessions are disabled */\n\tget sessionFile(): string | undefined {\n\t\treturn this.sessionManager.getSessionFile();\n\t}\n\n\t/** Current session ID */\n\tget sessionId(): string {\n\t\treturn this.sessionManager.getSessionId();\n\t}\n\n\t/** Current session display name, if set */\n\tget sessionName(): string | undefined {\n\t\treturn this.sessionManager.getSessionName();\n\t}\n\n\t/** Scoped models for cycling (from --models flag) */\n\tget scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel?: ThinkingLevel }> {\n\t\treturn this._scopedModels;\n\t}\n\n\t/** Update scoped models for cycling */\n\tsetScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>): void {\n\t\tthis._scopedModels = scopedModels;\n\t}\n\n\t/** File-based prompt templates */\n\tget promptTemplates(): ReadonlyArray<PromptTemplate> {\n\t\treturn this._resourceLoader.getPrompts().prompts;\n\t}\n\n\tprivate _normalizePromptSnippet(text: string | undefined): string | undefined {\n\t\tif (!text) return undefined;\n\t\tconst oneLine = text\n\t\t\t.replace(/[\\r\\n]+/g, \" \")\n\t\t\t.replace(/\\s+/g, \" \")\n\t\t\t.trim();\n\t\treturn oneLine.length > 0 ? oneLine : undefined;\n\t}\n\n\tprivate _normalizePromptGuidelines(guidelines: string[] | undefined): string[] {\n\t\tif (!guidelines || guidelines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst unique = new Set<string>();\n\t\tfor (const guideline of guidelines) {\n\t\t\tconst normalized = guideline.trim();\n\t\t\tif (normalized.length > 0) {\n\t\t\t\tunique.add(normalized);\n\t\t\t}\n\t\t}\n\t\treturn Array.from(unique);\n\t}\n\n\t/**\n\t * Extract the most recent user query text from conversation messages.\n\t * Returns undefined when no user message exists or content is empty.\n\t * Handles both string content and multimodal (array) content.\n\t */\n\tprivate _extractCurrentQuery(): string | undefined {\n\t\tconst messages = this.messages;\n\t\tif (messages.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\t// Scan recent messages (last 3) for the most recent user message\n\t\tconst recent = messages.slice(-3);\n\t\tfor (let i = recent.length - 1; i >= 0; i--) {\n\t\t\tconst msg = recent[i];\n\t\t\tif (!msg || !(\"role\" in msg) || msg.role !== \"user\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst content = (msg as { content: string | (TextContent | ImageContent)[] }).content;\n\t\t\tif (typeof content === \"string\") {\n\t\t\t\tconst trimmed = content.trim();\n\t\t\t\treturn trimmed.length > 0 ? trimmed : undefined;\n\t\t\t}\n\t\t\tif (Array.isArray(content)) {\n\t\t\t\tconst textParts = content.flatMap((item) => (item.type === \"text\" ? [item.text] : []));\n\t\t\t\tconst joined = textParts.join(\"\\n\").trim();\n\t\t\t\treturn joined.length > 0 ? joined : undefined;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate _getContextBudgetOptions(\n\t\tqueryContext = this._extractCurrentQuery(),\n\t\tmodel: Model<any> | undefined = this.model,\n\t): BuildSystemPromptOptions[\"contextBudget\"] | undefined {\n\t\tconst contextGovernorOverride = process.env.OMK_CONTEXT_GOVERNOR;\n\t\tif (contextGovernorOverride === \"0\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (contextGovernorOverride !== \"1\" && !this.settingsManager.getContextBudgetEnabled()) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst contextWindow = model?.contextWindow ?? 0;\n\t\tconst budget = computePromptTokenBudget({\n\t\t\tcontextWindow,\n\t\t\tmodelMaxTokens: model?.maxTokens,\n\t\t\tenvMaxPromptTokens: parsePositiveIntegerEnv(\"OMK_CONTEXT_GOVERNOR_MAX_PROMPT_TOKENS\"),\n\t\t\tenvResponseReserveTokens: parsePositiveIntegerEnv(\"OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS\"),\n\t\t\tenvPromptRatio: parsePositiveFloatEnv(\"OMK_CONTEXT_GOVERNOR_PROMPT_RATIO\"),\n\t\t\tenvResponseRatio: parsePositiveFloatEnv(\"OMK_CONTEXT_GOVERNOR_RESPONSE_RATIO\"),\n\t\t});\n\t\tconst maxPromptTokens = budget.maxPromptTokens;\n\t\tconst responseReserveTokens = budget.responseReserveTokens;\n\n\t\tconst cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();\n\t\tcacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);\n\t\tthis._contextBudgetCacheProvider = cacheProvider;\n\n\t\treturn {\n\t\t\tmaxPromptTokens,\n\t\t\tresponseReserveTokens,\n\t\t\tmodelId: model?.id ?? \"unknown\",\n\t\t\ttokenizerMode: parseTokenizerModeEnv(process.env.OMK_CONTEXT_GOVERNOR_TOKENIZER),\n\t\t\tactiveSkillNames: parseCommaSeparatedEnv(process.env.OMK_CONTEXT_GOVERNOR_ACTIVE_SKILLS),\n\t\t\tqueryContext,\n\t\t\tcacheProvider,\n\t\t};\n\t}\n\n\t/**\n\t * Compute responseReserveTokens from contextWindow.\n\t * Prefers the model's own maxTokens when available, otherwise uses a ratio.\n\t */\n\tprivate _recordPromptCachePlan(reason: string): void {\n\t\tconst boundary = this.agent.state.systemPromptCacheBoundary;\n\t\tconst scope = this.model ? `${this.model.provider}/${this.model.id}` : \"unknown\";\n\t\tconst nextKey = deriveContextPromptCacheKey(\n\t\t\t{\n\t\t\t\tsystemPrompt: this.agent.state.systemPrompt,\n\t\t\t\tsystemPromptCacheBoundary: boundary,\n\t\t\t\tsystemPromptCacheBoundaryBypass: this.agent.state.systemPromptCacheBoundaryBypass,\n\t\t\t\tmessages: [],\n\t\t\t\ttools: this.agent.state.tools,\n\t\t\t},\n\t\t\tscope,\n\t\t);\n\t\tconst transition = classifyPromptCacheTransition(this._promptCacheKey, nextKey);\n\t\tif (transition.kind === \"bypass\") {\n\t\t\tthis._promptCacheBoundaryBypasses += 1;\n\t\t\tthis._promptCacheStablePrefixCharacters = 0;\n\t\t\tif (transition.recordBreak) {\n\t\t\t\tthis._promptCacheLastBreakReason = reason;\n\t\t\t}\n\t\t\tthis._promptCacheKey = undefined;\n\t\t\treturn;\n\t\t}\n\t\tif (transition.kind === \"changed\") {\n\t\t\tthis._promptCacheKeyChanges += 1;\n\t\t\tthis._promptCacheLastBreakReason = reason;\n\t\t}\n\t\tthis._promptCacheKey = nextKey;\n\t\tthis._promptCacheStablePrefixCharacters = boundary ?? 0;\n\t}\n\n\tprivate _getDefaultActiveSkills(): string[] {\n\t\tconst trustedSkillNames = new Set(\n\t\t\tthis._resourceLoader\n\t\t\t\t.getSkills()\n\t\t\t\t.skills.filter((skill) => skill.sourceInfo.scope === \"user\")\n\t\t\t\t.map((skill) => skill.name),\n\t\t);\n\t\treturn this.settingsManager.getDefaultActiveSkills().filter((name) => trustedSkillNames.has(name));\n\t}\n\n\tprivate _rebuildSystemPrompt(toolNames: string[], model: Model<any> | undefined = this.model): string {\n\t\tconst defaultActiveSkills = this._getDefaultActiveSkills();\n\t\tconst assembled = assembleSessionSystemPrompt({\n\t\t\tcwd: this._cwd,\n\t\t\ttoolNames,\n\t\t\thasTool: (name) => this._toolRegistry.has(name),\n\t\t\ttoolPromptSnippets: this._toolPromptSnippets,\n\t\t\ttoolPromptGuidelines: this._toolPromptGuidelines,\n\t\t\tcustomPrompt: this._resourceLoader.getSystemPrompt(),\n\t\t\tappendSystemPrompt: this._resourceLoader.getAppendSystemPrompt(),\n\t\t\tproviderAppend: grokPlaybookAppendForProvider(model?.provider),\n\t\t\tskills: this._resourceLoader.getSkills().skills,\n\t\t\tactiveSkillNames: defaultActiveSkills,\n\t\t\tactiveSkillSource: defaultActiveSkills.length > 0 ? \"settings\" : undefined,\n\t\t\tcontextFiles: selectContextFilesForModel(this._resourceLoader.getAgentsFiles().agentsFiles, model),\n\t\t\tcontextBudget: this._getContextBudgetOptions(undefined, model),\n\t\t});\n\t\tthis._baseSystemPromptOptions = assembled.options;\n\t\tthis._baseSystemPromptCacheBoundary = assembled.cacheBoundary;\n\t\treturn assembled.prompt;\n\t}\n\n\t// =========================================================================\n\t// Prompting\n\t// =========================================================================\n\n\tprivate async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {\n\t\t// One identity per top-level run (§16.2): internal retries and\n\t\t// continuations inside this call share it, as does the resource lease.\n\t\tconst promptRunId = `prompt-run-${randomUUID()}`;\n\t\tconst startedAtEpochMs = Date.now();\n\t\t// Roadmap M2: the lease spans the whole run including internal retries\n\t\t// and continuations, so they share one admission decision (§8.3).\n\t\tconst resourceLease = await this._beginResourceGovernedRun(promptRunId);\n\t\tconst resourceObservations = this._resourceObservationJournals.get(promptRunId) ?? null;\n\t\tlet outcome: PromptSettlementOutcome = \"completed\";\n\t\ttry {\n\t\t\tawait this.agent.prompt(messages);\n\t\t\twhile (await this._handlePostAgentRun()) {\n\t\t\t\tawait this.agent.continue();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\toutcome =\n\t\t\t\tthis._userAbortRequested || (error instanceof Error && /abort/i.test(error.message)) ? \"aborted\" : \"failed\";\n\t\t\tthis._publishRuntimeFailure(error);\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tif (resourceLease !== null) {\n\t\t\t\t// Generation-safe: a stale release after another run acquired the\n\t\t\t\t// lease is a no-op instead of clobbering the newer cap (§8.1).\n\t\t\t\tthis._resourceLeaseController?.release(resourceLease);\n\t\t\t\tresourceObservations?.record(\"resource_lease_released_v1\", { promptRunId });\n\t\t\t}\n\t\t\tthis._flushPendingBashMessages();\n\t\t\t// §16.4 (M4): settle only after every in-run continuation drained.\n\t\t\tthis._emitPromptSettledIfReady({ promptRunId, startedAtEpochMs, outcome, resourceObservations });\n\t\t}\n\t}\n\n\t/**\n\t * Emit `prompt_settled` when the §16.4 conditions hold. Called exactly\n\t * once per promptRunId (from `_runAgentPrompt`'s finally), so the same\n\t * run can never emit twice; late-queued messages block emission\n\t * conservatively — the continuation run settles under its own id.\n\t * Settlement is a UX signal only and must never affect the run (§16.1).\n\t */\n\tprivate _emitPromptSettledIfReady(input: {\n\t\treadonly promptRunId: string;\n\t\treadonly startedAtEpochMs: number;\n\t\treadonly outcome: PromptSettlementOutcome;\n\t\treadonly resourceObservations: ResourceObservationJournal | null;\n\t}): void {\n\t\ttry {\n\t\t\tlet state = createPromptSettlementState(input.promptRunId, input.startedAtEpochMs);\n\t\t\tstate = reducePromptSettlement(state, { kind: \"terminal\", outcome: input.outcome });\n\t\t\tif (this.isStreaming) {\n\t\t\t\tstate = reducePromptSettlement(state, { kind: \"tool\", delta: 1 });\n\t\t\t}\n\t\t\tif (this.agent.hasQueuedMessages()) {\n\t\t\t\tstate = reducePromptSettlement(state, { kind: \"continuation\", delta: 1 });\n\t\t\t}\n\t\t\tconst settled = settlePromptIfReady(state, Date.now());\n\t\t\tif (settled.event !== null) {\n\t\t\t\tinput.resourceObservations?.record(\"prompt_settled_v1\", settledObservationFacts(settled.event));\n\t\t\t\tthis._emit(settled.event);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Never let settlement bookkeeping break a run.\n\t\t}\n\t}\n\n\t/**\n\t * Resource governor preflight for one top-level run (roadmap §8, §25.3 M2).\n\t *\n\t * - `off`: nothing; v0.96.1 observable behavior is preserved.\n\t * - `observe` (default): fire-and-forget probe records a decision for the\n\t * §19.4 read-only API; the cap never changes and the prompt is never\n\t * delayed. The pending probe is bounded (~cpuSampleMs) and never rejects.\n\t * - `adaptive`/`strict`: bounded blocking probe (default 300 ms deadline),\n\t * then a generation-safe lease applies the effective tool cap for this\n\t * run. The caller's `finally` releases it exactly once.\n\t *\n\t * Never throws: any probe or policy failure leaves this run ungoverned\n\t * (§2.1: probe failure must not crash or block the prompt).\n\t */\n\tprivate async _beginResourceGovernedRun(promptRunId: string): Promise<RunResourceLease | null> {\n\t\ttry {\n\t\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\t\tif (resolved.mode === \"off\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst resourceObservations = this._openResourceObservations(promptRunId);\n\t\t\tconst probeOptions = {\n\t\t\t\tcwd: this._cwd,\n\t\t\t\tmaxProbeMs: resolved.maxProbeMs,\n\t\t\t\tcpuSampleMs: resolved.cpuSampleMs,\n\t\t\t};\n\t\t\t// §7.3: Agent.maxToolConcurrency uses undefined for \"unlimited\"; the\n\t\t\t// admission contract uses 0 for the same meaning.\n\t\t\tconst configuredCaps = { maxToolConcurrency: this.agent.maxToolConcurrency ?? 0 };\n\t\t\tif (resolved.mode === \"observe\") {\n\t\t\t\tvoid captureHostResourceSnapshot(probeOptions)\n\t\t\t\t\t.then((snapshot) => {\n\t\t\t\t\t\tconst decision = decideResourceAdmission({\n\t\t\t\t\t\t\tsnapshot,\n\t\t\t\t\t\t\tconfig: resolved.admission,\n\t\t\t\t\t\t\tconfiguredCaps,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\t\t\t\t\tthis._lastResourceAdmission = decision;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresourceObservations?.record(\"resource_snapshot_v1\", snapshotObservationFacts(snapshot));\n\t\t\t\t\t\tresourceObservations?.record(\"resource_admission_v1\", admissionObservationFacts(decision));\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => {});\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst snapshot = await captureHostResourceSnapshot(probeOptions);\n\t\t\tconst decision = decideResourceAdmission({ snapshot, config: resolved.admission, configuredCaps });\n\t\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\t\tthis._lastResourceAdmission = decision;\n\t\t\t}\n\t\t\tresourceObservations?.record(\"resource_snapshot_v1\", snapshotObservationFacts(snapshot));\n\t\t\tresourceObservations?.record(\"resource_admission_v1\", admissionObservationFacts(decision));\n\t\t\tthis._resourceLeaseController ??= new RunResourceLeaseController({\n\t\t\t\tgetCap: () => this.agent.maxToolConcurrency,\n\t\t\t\tsetCap: (cap) => {\n\t\t\t\t\tthis.agent.maxToolConcurrency = cap;\n\t\t\t\t},\n\t\t\t});\n\t\t\tconst lease = this._resourceLeaseController.acquire({ promptRunId, decision });\n\t\t\tresourceObservations?.record(\"resource_lease_acquired_v1\", {\n\t\t\t\tdecisionId: decision.decisionId,\n\t\t\t\tappliedToolCap: decision.maxToolConcurrency,\n\t\t\t});\n\t\t\treturn lease;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate _openResourceObservations(promptRunId: string): ResourceObservationJournal | null {\n\t\tthis._latestResourcePromptRunId = promptRunId;\n\t\ttry {\n\t\t\tconst journal = ResourceObservationJournal.open(this._cwd, promptRunId);\n\t\t\tthis._resourceObservations = journal;\n\t\t\tthis._resourceObservationJournals.set(promptRunId, journal);\n\t\t\twhile (this._resourceObservationJournals.size > MAX_RESOURCE_OBSERVATION_JOURNALS) {\n\t\t\t\tconst oldestPromptRunId = this._resourceObservationJournals.keys().next().value;\n\t\t\t\tif (oldestPromptRunId === undefined) break;\n\t\t\t\tthis._resourceObservationJournals.delete(oldestPromptRunId);\n\t\t\t}\n\t\t\treturn journal;\n\t\t} catch {\n\t\t\tthis._resourceObservations = null;\n\t\t\tthis._resourceObservationJournals.delete(promptRunId);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/** §20.2: record the TUI-side completion sound result in its originating run journal. */\n\trecordCompletionSoundResult(\n\t\tpromptRunId: string,\n\t\tresult: import(\"./completion-sound.ts\").CompletionSoundResult,\n\t): void {\n\t\tconst journal = this._resourceObservationJournals.get(promptRunId);\n\t\tjournal?.record(\"completion_sound_result_v1\", soundObservationFacts(result));\n\t\tthis._resourceObservationJournals.delete(promptRunId);\n\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\tthis._latestResourcePromptRunId = null;\n\t\t\tif (this._resourceObservations === journal) this._resourceObservations = null;\n\t\t}\n\t}\n\n\t/** §19.4 read-only SDK surface: the most recent resource admission decision, if any. */\n\tgetCurrentResourceAdmission(): ResourceAdmissionDecision | null {\n\t\treturn this._lastResourceAdmission;\n\t}\n\n\t/** §19.4 read-only SDK surface: capture a fresh host resource snapshot. */\n\tasync getHostResourceSnapshot(): Promise<HostResourceSnapshot> {\n\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\treturn captureHostResourceSnapshot({\n\t\t\tcwd: this._cwd,\n\t\t\tmaxProbeMs: resolved.maxProbeMs,\n\t\t\tcpuSampleMs: resolved.cpuSampleMs,\n\t\t});\n\t}\n\n\t/** §19.4 read-only SDK surface: shared workload permit pool state, if any. */\n\tgetWorkloadPermitSnapshot(): WorkloadPermitPoolSnapshot | null {\n\t\treturn this._workloadPermitPool?.snapshot() ?? null;\n\t}\n\n\t/**\n\t * §14.1 shared-budget seam: child subagents must reuse this instance\n\t * instead of creating a private pool (host oversubscription). The live\n\t * child launcher wiring lands in M6.\n\t */\n\tget workloadPermitPool(): WorkloadPermitPool {\n\t\tthis._workloadPermitPool ??= new WorkloadPermitPool();\n\t\treturn this._workloadPermitPool;\n\t}\n\n\t/**\n\t * Resource safety gate for the bash boundary (roadmap §9.4/§11, M3).\n\t * Runs only in adaptive/strict mode with a recorded admission decision;\n\t * observe/off keep v0.96.1 behavior. Never throws: gate machinery\n\t * failures leave the command ungated (§2.1), while computed block\n\t * verdicts and permit rejections return a bounded §11.3 payload.\n\t */\n\tprivate async _acquireBashResourcePermit(command: string, signal: AbortSignal): Promise<BashResourcePermitGrant> {\n\t\ttry {\n\t\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\t\tif (resolved.mode !== \"adaptive\" && resolved.mode !== \"strict\") {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst decision = this._lastResourceAdmission;\n\t\t\tif (decision === null) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst classification = classifyWorkloadCommand(command);\n\t\t\tthis._resourceObservations?.record(\n\t\t\t\t\"workload_classification_v1\",\n\t\t\t\tclassificationObservationFacts(classification),\n\t\t\t);\n\t\t\tconst verdict = decideResourceSafetyGate({ commandSafety: \"allowed\", classification, decision });\n\t\t\tif (verdict.kind === \"allow\") {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tif (verdict.kind === \"block\") {\n\t\t\t\treturn { blocked: JSON.stringify(verdict.block) };\n\t\t\t}\n\t\t\tthis._workloadPermitPool ??= new WorkloadPermitPool({ capacity: decision.maxHeavyProcesses });\n\t\t\tconst pool = this._workloadPermitPool;\n\t\t\tpool.setCapacity(decision.maxHeavyProcesses);\n\t\t\tconst waitStartedMs = Date.now();\n\t\t\tthis._resourceObservations?.record(\"workload_permit_wait_v1\", {\n\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\tweight: verdict.weight,\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst permit = await pool.acquire({\n\t\t\t\t\trequestId: `bash-${randomUUID()}`,\n\t\t\t\t\tpromptRunId: this._resourceLeaseController?.activeLease?.promptRunId ?? \"unleased\",\n\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\tsignal,\n\t\t\t\t\t// Bounded §10.3 timeout-aware wait; a settings knob can follow later.\n\t\t\t\t\ttimeoutMs: 60_000,\n\t\t\t\t});\n\t\t\t\tthis._resourceObservations?.record(\"workload_permit_acquired_v1\", {\n\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\twaitMs: Date.now() - waitStartedMs,\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\trelease: () => {\n\t\t\t\t\t\tpermit.release();\n\t\t\t\t\t\tthis._resourceObservations?.record(\"workload_permit_released_v1\", {\n\t\t\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconst code = error instanceof WorkloadPermitError ? error.code : \"queue_overflow\";\n\t\t\t\treturn {\n\t\t\t\t\tblocked: JSON.stringify({\n\t\t\t\t\t\tkind: \"resource_pressure\",\n\t\t\t\t\t\tpressure: decision.pressure,\n\t\t\t\t\t\taction: \"defer-heavy\",\n\t\t\t\t\t\treasonCodes: [...decision.reasons, `resource.permit.${code}`],\n\t\t\t\t\t\trequiredAction: RESOURCE_PRESSURE_REQUIRED_ACTION,\n\t\t\t\t\t}),\n\t\t\t\t};\n\t\t\t}\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async _handlePostAgentRun(): Promise<boolean> {\n\t\tconst msg = this._lastAssistantMessage;\n\t\tthis._lastAssistantMessage = undefined;\n\t\tif (!msg) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (msg.stopReason === \"error\" && this._retryAttempt > 0) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\tfinalError: msg.errorMessage,\n\t\t\t});\n\t\t\tthis._retryAttempt = 0;\n\t\t\tthis._refusedModels.clear();\n\t\t}\n\n\t\tif (await this._checkCompaction(msg)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// The agent loop drains both queues before emitting agent_end. Any messages\n\t\t// here were queued by agent_end extension handlers and need a continuation.\n\t\treturn this.agent.hasQueuedMessages();\n\t}\n\n\t/**\n\t * Send a prompt to the agent.\n\t * - Handles extension commands (registered via omk.registerCommand) immediately, even during streaming\n\t * - Expands file-based prompt templates by default\n\t * - During streaming, queues via steer() or followUp() based on streamingBehavior option\n\t * - Validates model and API key before sending (when not streaming)\n\t * @throws Error if streaming and no streamingBehavior specified\n\t * @throws Error if no model selected or no API key available (when not streaming)\n\t */\n\tasync prompt(text: string, options?: PromptOptions): Promise<void> {\n\t\tconst expandPromptTemplates = options?.expandPromptTemplates ?? true;\n\t\tconst preflightResult = options?.preflightResult;\n\t\tlet currentText = redactSensitiveText(text);\n\t\tconst defaultActiveSkills = this._getDefaultActiveSkills();\n\t\tlet promptActiveSkillNames = mergePromptActiveSkillNames(defaultActiveSkills, options?.activeSkillNames ?? []);\n\t\tlet promptActiveSkillSource =\n\t\t\tdefaultActiveSkills.length > 0\n\t\t\t\t? options?.activeSkillSource\n\t\t\t\t\t? `settings+${options.activeSkillSource}`\n\t\t\t\t\t: \"settings\"\n\t\t\t\t: options?.activeSkillSource;\n\t\tlet isBangSkillInvocation = false;\n\t\tif (expandPromptTemplates) {\n\t\t\tconst bangInvocation = parseBangInvocation(text, {\n\t\t\t\thasSkill: (name) => this._resourceLoader.getSkills().skills.some((skill) => skill.name === name),\n\t\t\t});\n\t\t\tif (bangInvocation.kind === \"skill\") {\n\t\t\t\tisBangSkillInvocation = true;\n\t\t\t\tcurrentText = bangInvocation.prompt\n\t\t\t\t\t? `/skill:${bangInvocation.skillName} ${bangInvocation.prompt}`\n\t\t\t\t\t: `/skill:${bangInvocation.skillName}`;\n\t\t\t\tpromptActiveSkillNames = mergePromptActiveSkillNames(\n\t\t\t\t\tpromptActiveSkillNames,\n\t\t\t\t\tbangInvocation.activeSkillNames,\n\t\t\t\t);\n\t\t\t\tpromptActiveSkillSource =\n\t\t\t\t\tdefaultActiveSkills.length > 0 ? `settings+${bangInvocation.source}` : bangInvocation.source;\n\t\t\t}\n\t\t}\n\t\tlet messages: AgentMessage[] | undefined;\n\n\t\ttry {\n\t\t\t// Handle extension commands first (execute immediately, even during streaming)\n\t\t\t// Extension commands manage their own LLM interaction via omk.sendMessage()\n\t\t\tif (expandPromptTemplates && !isBangSkillInvocation && currentText.startsWith(\"/\")) {\n\t\t\t\tconst handled = await this._tryExecuteExtensionCommand(currentText);\n\t\t\t\tif (handled) {\n\t\t\t\t\t// Extension command executed, no prompt to send\n\t\t\t\t\tpreflightResult?.(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Emit input event for extension interception (before skill/template expansion)\n\t\t\tlet currentImages = options?.images;\n\t\t\tif (this._extensionRunner.hasHandlers(\"input\")) {\n\t\t\t\tconst inputResult = await this._extensionRunner.emitInput(\n\t\t\t\t\tcurrentText,\n\t\t\t\t\tcurrentImages,\n\t\t\t\t\toptions?.source ?? \"interactive\",\n\t\t\t\t\tthis.isStreaming ? options?.streamingBehavior : undefined,\n\t\t\t\t);\n\t\t\t\tif (inputResult.action === \"handled\") {\n\t\t\t\t\tpreflightResult?.(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (inputResult.action === \"transform\") {\n\t\t\t\t\tcurrentText = inputResult.text;\n\t\t\t\t\tcurrentImages = inputResult.images ?? currentImages;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Expand skill commands (/skill:name args) and prompt templates (/template args)\n\t\t\tlet expandedText = currentText;\n\t\t\tif (expandPromptTemplates) {\n\t\t\t\texpandedText = this._expandSkillCommand(expandedText);\n\t\t\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\t\t\t}\n\t\t\texpandedText = redactSensitiveText(expandedText);\n\n\t\t\t// If streaming, queue via steer() or followUp() based on option\n\t\t\tif (this.isStreaming) {\n\t\t\t\tif (!options?.streamingBehavior) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (options.streamingBehavior === \"followUp\") {\n\t\t\t\t\tawait this._queueFollowUp(expandedText, currentImages);\n\t\t\t\t} else {\n\t\t\t\t\tawait this._queueSteer(expandedText, currentImages);\n\t\t\t\t}\n\t\t\t\tpreflightResult?.(true);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Flush any pending bash messages before the new prompt\n\t\t\tthis._flushPendingBashMessages();\n\n\t\t\t// Validate model\n\t\t\tif (!this.model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\t// Root-level: eject sticky safety models (Fable) before the turn leaves the machine.\n\t\t\tawait this._ejectStickySafetyModelIfNeeded();\n\n\t\t\t// Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).\n\t\t\tassertTextChatModelForCompletion(this.model.id, this.model.provider);\n\n\t\t\tif (!this._modelRegistry.hasConfiguredAuth(this.model)) {\n\t\t\t\tconst isOAuth = this._modelRegistry.isUsingOAuth(this.model);\n\t\t\t\tif (isOAuth) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Authentication failed for \"${this.model.provider}\". ` +\n\t\t\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t\t\t`Run '/login ${this.model.provider}' to re-authenticate.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow new Error(formatNoApiKeyFoundMessage(this.model.provider));\n\t\t\t}\n\n\t\t\t// Check if we need to compact before sending (catches aborted responses)\n\t\t\tconst lastAssistant = this._findLastAssistantMessage();\n\t\t\tif (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agent.continue();\n\t\t\t\t\twhile (await this._handlePostAgentRun()) {\n\t\t\t\t\t\tawait this.agent.continue();\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tthis._flushPendingBashMessages();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Auto thinking mode: resolve this turn's level from the prompt content.\n\t\t\t// Manual mode never enters the router, so /think <level> always wins.\n\t\t\tthis._applyAutoThinkingLevelForTurn(expandedText);\n\n\t\t\t// Build messages array (custom message if any, then user message)\n\t\t\tmessages = [];\n\n\t\t\t// Add user message\n\t\t\tconst userContent: (TextContent | ImageContent)[] = [{ type: \"text\", text: expandedText }];\n\t\t\tif (currentImages) {\n\t\t\t\tuserContent.push(...currentImages);\n\t\t\t}\n\t\t\tmessages.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: userContent,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\n\t\t\t// Inject any pending \"nextTurn\" messages as context alongside the user message\n\t\t\tfor (const msg of this._pendingNextTurnMessages) {\n\t\t\t\tmessages.push(msg);\n\t\t\t}\n\t\t\tthis._pendingNextTurnMessages = [];\n\n\t\t\tconst turnSystemPromptOptions = {\n\t\t\t\t...this._baseSystemPromptOptions,\n\t\t\t\tcontextBudget: this._getContextBudgetOptions(expandedText),\n\t\t\t\tactiveSkillNames: promptActiveSkillNames,\n\t\t\t\tactiveSkillSource: promptActiveSkillSource,\n\t\t\t};\n\t\t\tconst turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);\n\n\t\t\t// Emit before_agent_start extension event\n\t\t\tconst result = await this._extensionRunner.emitBeforeAgentStart(\n\t\t\t\texpandedText,\n\t\t\t\tcurrentImages,\n\t\t\t\tturnSystemPrompt.prompt,\n\t\t\t\tturnSystemPromptOptions,\n\t\t\t);\n\t\t\t// Add all custom messages from extensions\n\t\t\tif (result?.messages) {\n\t\t\t\tfor (const msg of result.messages) {\n\t\t\t\t\tmessages.push({\n\t\t\t\t\t\trole: \"custom\",\n\t\t\t\t\t\tcustomType: msg.customType,\n\t\t\t\t\t\tcontent: msg.content,\n\t\t\t\t\t\tdisplay: msg.display,\n\t\t\t\t\t\tdetails: msg.details,\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Apply extension-modified system prompt, or reset to the turn plan.\n\t\t\tif (result?.systemPrompt) {\n\t\t\t\tconst preservesCacheBoundary = result.systemPrompt === turnSystemPrompt.prompt;\n\t\t\t\tthis.agent.state.systemPrompt = result.systemPrompt;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundary = preservesCacheBoundary\n\t\t\t\t\t? turnSystemPrompt.cacheBoundary\n\t\t\t\t\t: undefined;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundaryBypass = !preservesCacheBoundary;\n\t\t\t} else {\n\t\t\t\t// Ensure we're using the turn prompt (in case the previous turn had modifications).\n\t\t\t\tthis.agent.state.systemPrompt = turnSystemPrompt.prompt;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundary = turnSystemPrompt.cacheBoundary;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\t\t}\n\t\t\tthis._recordPromptCachePlan(result?.systemPrompt ? \"extension-override\" : \"turn-plan\");\n\n\t\t\tawait this._checkProjectedCompaction(messages);\n\t\t} catch (error) {\n\t\t\tpreflightResult?.(false);\n\t\t\tconst rawMessage = error instanceof Error ? error.message : String(error);\n\t\t\tconst cause = this._classifyPreflightCause(rawMessage);\n\t\t\tconst timestamp = new Date().toISOString();\n\t\t\tthis._publishTermination(\n\t\t\t\tclassifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId: `preflight-${randomUUID()}`,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: this._terminationMessage(rawMessage, \"Prompt preflight failed.\"),\n\t\t\t\t\tcause,\n\t\t\t\t\tsideEffects: \"none\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\n\t\tif (!messages) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreflightResult?.(true);\n\t\tawait this._runAgentPrompt(messages);\n\t}\n\n\t/**\n\t * Try to execute an extension command. Returns true if command was found and executed.\n\t */\n\tprivate async _tryExecuteExtensionCommand(text: string): Promise<boolean> {\n\t\t// Parse command name and args\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\t\tconst command = this._extensionRunner.getCommand(commandName);\n\t\tif (!command) return false;\n\n\t\t// Get command context from extension runner (includes session control methods)\n\t\tconst ctx = this._extensionRunner.createCommandContext();\n\n\t\ttry {\n\t\t\tawait command.handler(args, ctx);\n\t\t\treturn true;\n\t\t} catch (err) {\n\t\t\t// Emit error via extension runner\n\t\t\tthis._extensionRunner.emitError({\n\t\t\t\textensionPath: `command:${commandName}`,\n\t\t\t\tevent: \"command\",\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t/**\n\t * Expand skill commands (/skill:name args) to their full content.\n\t * Returns the expanded text, or the original text if not a skill command or skill not found.\n\t * Emits errors via extension runner if file read fails.\n\t */\n\tprivate _expandSkillCommand(text: string): string {\n\t\tif (!text.startsWith(\"/skill:\")) return text;\n\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);\n\t\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1).trim();\n\n\t\tconst skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName);\n\t\tif (!skill) return text; // Unknown skill, pass through\n\n\t\ttry {\n\t\t\tconst content = readFileSync(skill.filePath, \"utf-8\");\n\t\t\tconst body = stripFrontmatter(content).trim();\n\t\t\tconst skillBlock = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n\t\t\treturn args ? `${skillBlock}\\n\\n${args}` : skillBlock;\n\t\t} catch (err) {\n\t\t\t// Emit error like extension commands do\n\t\t\tthis._extensionRunner.emitError({\n\t\t\t\textensionPath: skill.filePath,\n\t\t\t\tevent: \"skill_expansion\",\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\treturn text; // Return original on error\n\t\t}\n\t}\n\n\t/**\n\t * Queue a steering message while the agent is running.\n\t * Delivered after the current assistant turn finishes executing its tool calls,\n\t * before the next LLM call.\n\t * Expands skill commands and prompt templates. Errors on extension commands.\n\t * @param images Optional image attachments to include with the message\n\t * @throws Error if text is an extension command\n\t */\n\tasync steer(text: string, images?: ImageContent[]): Promise<void> {\n\t\tconst sanitizedText = redactSensitiveText(text);\n\n\t\t// Check for extension commands (cannot be queued)\n\t\tif (sanitizedText.startsWith(\"/\")) {\n\t\t\tthis._throwIfExtensionCommand(sanitizedText);\n\t\t}\n\n\t\t// Expand skill commands and prompt templates\n\t\tlet expandedText = this._expandSkillCommand(sanitizedText);\n\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\n\t\tawait this._queueSteer(redactSensitiveText(expandedText), images);\n\t}\n\n\t/**\n\t * Queue a follow-up message to be processed after the agent finishes.\n\t * Delivered only when agent has no more tool calls or steering messages.\n\t * Expands skill commands and prompt templates. Errors on extension commands.\n\t * @param images Optional image attachments to include with the message\n\t * @throws Error if text is an extension command\n\t */\n\tasync followUp(text: string, images?: ImageContent[]): Promise<void> {\n\t\tconst sanitizedText = redactSensitiveText(text);\n\n\t\t// Check for extension commands (cannot be queued)\n\t\tif (sanitizedText.startsWith(\"/\")) {\n\t\t\tthis._throwIfExtensionCommand(sanitizedText);\n\t\t}\n\n\t\t// Expand skill commands and prompt templates\n\t\tlet expandedText = this._expandSkillCommand(sanitizedText);\n\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\n\t\tawait this._queueFollowUp(redactSensitiveText(expandedText), images);\n\t}\n\n\t/**\n\t * Internal: Queue a steering message (already expanded, no extension command check).\n\t */\n\tprivate async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {\n\t\tthis._invalidateContextBudgetCache({ type: \"userSteering\" });\n\t\tthis._steeringMessages.push(text);\n\t\tthis._emitQueueUpdate();\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (images) {\n\t\t\tcontent.push(...images);\n\t\t}\n\t\tthis.agent.steer({\n\t\t\trole: \"user\",\n\t\t\tcontent,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t/**\n\t * Internal: Queue a follow-up message (already expanded, no extension command check).\n\t */\n\tprivate async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {\n\t\tthis._followUpMessages.push(text);\n\t\tthis._emitQueueUpdate();\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (images) {\n\t\t\tcontent.push(...images);\n\t\t}\n\t\tthis.agent.followUp({\n\t\t\trole: \"user\",\n\t\t\tcontent,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t/**\n\t * Throw an error if the text is an extension command.\n\t */\n\tprivate _throwIfExtensionCommand(text: string): void {\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\tconst command = this._extensionRunner.getCommand(commandName);\n\n\t\tif (command) {\n\t\t\tthrow new Error(\n\t\t\t\t`Extension command \"/${commandName}\" cannot be queued. Use prompt() or execute the command when not streaming.`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Send a custom message to the session. Creates a CustomMessageEntry.\n\t *\n\t * Handles three cases:\n\t * - Streaming: queues message, processed when loop pulls from queue\n\t * - Not streaming + triggerTurn: appends to state/session, starts new turn\n\t * - Not streaming + no trigger: appends to state/session, no turn\n\t *\n\t * @param message Custom message with customType, content, display, details\n\t * @param options.triggerTurn If true and not streaming, triggers a new LLM turn\n\t * @param options.deliverAs Delivery mode: \"steer\", \"followUp\", or \"nextTurn\"\n\t */\n\tasync sendCustomMessage<T = unknown>(\n\t\tmessage: Pick<CustomMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" | \"nextTurn\" },\n\t): Promise<void> {\n\t\tconst appMessage = {\n\t\t\trole: \"custom\" as const,\n\t\t\tcustomType: message.customType,\n\t\t\tcontent: message.content,\n\t\t\tdisplay: message.display,\n\t\t\tdetails: message.details,\n\t\t\ttimestamp: Date.now(),\n\t\t} satisfies CustomMessage<T>;\n\t\tthis._recordEvidenceReceiptInvalidation(message.customType);\n\t\tif (options?.deliverAs === \"nextTurn\") {\n\t\t\tthis._pendingNextTurnMessages.push(appMessage);\n\t\t} else if (this.isStreaming) {\n\t\t\tif (options?.deliverAs === \"followUp\") {\n\t\t\t\tthis.agent.followUp(appMessage);\n\t\t\t} else {\n\t\t\t\tthis.agent.steer(appMessage);\n\t\t\t}\n\t\t} else if (options?.triggerTurn) {\n\t\t\tawait this._runAgentPrompt(appMessage);\n\t\t} else {\n\t\t\tthis.agent.state.messages.push(appMessage);\n\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\tmessage.customType,\n\t\t\t\tmessage.content,\n\t\t\t\tmessage.display,\n\t\t\t\tmessage.details,\n\t\t\t);\n\t\t\tthis._emit({ type: \"message_start\", message: appMessage });\n\t\t\tthis._emit({ type: \"message_end\", message: appMessage });\n\t\t}\n\t}\n\n\t/**\n\t * Send a user message to the agent. Always triggers a turn.\n\t * When the agent is streaming, use deliverAs to specify how to queue the message.\n\t *\n\t * @param content User message content (string or content array)\n\t * @param options.deliverAs Delivery mode when streaming: \"steer\" or \"followUp\"\n\t */\n\tasync sendUserMessage(\n\t\tcontent: string | (TextContent | ImageContent)[],\n\t\toptions?: { deliverAs?: \"steer\" | \"followUp\" },\n\t): Promise<void> {\n\t\t// Normalize content to text string + optional images\n\t\tlet text: string;\n\t\tlet images: ImageContent[] | undefined;\n\n\t\tif (typeof content === \"string\") {\n\t\t\ttext = content;\n\t\t} else {\n\t\t\tconst textParts: string[] = [];\n\t\t\timages = [];\n\t\t\tfor (const part of content) {\n\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t} else {\n\t\t\t\t\timages.push(part);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = textParts.join(\"\\n\");\n\t\t\tif (images.length === 0) images = undefined;\n\t\t}\n\n\t\t// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion\n\t\tawait this.prompt(text, {\n\t\t\texpandPromptTemplates: false,\n\t\t\tstreamingBehavior: options?.deliverAs,\n\t\t\timages,\n\t\t\tsource: \"extension\",\n\t\t});\n\t}\n\n\t/**\n\t * Clear all queued messages and return them.\n\t * Useful for restoring to editor when user aborts.\n\t * @returns Object with steering and followUp arrays\n\t */\n\tclearQueue(): { steering: string[]; followUp: string[] } {\n\t\tconst steering = [...this._steeringMessages];\n\t\tconst followUp = [...this._followUpMessages];\n\t\tthis._steeringMessages = [];\n\t\tthis._followUpMessages = [];\n\t\tthis.agent.clearAllQueues();\n\t\tthis._emitQueueUpdate();\n\t\treturn { steering, followUp };\n\t}\n\n\t/** Number of pending messages (includes both steering and follow-up) */\n\tget pendingMessageCount(): number {\n\t\treturn this._steeringMessages.length + this._followUpMessages.length;\n\t}\n\n\t/** Get pending steering messages (read-only) */\n\tgetSteeringMessages(): readonly string[] {\n\t\treturn this._steeringMessages;\n\t}\n\n\t/** Get pending follow-up messages (read-only) */\n\tgetFollowUpMessages(): readonly string[] {\n\t\treturn this._followUpMessages;\n\t}\n\n\tget resourceLoader(): ResourceLoader {\n\t\treturn this._resourceLoader;\n\t}\n\n\t/**\n\t * Abort current operation and wait for agent to become idle.\n\t */\n\tasync abort(): Promise<void> {\n\t\tthis._userAbortRequested = this.isStreaming || this.isRetrying;\n\t\tthis.abortRetry();\n\t\tthis.agent.abort();\n\t\tawait this.agent.waitForIdle();\n\t}\n\n\t/** Record an observed process signal before a mode begins shutdown. */\n\trecordProcessSignal(signal: SessionProcessSignal): SessionTermination {\n\t\tconst timestamp = new Date().toISOString();\n\t\tconst runId = this._activeRunId ?? `signal-${randomUUID()}`;\n\t\tconst termination = classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage: `Process received ${signal}.`,\n\t\t\tcause: { area: \"process\", code: \"signal\", signal },\n\t\t\tsideEffects: this._activeRunId === null ? \"none\" : \"possible\",\n\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t});\n\t\tif (this._activeRunId !== null) {\n\t\t\tthis._runJournalStore.finish({\n\t\t\t\ttermination,\n\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tthis._activeRunId = null;\n\t\t}\n\t\tthis._publishTermination(termination);\n\t\treturn termination;\n\t}\n\n\t// =========================================================================\n\t// Model Management\n\t// =========================================================================\n\n\tprivate async _emitModelSelect(\n\t\tnextModel: Model<any>,\n\t\tpreviousModel: Model<any> | undefined,\n\t\tsource: \"set\" | \"cycle\" | \"restore\",\n\t): Promise<void> {\n\t\tif (modelsAreEqual(previousModel, nextModel)) return;\n\t\tthis._invalidateContextBudgetCache({ type: \"activeModelId\", value: this._contextCacheModelId(nextModel) });\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"model_select\",\n\t\t\tmodel: nextModel,\n\t\t\tpreviousModel,\n\t\t\tsource,\n\t\t});\n\t}\n\n\t/**\n\t * Set model directly.\n\t * Validates that auth is configured, saves to session and settings.\n\t * @throws Error if no auth is configured for the model\n\t */\n\tasync setModel(model: Model<any>): Promise<void> {\n\t\tif (!this._modelRegistry.hasConfiguredAuth(model)) {\n\t\t\tthrow new Error(`No API key for ${model.provider}/${model.id}`);\n\t\t}\n\n\t\t// Root-level: block sticky safety models (Fable) unless the user pinned `--model`.\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\tshouldEjectStickySafetyModel({\n\t\t\t\tblockStickySafetyModels: resilience.blockStickySafetyModels,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t}) &&\n\t\t\tisStickySafetyModel(model.id, model.provider)\n\t\t) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(model.id, model.provider));\n\t\t}\n\n\t\t// Grok OAuth: block selecting Imagine models as the session chat model.\n\t\tassertTextChatModelForCompletion(model.id, model.provider);\n\n\t\tconst previousModel = this.model;\n\t\tconst thinkingLevel = this._getThinkingLevelForModelSwitch();\n\t\tconst nextSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames(), model);\n\t\tthis.agent.state.model = model;\n\t\tthis._baseSystemPrompt = nextSystemPrompt;\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis.sessionManager.appendModelChange(model.provider, model.id);\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\n\t\t// Re-clamp thinking level for new model's capabilities\n\t\tthis.setThinkingLevel(thinkingLevel);\n\n\t\tawait this._emitModelSelect(model, previousModel, \"set\");\n\t}\n\n\t/**\n\t * Cycle to next/previous model.\n\t * Uses scoped models (from --models flag) if available, otherwise all available models.\n\t * @param direction - \"forward\" (default) or \"backward\"\n\t * @returns The new model info, or undefined if only one model available\n\t */\n\tasync cycleModel(direction: \"forward\" | \"backward\" = \"forward\"): Promise<ModelCycleResult | undefined> {\n\t\tif (this._scopedModels.length > 0) {\n\t\t\treturn this._cycleScopedModel(direction);\n\t\t}\n\t\treturn this._cycleAvailableModel(direction);\n\t}\n\n\tprivate async _cycleScopedModel(direction: \"forward\" | \"backward\"): Promise<ModelCycleResult | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tconst scopedModels = this._scopedModels.filter((scoped) => {\n\t\t\tif (!this._modelRegistry.hasConfiguredAuth(scoped.model)) return false;\n\t\t\tif (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tif (scopedModels.length <= 1) return undefined;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst len = scopedModels.length;\n\t\tconst nextIndex = direction === \"forward\" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;\n\t\tconst next = scopedModels[nextIndex];\n\t\tconst thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);\n\n\t\t// Single path through setModel (sticky block + auth + Imagine guards).\n\t\tawait this.setModel(next.model);\n\t\t// Explicit scoped thinking level still applies after switch.\n\t\tthis.setThinkingLevel(thinkingLevel);\n\n\t\treturn { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };\n\t}\n\n\tprivate async _cycleAvailableModel(direction: \"forward\" | \"backward\"): Promise<ModelCycleResult | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tconst raw = await this._modelRegistry.getAvailable();\n\t\t// Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).\n\t\tconst availableModels = resilience.blockStickySafetyModels\n\t\t\t? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))\n\t\t\t: raw;\n\t\tif (availableModels.length <= 1) return undefined;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst len = availableModels.length;\n\t\tconst nextIndex = direction === \"forward\" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;\n\t\tconst nextModel = availableModels[nextIndex];\n\n\t\t// Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.\n\t\tawait this.setModel(nextModel);\n\n\t\treturn { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };\n\t}\n\n\t// =========================================================================\n\t// Thinking Level Management\n\t// =========================================================================\n\n\t/**\n\t * Set thinking level.\n\t * Clamps to model capabilities based on available thinking levels.\n\t * Saves to session and settings only if the level actually changes.\n\t */\n\tsetThinkingLevel(level: ThinkingLevel): void {\n\t\tconst availableLevels = this.getAvailableThinkingLevels();\n\t\tconst effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);\n\n\t\t// Only persist if actually changing\n\t\tconst previousLevel = this.agent.state.thinkingLevel;\n\t\tconst isChanging = effectiveLevel !== previousLevel;\n\n\t\tthis.agent.state.thinkingLevel = effectiveLevel;\n\n\t\tif (isChanging) {\n\t\t\tthis.sessionManager.appendThinkingLevelChange(effectiveLevel);\n\t\t\tif (this.supportsThinking() || effectiveLevel !== \"off\") {\n\t\t\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveLevel);\n\t\t\t}\n\t\t\tthis._emit({ type: \"thinking_level_changed\", level: effectiveLevel });\n\t\t\tvoid this._extensionRunner.emit({\n\t\t\t\ttype: \"thinking_level_select\",\n\t\t\t\tlevel: effectiveLevel,\n\t\t\t\tpreviousLevel,\n\t\t\t});\n\t\t}\n\t}\n\n\t/** Apply an explicit user choice and retain it as bounded router feedback. */\n\tsetUserThinkingLevel(level: ThinkingLevel): void {\n\t\tconst overridesAuto = this._thinkingMode === \"auto\";\n\t\tthis.setThinkingMode(\"manual\");\n\t\tthis.setThinkingLevel(level);\n\t\tif (overridesAuto) {\n\t\t\tthis._recordUserThinkingOverride(this.thinkingLevel);\n\t\t} else {\n\t\t\tthis._lastAutoRouterDecision = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Set the thinking mode. \"auto\" resolves a level per turn via the reasoning\n\t * router; \"manual\" keeps the explicitly selected level. The mode persists for\n\t * the session lifetime and is never written to user settings.\n\t */\n\tsetThinkingMode(mode: ThinkingMode): void {\n\t\tthis._thinkingMode = mode;\n\t}\n\n\t/**\n\t * In auto thinking mode, resolve and apply this turn's thinking level from the\n\t * prompt content. Updates agent state, records the change in the session, and\n\t * notifies observers - but never overwrites the user's persisted default\n\t * thinking level in settings. Models without reasoning support bypass the\n\t * router entirely (level stays \"off\").\n\t */\n\tprivate _applyAutoThinkingLevelForTurn(promptText: string): void {\n\t\tif (this._thinkingMode !== \"auto\") return;\n\t\tif (!this.supportsThinking()) return;\n\n\t\tthis._applyAutoThinkingLevelV4(promptText);\n\t}\n\n\t/**\n\t * Auto-mode resolver. Reuses the N=8 recent-class history and\n\t * context-pressure bucket, routed through the confidence-bearing v4 classifier\n\t * and its uncertainty-aware resolver. No\n\t * `laneType` applies to the main session (always \"none\"/`undefined`);\n\t * the default-off Adaptorch bridge is wired but its current transport returns\n\t * no advisory hint. `bias` stays `0` unless BOTH hold: (a) the global,\n\t * owner-only `reasoningRouterLearning.enabled` setting is `true` (default\n\t * off; a project-scope `.omk/settings.json` value for this key is never\n\t * consulted -- see settings-manager.ts), and (b) a compiled\n\t * `RouterBiasSnapshot` was found and passed strict validation at the\n\t * configured path or opaque repository/worktree-scoped default, loaded and\n\t * cached (\"pinned\") at most once per session (see\n\t * `_getReasoningRouterBiasSnapshot`). When learning is enabled, one bounded\n\t * \"accepted\" record (no raw prompt/path/diff/session/model/provider/tool/hook\n\t * content; see router-feedback-collector.ts's exact ten-key schema) is\n\t * appended after every v4 auto-turn. A later explicit user level records one\n\t * bounded `s1-override` direction for the same cell. Both remain inert until\n\t * a future, separate offline compile step. The\n\t * resolver's own confidence-band/fallback-reason escalation (see\n\t * reasoning-router-v4.ts) still applies on top of the base+lane+bias\n\t * target, so a low-confidence or fallback-decided verdict can still only\n\t * match or exceed the confident-path level.\n\t */\n\tprivate _applyAutoThinkingLevelV4(promptText: string): void {\n\t\tconst availableLevels = this.getAvailableThinkingLevels();\n\t\tconst features = deriveRouterFeedbackFeaturesV4(promptText);\n\t\tconst verdict = classifyTaskV4(\n\t\t\t{\n\t\t\t\tprompt: promptText,\n\t\t\t\thistory: this._taskClassHistory,\n\t\t\t\tpressureBucket: this._computePressureBucket(),\n\t\t\t},\n\t\t\tundefined,\n\t\t\tfeatures,\n\t\t);\n\n\t\tthis._taskClassHistory.unshift(verdict.taskClass);\n\t\tif (this._taskClassHistory.length > 8) this._taskClassHistory.length = 8;\n\n\t\tconst learningEnabled = this.settingsManager.getReasoningRouterLearningEnabled();\n\t\tconst snapshot = learningEnabled ? this._getReasoningRouterBiasSnapshot() : null;\n\t\tconst bias =\n\t\t\tsnapshot === null\n\t\t\t\t? 0\n\t\t\t\t: getBiasStepsForCell(snapshot, {\n\t\t\t\t\t\tpredictedClass: verdict.taskClass,\n\t\t\t\t\t\tlaneType: \"none\",\n\t\t\t\t\t\tlenBucket: features.lenBucket,\n\t\t\t\t\t\thadFence: features.hadFence,\n\t\t\t\t\t\thadDiff: features.hadDiff,\n\t\t\t\t\t});\n\n\t\tconst resolved = resolveThinkingLevelV4WithUncertainty(\n\t\t\tverdict,\n\t\t\tavailableLevels,\n\t\t\tundefined,\n\t\t\tbias,\n\t\t\tthis._getAdaptorchHint(verdict, features),\n\t\t);\n\n\t\tthis._lastAutoRouterDecision = undefined;\n\t\tif (learningEnabled && resolved !== \"off\") {\n\t\t\tconst decision: RouterAutoDecision = {\n\t\t\t\trouterVersion: \"v4\",\n\t\t\t\tlaneType: \"none\",\n\t\t\t\tpredictedClass: verdict.taskClass,\n\t\t\t\tresolvedLevel: resolved,\n\t\t\t\tlenBucket: features.lenBucket,\n\t\t\t\thadFence: features.hadFence,\n\t\t\t\thadDiff: features.hadDiff,\n\t\t\t};\n\t\t\tthis._lastAutoRouterDecision = decision;\n\t\t\tthis._appendRouterFeedback({\n\t\t\t\t...decision,\n\t\t\t\tacceptedLevel: resolved,\n\t\t\t\tsignal: \"s2-accept\",\n\t\t\t\toutcome: \"accepted\",\n\t\t\t});\n\t\t}\n\n\t\tconst previousLevel = this.agent.state.thinkingLevel;\n\t\tif (resolved === previousLevel) return;\n\n\t\tthis.agent.state.thinkingLevel = resolved;\n\t\tthis.sessionManager.appendThinkingLevelChange(resolved);\n\t\tthis._emit({ type: \"thinking_level_changed\", level: resolved });\n\t}\n\n\tprivate _getRepositoryRouterLearningPaths(): RepositoryRouterLearningPaths {\n\t\tif (this._repositoryRouterLearningPaths === undefined) {\n\t\t\tthis._repositoryRouterLearningPaths = getRepositoryRouterLearningPaths(this._cwd);\n\t\t}\n\t\treturn this._repositoryRouterLearningPaths;\n\t}\n\n\tprivate _appendRouterFeedback(record: RouterFeedbackRecord): void {\n\t\tappendRouterFeedbackRecord(record, {\n\t\t\tenabled: this.settingsManager.getReasoningRouterLearningEnabled(),\n\t\t\tledgerPath:\n\t\t\t\tthis.settingsManager.getReasoningRouterLearningFeedbackLedgerPath() ??\n\t\t\t\tthis._getRepositoryRouterLearningPaths().ledgerPath,\n\t\t});\n\t}\n\n\tprivate _recordUserThinkingOverride(acceptedLevel: ThinkingLevel): void {\n\t\tconst decision = this._lastAutoRouterDecision;\n\t\tthis._lastAutoRouterDecision = undefined;\n\t\tif (!this.settingsManager.getReasoningRouterLearningEnabled() || !decision || acceptedLevel === \"off\") return;\n\n\t\tconst resolvedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(decision.resolvedLevel);\n\t\tconst acceptedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(acceptedLevel);\n\t\tconst outcome = acceptedIndex > resolvedIndex ? \"up\" : acceptedIndex < resolvedIndex ? \"down\" : \"same\";\n\t\tthis._appendRouterFeedback({\n\t\t\t...decision,\n\t\t\tacceptedLevel,\n\t\t\tsignal: \"s1-override\",\n\t\t\toutcome,\n\t\t});\n\t}\n\n\t/**\n\t * Strictly load the compiled bias snapshot once per session. The first call\n\t * pins either a valid snapshot or a null miss; later calls never touch disk.\n\t */\n\tprivate _getReasoningRouterBiasSnapshot(): RouterBiasSnapshot | null {\n\t\tif (this._reasoningRouterBiasSnapshotLoaded) return this._reasoningRouterBiasSnapshot;\n\t\tthis._reasoningRouterBiasSnapshotLoaded = true;\n\n\t\tconst path =\n\t\t\tthis.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??\n\t\t\tthis._getRepositoryRouterLearningPaths().biasSnapshotPath;\n\t\ttry {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tthis._reasoningRouterBiasSnapshot = parseRouterBiasSnapshot(readFileSync(path, \"utf-8\"));\n\t\t\t}\n\t\t} catch {\n\t\t\tthis._reasoningRouterBiasSnapshot = null;\n\t\t}\n\t\treturn this._reasoningRouterBiasSnapshot;\n\t}\n\n\t/**\n\t * Context-pressure band 0..3 from the projected token estimate over the\n\t * model context window (reuses estimateProjectedContextTokens). 0..<0.5,\n\t * 1..<0.75, 2..<0.9, 3..>=0.9. Inert under DEFAULT_WEIGHTS (no pressure\n\t * coefficient); computed for future calibrated weight presets.\n\t */\n\tprivate _computePressureBucket(): number {\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst estimate = estimateProjectedContextTokens(this.agent.state.messages, []);\n\t\tconst pressure = estimate.tokens / contextWindow;\n\t\tif (pressure >= 0.9) return 3;\n\t\tif (pressure >= 0.75) return 2;\n\t\tif (pressure >= 0.5) return 1;\n\t\treturn 0;\n\t}\n\n\t/**\n\t * AdaptOrch advisory bridge hint accessor (default-off, global-only).\n\t * Lazily constructs the bridge on first call when enabled; returns `null`\n\t * (no hint) otherwise. The bridge's `getFreshHint` is synchronous and\n\t * cache-read-only, so this never blocks the turn-start path. A fire-and-\n\t * forget `requestRefresh` is issued for the NEXT turn's benefit.\n\t *\n\t * The returned hint is shaped as `{ level, confidence }` for the resolver's\n\t * hint-fusion slot: confidence is mapped from the bridge's closed\n\t * `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is\n\t * derived from the bridge's `taskClass` via the same static rule table the\n\t * resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates\n\t * whether the hint actually fuses, so a \"low\" band hint is structurally\n\t * inert.\n\t */\n\tprivate _getAdaptorchHint(\n\t\tverdict: { taskClass: TaskClassV4 },\n\t\tfeatures: RouterFeedbackFeaturesV4,\n\t): { level: import(\"omk-agent-core\").ThinkingLevel; confidence: number } | null {\n\t\tif (!this.settingsManager.getAdaptorchBridgeEnabled()) return null;\n\n\t\t// Lazy init (at most once per session)\n\t\tif (!this._adaptorchBridgeInitAttempted) {\n\t\t\tthis._adaptorchBridgeInitAttempted = true;\n\t\t\tconst bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();\n\t\t\tthis._adaptorchBridge = createAdaptorchBridge({\n\t\t\t\t// The advisory function is a no-op stub until a real MCP transport\n\t\t\t\t// is wired by a future lane. The bridge's circuit breaker and budget\n\t\t\t\t// counter ensure this stub can never cause harm.\n\t\t\t\tadvisoryFn: async () => null,\n\t\t\t\tttlMs: bridgeSettings?.ttlMs,\n\t\t\t\ttimeoutMs: bridgeSettings?.timeoutMs,\n\t\t\t\tmaxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,\n\t\t\t\tfailureThreshold: bridgeSettings?.failureThreshold,\n\t\t\t});\n\t\t}\n\n\t\tconst bridge = this._adaptorchBridge;\n\t\tif (bridge === null) return null;\n\n\t\tconst payload: AdaptorchConsultPayload = {\n\t\t\tschemaVersion: 1,\n\t\t\ttaskClass: verdict.taskClass as AdaptorchConsultPayload[\"taskClass\"],\n\t\t\trunnerUp: verdict.taskClass as AdaptorchConsultPayload[\"runnerUp\"],\n\t\t\tmarginBucket: \"high\",\n\t\t\tlenBucket: features.lenBucket as AdaptorchConsultPayload[\"lenBucket\"],\n\t\t\thadFence: features.hadFence,\n\t\t\thadDiff: features.hadDiff,\n\t\t\tpressureBucket: this._computePressureBucket() as AdaptorchConsultPayload[\"pressureBucket\"],\n\t\t};\n\n\t\t// Fire-and-forget refresh for next turn\n\t\tbridge.requestRefresh(payload);\n\n\t\t// Synchronous cache read for this turn\n\t\tconst hint = bridge.getFreshHint(payload);\n\t\tif (hint === null) return null;\n\n\t\t// Map confidenceBand -> numeric confidence for the resolver's threshold gate\n\t\tconst confidenceMap: Record<string, number> = { low: 0.5, medium: 0.75, high: 0.95 };\n\t\tconst confidence = confidenceMap[hint.confidenceBand] ?? 0.5;\n\n\t\treturn { level: TASK_CLASS_THINKING_LEVELS_V4[hint.taskClass], confidence };\n\t}\n\n\t/**\n\t * Cycle to next thinking level.\n\t * @returns New level, or undefined if model doesn't support thinking\n\t */\n\tcycleThinkingLevel(): ThinkingLevel | undefined {\n\t\tif (!this.supportsThinking()) return undefined;\n\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tconst currentIndex = levels.indexOf(this.thinkingLevel);\n\t\tconst nextIndex = (currentIndex + 1) % levels.length;\n\t\tconst nextLevel = levels[nextIndex];\n\t\tconst overridesAuto = this._thinkingMode === \"auto\";\n\n\t\tthis.setThinkingLevel(nextLevel);\n\t\tif (overridesAuto) {\n\t\t\tthis.setThinkingMode(\"manual\");\n\t\t\tthis._recordUserThinkingOverride(nextLevel);\n\t\t}\n\t\treturn nextLevel;\n\t}\n\n\t/**\n\t * Get available thinking levels for current model.\n\t * The provider will clamp to what the specific model supports internally.\n\t */\n\tgetAvailableThinkingLevels(): ThinkingLevel[] {\n\t\tif (!this.model) return THINKING_LEVELS;\n\t\treturn getSupportedThinkingLevels(this.model) as ThinkingLevel[];\n\t}\n\n\t/**\n\t * Check if current model supports thinking/reasoning.\n\t */\n\tsupportsThinking(): boolean {\n\t\treturn Boolean(this.model?.reasoning);\n\t}\n\n\tprivate _getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel {\n\t\tif (explicitLevel !== undefined) {\n\t\t\treturn explicitLevel;\n\t\t}\n\t\tif (!this.supportsThinking()) {\n\t\t\treturn this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t\t}\n\t\treturn this.thinkingLevel;\n\t}\n\n\tprivate _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel {\n\t\treturn this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : \"off\";\n\t}\n\n\t// =========================================================================\n\t// Queue Mode Management\n\t// =========================================================================\n\n\t/**\n\t * Set steering message mode.\n\t * Saves to settings.\n\t */\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.agent.steeringMode = mode;\n\t\tthis.settingsManager.setSteeringMode(mode);\n\t}\n\n\t/**\n\t * Set follow-up message mode.\n\t * Saves to settings.\n\t */\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.agent.followUpMode = mode;\n\t\tthis.settingsManager.setFollowUpMode(mode);\n\t}\n\n\t// =========================================================================\n\t// Compaction\n\t// =========================================================================\n\n\tprivate _beginCompactionTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\treturn this._compactionService.beginTransaction(compactionModel, emergency);\n\t}\n\n\tprivate _commitCompaction(\n\t\tbegun: BegunCompaction,\n\t\tresult: CompactionResult,\n\t\tfromExtension: boolean,\n\t): CommittedCompaction {\n\t\treturn this._compactionService.commit(begun, result, fromExtension);\n\t}\n\n\tprivate _pendingToolResultReserve(settings: CompactionSettings): number {\n\t\tconst pendingIds = this.agent.state.pendingToolCalls;\n\t\tconst calls = new Map<string, { readonly name: string; readonly args: unknown }>();\n\t\tconst streaming = this.agent.state.streamingMessage;\n\t\tconst messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;\n\t\tfor (const message of messages) {\n\t\t\tif (message.role !== \"assistant\" || !Array.isArray(message.content)) continue;\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type === \"toolCall\" && pendingIds.has(part.id)) {\n\t\t\t\t\tcalls.set(part.id, { name: part.name, args: part.arguments });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst counts: Record<ToolResultClass, number> = { text: 0, image: 0, \"large-output\": 0 };\n\t\tfor (const id of pendingIds) {\n\t\t\tconst call = calls.get(id);\n\t\t\tconst resultClass = call ? pendingToolResultClass(call.name, call.args) : \"large-output\";\n\t\t\tcounts[resultClass] += 1;\n\t\t}\n\t\tconst requests: ToolResultReserveRequest[] = [];\n\t\tfor (const resultClass of [\"text\", \"image\", \"large-output\"] as const) {\n\t\t\tif (counts[resultClass] > 0) {\n\t\t\t\trequests.push({\n\t\t\t\t\tclass: resultClass,\n\t\t\t\t\tcount: counts[resultClass],\n\t\t\t\t\ttokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tconst configured = settings.reservedToolResultTokens ?? 0;\n\t\trequests.push({ class: \"large-output\", count: 1, tokensPerResult: configured });\n\t\treturn estimateToolResultReserve(requests);\n\t}\n\n\tprivate _compactionHysteresisConfig(contextWindow: number, settings: CompactionSettings) {\n\t\tconst threshold = getCompactionHeadroomThreshold(contextWindow, {\n\t\t\t...settings,\n\t\t\treservedToolResultTokens: this._pendingToolResultReserve(settings),\n\t\t});\n\t\tif (!threshold) return undefined;\n\t\tconst triggerRatio = Math.min(\n\t\t\t1,\n\t\t\tMath.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow),\n\t\t);\n\t\tconst configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;\n\t\tconst rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);\n\t\tconst emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);\n\t\treturn createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });\n\t}\n\n\tprivate _runtimeCompactionDecision(\n\t\tcontextTokens: number,\n\t\tcontextWindow: number,\n\t\tsettings: CompactionSettings,\n\t): { readonly compact: boolean; readonly emergency: boolean } {\n\t\tif (!Number.isFinite(contextTokens) || contextTokens < 0) return { compact: false, emergency: false };\n\t\tconst config = this._compactionHysteresisConfig(contextWindow, settings);\n\t\tif (!config) return { compact: false, emergency: false };\n\t\tconst result = stepCompactionHysteresis({\n\t\t\tconfig,\n\t\t\tstate: this._compactionHysteresisState,\n\t\t\tratio: Math.min(1, contextTokens / contextWindow),\n\t\t});\n\t\tthis._compactionHysteresisState = result.nextState;\n\t\treturn { compact: result.action === \"compact\", emergency: result.reason === \"emergency_threshold_reached\" };\n\t}\n\n\tprivate async _runThresholdCompaction(emergency: boolean): Promise<boolean> {\n\t\tthis._thresholdCompactionEmergency = emergency;\n\t\ttry {\n\t\t\treturn await this._runAutoCompaction(\"threshold\", false);\n\t\t} finally {\n\t\t\tthis._thresholdCompactionEmergency = false;\n\t\t}\n\t}\n\n\tprivate _recordCompactionCommitForHysteresis(): void {\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\t\tconst config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());\n\t\tif (!config) return;\n\t\tthis._compactionHysteresisState = stepCompactionHysteresis({\n\t\t\tconfig,\n\t\t\tstate: this._compactionHysteresisState,\n\t\t\tratio: 0,\n\t\t\toutcome: \"commit\",\n\t\t}).nextState;\n\t}\n\n\tprivate _resolveCompactionModel(sessionModel: Model<Api>): Model<Api> {\n\t\tconst configuredModel = this.settingsManager.getCompactionModel();\n\t\tconst availableModels = this._modelRegistry.getAvailable();\n\t\tlet model: Model<Api>;\n\t\tif (configuredModel) {\n\t\t\tconst configured = findExactModelReferenceMatch(configuredModel, availableModels);\n\t\t\tif (!configured) {\n\t\t\t\tthrow new Error(`Configured compaction model \"${configuredModel}\" is unavailable or unauthenticated.`);\n\t\t\t}\n\t\t\tmodel = configured;\n\t\t} else {\n\t\t\tmodel = resolveCompactionModel(sessionModel, availableModels);\n\t\t}\n\t\tthis._invalidateContextBudgetCache({\n\t\t\ttype: \"compactionModelId\",\n\t\t\tvalue: this._contextCacheModelId(model),\n\t\t});\n\t\treturn model;\n\t}\n\n\t/**\n\t * Manually compact the session context.\n\t * Aborts current agent operation first.\n\t * @param customInstructions Optional instructions for the compaction summary\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tawait this.abort();\n\t\tthis._disconnectFromAgent();\n\t\tthis._compactionAbortController = new AbortController();\n\t\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\t\tlet committedCompaction = false;\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\tconst compactionModel = this._resolveCompactionModel(this.model);\n\t\t\tconst { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);\n\n\t\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\t\tconst begun = this._beginCompactionTransaction(compactionModel, false);\n\t\t\tconst pathEntries = [...begun.capture.branchEntries];\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\t// Check why we can't compact\n\t\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t\t}\n\t\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t\t}\n\n\t\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\t\tconst result = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\tsignal: this._compactionAbortController.signal,\n\t\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t\t}\n\n\t\t\t\tif (result?.compaction) {\n\t\t\t\t\textensionCompaction = result.compaction;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet summary: string;\n\t\t\tlet firstKeptEntryId: string;\n\t\t\tlet tokensBefore: number;\n\t\t\tlet details: unknown;\n\n\t\t\tif (extensionCompaction) {\n\t\t\t\t// Extension provided compaction content\n\t\t\t\tsummary = extensionCompaction.summary;\n\t\t\t\tfirstKeptEntryId = extensionCompaction.firstKeptEntryId;\n\t\t\t\ttokensBefore = extensionCompaction.tokensBefore;\n\t\t\t\tdetails = extensionCompaction.details;\n\t\t\t} else {\n\t\t\t\t// Generate compaction result\n\t\t\t\tconst result = await compact(\n\t\t\t\t\tpreparation,\n\t\t\t\t\tcompactionModel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\tthis._compactionAbortController.signal,\n\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\tthis.agent.streamFn,\n\t\t\t\t\tthis.settingsManager.getRetrySettings(),\n\t\t\t\t\tthis._summarizationRetryCallbacks({ source: \"compaction\", reason: \"manual\" }),\n\t\t\t\t);\n\t\t\t\tsummary = result.summary;\n\t\t\t\tfirstKeptEntryId = result.firstKeptEntryId;\n\t\t\t\ttokensBefore = result.tokensBefore;\n\t\t\t\tdetails = result.details;\n\t\t\t}\n\n\t\t\tif (this._compactionAbortController.signal.aborted) {\n\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t}\n\n\t\t\tconst compactionResult: CompactionResult = {\n\t\t\t\tsummary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tconst committed = this._commitCompaction(begun, compactionResult, fromExtension);\n\t\t\tcommittedCompaction = true;\n\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\n\t\t\tif (this._extensionRunner) {\n\t\t\t\tawait this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_compact\",\n\t\t\t\t\tcompactionEntry: committed.entry,\n\t\t\t\t\tfromExtension,\n\t\t\t\t});\n\t\t\t}\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: compactionResult,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn compactionResult;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\t\tconst stale = /stale|session changed during compaction|already compacted/i.test(message);\n\t\t\tlet compactionCode: \"aborted\" | \"stale\" | \"failed\" = \"failed\";\n\t\t\tif (aborted) compactionCode = \"aborted\";\n\t\t\telse if (stale) compactionCode = \"stale\";\n\t\t\tconst timestamp = new Date().toISOString();\n\t\t\tthis._publishTermination(\n\t\t\t\tclassifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId: `compaction-${randomUUID()}`,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: this._terminationMessage(message, \"Manual compaction failed.\"),\n\t\t\t\t\tcause: { area: \"compaction\", code: compactionCode },\n\t\t\t\t\tsideEffects: committedCompaction ? \"confirmed\" : \"none\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t\t});\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis._compactionAbortController = undefined;\n\t\t\tthis._reconnectToAgent();\n\t\t}\n\t}\n\n\t/**\n\t * Cancel in-progress compaction (manual or auto).\n\t */\n\tabortCompaction(): void {\n\t\tthis._compactionAbortController?.abort();\n\t\tthis._autoCompactionAbortController?.abort();\n\t}\n\n\t/**\n\t * Cancel in-progress branch summarization.\n\t */\n\tabortBranchSummary(): void {\n\t\tthis._branchSummaryAbortController?.abort();\n\t}\n\n\t/** True when the transcript carries image content that forces a vision-route turn. */\n\tprivate _transcriptHasImages(messages: AgentMessage[]): boolean {\n\t\treturn messages.some((m) => {\n\t\t\tconst content = (m as { content?: unknown }).content;\n\t\t\tif (!Array.isArray(content)) return false;\n\t\t\treturn content.some((part: unknown) => {\n\t\t\t\treturn typeof part === \"object\" && part !== null && (part as { type?: string }).type === \"image\";\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Effective context window for the upcoming turn.\n\t * Image-bearing turns with a text-only session model are auto-routed to the\n\t * vision model (gpt-5.6-luna, 400K) — that window is the real limit. Text-only\n\t * turns keep the session model's (typically much larger) window.\n\t */\n\tprivate _effectiveTurnContextWindow(pendingMessages: AgentMessage[], sessionWindow: number): number {\n\t\t// Vision routing keys off the full transcript, not just the pending turn:\n\t\t// images retained in history keep every subsequent request on the vision model.\n\t\tconst hasImages =\n\t\t\tthis._transcriptHasImages(this.agent.state.messages) || this._transcriptHasImages(pendingMessages);\n\t\tconst model = this.model;\n\t\tif (!hasImages || !model || (model.input ?? []).includes(\"image\")) {\n\t\t\treturn sessionWindow;\n\t\t}\n\t\treturn Math.min(sessionWindow, getVisionRouteModel(model).contextWindow);\n\t}\n\n\tprivate async _checkProjectedCompaction(pendingMessages: AgentMessage[]): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\tif (!settings.enabled || pendingMessages.length === 0) return false;\n\n\t\tconst sessionWindow = this.model?.contextWindow ?? 0;\n\t\tif (sessionWindow <= 0) return false;\n\t\t// Image-bearing turns are served by the auto-routed vision model, not the\n\t\t// session model. Its window (not the session model's) is the real limit the\n\t\t// provider enforces, so threshold compaction must use it or the vision\n\t\t// request overflows (context_length_exceeded) before deepseek-based\n\t\t// compaction would fire.\n\t\tconst contextWindow = this._effectiveTurnContextWindow(pendingMessages, sessionWindow);\n\t\tif (contextWindow <= 0) return false;\n\n\t\tconst messages = [...this.agent.state.messages, ...pendingMessages];\n\t\tconst estimate = estimateProjectedContextTokens(this.agent.state.messages, pendingMessages);\n\t\tconst compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());\n\t\tif (estimate.lastUsageIndex !== null && compactionEntry) {\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tconst decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);\n\t\tif (decision.compact) {\n\t\t\treturn this._runThresholdCompaction(decision.emergency);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Check if compaction is needed and run it.\n\t * Called after agent_end and before prompt submission.\n\t *\n\t * Two cases:\n\t * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n\t * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)\n\t *\n\t * @param assistantMessage The assistant message to check\n\t * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n\t */\n\tprivate async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\t// Stale pre-compaction usage/errors must not retrigger compaction on the\n\t\t// first prompt after one finished.\n\t\tconst compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());\n\t\tif (\n\t\t\tshouldSkipCompactionCheck({\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tskipAbortedCheck,\n\t\t\t\tstopReason: assistantMessage.stopReason,\n\t\t\t\tmessageTimestamp: assistantMessage.timestamp,\n\t\t\t\tlatestCompactionTimestamp: compactionEntry ? new Date(compactionEntry.timestamp).getTime() : undefined,\n\t\t\t})\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\n\t\t// Case 1: Overflow - LLM returned context overflow error. Only counts when\n\t\t// the failing message belongs to the session model — or to the auto-routed\n\t\t// vision model while the session model is text-only.\n\t\tif (\n\t\t\tisSessionModelOverflow({\n\t\t\t\tmessage: assistantMessage,\n\t\t\t\tcontextWindow,\n\t\t\t\tsessionProvider: this.model?.provider,\n\t\t\t\tsessionModelId: this.model?.id,\n\t\t\t\tsessionInputs: this.model?.input,\n\t\t\t})\n\t\t) {\n\t\t\tif (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Context overflow recovery failed after two staged compact-and-retry attempts. Reduce the latest input or switch to a model with a larger effective context window.\",\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis._overflowRecoveryAttempts++;\n\t\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t\t// but we don't want it in context for the retry)\n\t\t\tconst messages = this.agent.state.messages;\n\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t\t}\n\t\t\treturn this._runAutoCompaction(\"overflow\", true);\n\t\t}\n\n\t\t// Case 2: Threshold - context is getting large\n\t\t// For error messages (no usage data), estimate from last successful response.\n\t\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\t\tlet contextTokens: number;\n\t\tif (assistantMessage.stopReason === \"error\") {\n\t\t\tconst messages = this.agent.state.messages;\n\t\t\tconst estimate = estimateContextTokens(messages);\n\t\t\tif (estimate.lastUsageIndex === null) return false; // No usage data at all\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcontextTokens = estimate.tokens;\n\t\t} else {\n\t\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t\t}\n\t\tconst decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);\n\t\tif (decision.compact) {\n\t\t\treturn this._runThresholdCompaction(decision.emergency);\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate _overflowCompactionSettings(settings: CompactionSettings, attempt: number): CompactionSettings {\n\t\tif (attempt < MAX_OVERFLOW_RECOVERY_ATTEMPTS) return settings;\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens: Math.min(settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),\n\t\t\treservedOutputTokens: Math.min(\n\t\t\t\tsettings.reservedOutputTokens ?? settings.reserveTokens,\n\t\t\t\tOVERFLOW_RECOVERY_EMERGENCY_TOKENS,\n\t\t\t),\n\t\t\tkeepRecentTokens: Math.min(settings.keepRecentTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),\n\t\t};\n\t}\n\n\t/**\n\t * Internal: Run auto-compaction with events.\n\t */\n\tprivate async _runAutoCompaction(\n\t\treason: \"overflow\" | \"threshold\",\n\t\twillRetry: boolean,\n\t\temergency = reason === \"overflow\" || this._thresholdCompactionEmergency,\n\t): Promise<boolean> {\n\t\tconst configuredSettings = this.settingsManager.getCompactionSettings();\n\t\tconst settings =\n\t\t\treason === \"overflow\"\n\t\t\t\t? this._overflowCompactionSettings(configuredSettings, this._overflowRecoveryAttempts)\n\t\t\t\t: configuredSettings;\n\n\t\tthis._emit({ type: \"compaction_start\", reason });\n\t\tthis._autoCompactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst compactionModel = this._resolveCompactionModel(this.model);\n\t\t\tlet apiKey: string | undefined;\n\t\t\tlet headers: Record<string, string> | undefined;\n\t\t\tif (this.agent.streamFn === streamSimple) {\n\t\t\t\tconst authResult = await this._modelRegistry.getApiKeyAndHeaders(compactionModel);\n\t\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\t\tconst providerLabel = compactionModel.provider;\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\t\treason,\n\t\t\t\t\t\tresult: undefined,\n\t\t\t\t\t\taborted: false,\n\t\t\t\t\t\twillRetry: false,\n\t\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\t`Auto-compaction could not authenticate for \"${providerLabel}\" (${compactionModel.id}). ` +\n\t\t\t\t\t\t\t`Check proxy/OAuth health and run '/login ${providerLabel}' if needed.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tapiKey = authResult.apiKey;\n\t\t\t\theaders = authResult.headers;\n\t\t\t} else {\n\t\t\t\t({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));\n\t\t\t}\n\n\t\t\tconst begun = this._beginCompactionTransaction(compactionModel, emergency);\n\t\t\tconst pathEntries = [...begun.capture.branchEntries];\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\t\tconst extensionResult = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\t\tcustomInstructions: undefined,\n\t\t\t\t\tsignal: this._autoCompactionAbortController.signal,\n\t\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\t\tif (extensionResult?.cancel) {\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\t\treason,\n\t\t\t\t\t\tresult: undefined,\n\t\t\t\t\t\taborted: true,\n\t\t\t\t\t\twillRetry: false,\n\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (extensionResult?.compaction) {\n\t\t\t\t\textensionCompaction = extensionResult.compaction;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet summary: string;\n\t\t\tlet firstKeptEntryId: string;\n\t\t\tlet tokensBefore: number;\n\t\t\tlet details: unknown;\n\n\t\t\tif (extensionCompaction) {\n\t\t\t\t// Extension provided compaction content\n\t\t\t\tsummary = extensionCompaction.summary;\n\t\t\t\tfirstKeptEntryId = extensionCompaction.firstKeptEntryId;\n\t\t\t\ttokensBefore = extensionCompaction.tokensBefore;\n\t\t\t\tdetails = extensionCompaction.details;\n\t\t\t} else {\n\t\t\t\t// Generate compaction result\n\t\t\t\tconst compactResult = await compact(\n\t\t\t\t\tpreparation,\n\t\t\t\t\tcompactionModel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tundefined,\n\t\t\t\t\tthis._autoCompactionAbortController.signal,\n\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\tthis.agent.streamFn,\n\t\t\t\t\tthis.settingsManager.getRetrySettings(),\n\t\t\t\t\tthis._summarizationRetryCallbacks({ source: \"compaction\", reason }),\n\t\t\t\t);\n\t\t\t\tsummary = compactResult.summary;\n\t\t\t\tfirstKeptEntryId = compactResult.firstKeptEntryId;\n\t\t\t\ttokensBefore = compactResult.tokensBefore;\n\t\t\t\tdetails = compactResult.details;\n\t\t\t}\n\n\t\t\tif (this._autoCompactionAbortController.signal.aborted) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst result: CompactionResult = {\n\t\t\t\tsummary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tconst committed = this._commitCompaction(begun, result, fromExtension);\n\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\n\t\t\tif (this._extensionRunner) {\n\t\t\t\tawait this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_compact\",\n\t\t\t\t\tcompactionEntry: committed.entry,\n\t\t\t\t\tfromExtension,\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Whether the agent loop will resume after this compaction. Shared by\n\t\t\t// the compaction_end event (so the TUI flushes its queued messages\n\t\t\t// correctly) and the return value below.\n\t\t\tconst resumeMessages = this.agent.state.messages;\n\t\t\tlet lastAssistantMsg: AssistantMessage | undefined;\n\t\t\tfor (let i = resumeMessages.length - 1; i >= 0; i--) {\n\t\t\t\tconst candidate = resumeMessages[i];\n\t\t\t\tif (candidate.role === \"assistant\") {\n\t\t\t\t\tlastAssistantMsg = candidate as AssistantMessage;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst endedCleanly = lastAssistantMsg?.stopReason === \"stop\";\n\t\t\tconst willResume = this.agent.hasQueuedMessages() || !endedCleanly;\n\t\t\tconst emitWillRetry = compactionEmitWillRetry(willRetry, willResume);\n\t\t\tthis._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry: emitWillRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = this.agent.state.messages;\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg as AssistantMessage).stopReason === \"error\") {\n\t\t\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Resume the agent loop after a successful auto-compaction unless the\n\t\t\t// agent had already finished cleanly. Previously this returned\n\t\t\t// `hasQueuedMessages()`, which made the agent stall right after the\n\t\t\t// purple \"Auto-compacting...\" indicator whenever no steer/follow-up\n\t\t\t// messages were queued — even when the last turn was cut off mid-task\n\t\t\t// (stopReason toolUse/length/error/aborted). Continue in that case so\n\t\t\t// the task runs to completion on the compacted context. Hysteresis\n\t\t\t// (disarmed until context drops below rearmRatio) prevents an immediate\n\t\t\t// re-trigger; a clean \"stop\" with nothing queued still ends the loop.\n\t\t\treturn willResume;\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage:\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t\t});\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._autoCompactionAbortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Toggle auto-compaction setting.\n\t */\n\tsetAutoCompactionEnabled(enabled: boolean): void {\n\t\tthis.settingsManager.setCompactionEnabled(enabled);\n\t}\n\n\t/** Whether auto-compaction is enabled */\n\tget autoCompactionEnabled(): boolean {\n\t\treturn this.settingsManager.getCompactionEnabled();\n\t}\n\n\tasync bindExtensions(bindings: ExtensionBindings): Promise<void> {\n\t\tif (bindings.uiContext !== undefined) {\n\t\t\tthis._extensionUIContext = bindings.uiContext;\n\t\t}\n\t\tif (bindings.mode !== undefined) {\n\t\t\tthis._extensionMode = bindings.mode;\n\t\t}\n\t\tif (bindings.commandContextActions !== undefined) {\n\t\t\tthis._extensionCommandContextActions = bindings.commandContextActions;\n\t\t}\n\t\tif (bindings.abortHandler !== undefined) {\n\t\t\tthis._extensionAbortHandler = bindings.abortHandler;\n\t\t}\n\t\tif (bindings.shutdownHandler !== undefined) {\n\t\t\tthis._extensionShutdownHandler = bindings.shutdownHandler;\n\t\t}\n\t\tif (bindings.onError !== undefined) {\n\t\t\tthis._extensionErrorListener = bindings.onError;\n\t\t}\n\n\t\tthis._applyExtensionBindings(this._extensionRunner);\n\t\tawait this._extensionRunner.emit(this._sessionStartEvent);\n\t\tawait this.extendResourcesFromExtensions(this._sessionStartEvent.reason === \"reload\" ? \"reload\" : \"startup\");\n\t}\n\n\tprivate async extendResourcesFromExtensions(reason: \"startup\" | \"reload\"): Promise<void> {\n\t\tif (!this._extensionRunner.hasHandlers(\"resources_discover\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { skillPaths, promptPaths, themePaths } = await this._extensionRunner.emitResourcesDiscover(\n\t\t\tthis._cwd,\n\t\t\treason,\n\t\t);\n\n\t\tif (skillPaths.length === 0 && promptPaths.length === 0 && themePaths.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst extensionPaths: ResourceExtensionPaths = {\n\t\t\tskillPaths: this.buildExtensionResourcePaths(skillPaths),\n\t\t\tpromptPaths: this.buildExtensionResourcePaths(promptPaths),\n\t\t\tthemePaths: this.buildExtensionResourcePaths(themePaths),\n\t\t};\n\n\t\tthis._resourceLoader.extendResources(extensionPaths);\n\t\tthis._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis._recordPromptCachePlan(\"resource-reload\");\n\t}\n\n\tprivate buildExtensionResourcePaths(entries: Array<{ path: string; extensionPath: string }>): Array<{\n\t\tpath: string;\n\t\tmetadata: { source: string; scope: \"temporary\"; origin: \"top-level\"; baseDir?: string };\n\t}> {\n\t\treturn entries.map((entry) => {\n\t\t\tconst source = this.getExtensionSourceLabel(entry.extensionPath);\n\t\t\tconst baseDir = entry.extensionPath.startsWith(\"<\") ? undefined : dirname(entry.extensionPath);\n\t\t\treturn {\n\t\t\t\tpath: entry.path,\n\t\t\t\tmetadata: {\n\t\t\t\t\tsource,\n\t\t\t\t\tscope: \"temporary\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t\tbaseDir,\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate getExtensionSourceLabel(extensionPath: string): string {\n\t\tif (extensionPath.startsWith(\"<\")) {\n\t\t\treturn `extension:${extensionPath.replace(/[<>]/g, \"\")}`;\n\t\t}\n\t\tconst base = basename(extensionPath);\n\t\tconst name = base.replace(/\\.(ts|js)$/, \"\");\n\t\treturn `extension:${name}`;\n\t}\n\n\tprivate _applyExtensionBindings(runner: ExtensionRunner): void {\n\t\trunner.setUIContext(this._extensionUIContext, this._extensionMode);\n\t\trunner.bindCommandContext(this._extensionCommandContextActions);\n\n\t\tthis._extensionErrorUnsubscriber?.();\n\t\tthis._extensionErrorUnsubscriber = this._extensionErrorListener\n\t\t\t? runner.onError(this._extensionErrorListener)\n\t\t\t: undefined;\n\t}\n\n\tprivate _refreshCurrentModelFromRegistry(): void {\n\t\tconst currentModel = this.model;\n\t\tif (!currentModel) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst refreshedModel = this._modelRegistry.find(currentModel.provider, currentModel.id);\n\t\tif (!refreshedModel || refreshedModel === currentModel) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.agent.state.model = refreshedModel;\n\t}\n\n\tprivate _bindExtensionCore(runner: ExtensionRunner): void {\n\t\tconst getCommands = (): SlashCommandInfo[] => {\n\t\t\tconst extensionCommands: SlashCommandInfo[] = runner.getRegisteredCommands().map((command) => ({\n\t\t\t\tname: command.invocationName,\n\t\t\t\tdescription: command.description,\n\t\t\t\tsource: \"extension\",\n\t\t\t\tsourceInfo: command.sourceInfo,\n\t\t\t}));\n\n\t\t\tconst templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({\n\t\t\t\tname: template.name,\n\t\t\t\tdescription: template.description,\n\t\t\t\tsource: \"prompt\",\n\t\t\t\tsourceInfo: template.sourceInfo,\n\t\t\t}));\n\n\t\t\tconst skills: SlashCommandInfo[] = this._resourceLoader.getSkills().skills.map((skill) => ({\n\t\t\t\tname: `skill:${skill.name}`,\n\t\t\t\tdescription: skill.description,\n\t\t\t\tsource: \"skill\",\n\t\t\t\tsourceInfo: skill.sourceInfo,\n\t\t\t}));\n\n\t\t\treturn [...extensionCommands, ...templates, ...skills];\n\t\t};\n\n\t\trunner.bindCore(\n\t\t\t{\n\t\t\t\tsendMessage: (message, options) => {\n\t\t\t\t\tthis.sendCustomMessage(message, options).catch((err) => {\n\t\t\t\t\t\trunner.emitError({\n\t\t\t\t\t\t\textensionPath: \"<runtime>\",\n\t\t\t\t\t\t\tevent: \"send_message\",\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tsendUserMessage: (content, options) => {\n\t\t\t\t\tthis.sendUserMessage(content, options).catch((err) => {\n\t\t\t\t\t\trunner.emitError({\n\t\t\t\t\t\t\textensionPath: \"<runtime>\",\n\t\t\t\t\t\t\tevent: \"send_user_message\",\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tappendEntry: (customType, data) => {\n\t\t\t\t\tthis.sessionManager.appendCustomEntry(customType, data);\n\t\t\t\t\tthis._recordEvidenceReceiptInvalidation(customType);\n\t\t\t\t},\n\t\t\t\tsetSessionName: (name) => {\n\t\t\t\t\tthis.setSessionName(name);\n\t\t\t\t},\n\t\t\t\tgetSessionName: () => {\n\t\t\t\t\treturn this.sessionManager.getSessionName();\n\t\t\t\t},\n\t\t\t\tsetLabel: (entryId, label) => {\n\t\t\t\t\tthis.sessionManager.appendLabelChange(entryId, label);\n\t\t\t\t},\n\t\t\t\tgetActiveTools: () => this.getActiveToolNames(),\n\t\t\t\tgetAllTools: () => this.getAllTools(),\n\t\t\t\tsetActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),\n\t\t\t\trefreshTools: () => this._refreshToolRegistry(),\n\t\t\t\tgetCommands,\n\t\t\t\tsetModel: async (model) => {\n\t\t\t\t\tif (!this.modelRegistry.hasConfiguredAuth(model)) return false;\n\t\t\t\t\tawait this.setModel(model);\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\tgetThinkingLevel: () => this.thinkingLevel,\n\t\t\t\tsetThinkingLevel: (level) => this.setThinkingLevel(level),\n\t\t\t\t// Optional: no in-process MCP client manager yet. Leave unbound so\n\t\t\t\t// ExtensionAPI.callMcpTool remains present (load-time capture) but throws\n\t\t\t\t// until a session-level handler is provided. Tests / future MCP hub bind here.\n\t\t\t\tcallMcpTool: undefined,\n\t\t\t},\n\t\t\t{\n\t\t\t\tgetModel: () => this.model,\n\t\t\t\tisIdle: () => !this.isStreaming,\n\t\t\t\tgetSignal: () => this.agent.signal,\n\t\t\t\tabort: () => {\n\t\t\t\t\tif (this._extensionAbortHandler) {\n\t\t\t\t\t\tthis._extensionAbortHandler();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tvoid this.abort();\n\t\t\t\t},\n\t\t\t\thasPendingMessages: () => this.pendingMessageCount > 0,\n\t\t\t\tshutdown: () => {\n\t\t\t\t\tthis._extensionShutdownHandler?.();\n\t\t\t\t},\n\t\t\t\tgetContextUsage: () => this.getContextUsage(),\n\t\t\t\tcompact: (options) => {\n\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await this.compact(options?.customInstructions);\n\t\t\t\t\t\t\toptions?.onComplete?.(result);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t\t\toptions?.onError?.(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t\tgetSystemPrompt: () => this.systemPrompt,\n\t\t\t\tgetSystemPromptOptions: () => this._baseSystemPromptOptions,\n\t\t\t},\n\t\t\t{\n\t\t\t\tregisterProvider: (name, config) => {\n\t\t\t\t\tthis._modelRegistry.registerProvider(name, config);\n\t\t\t\t\tthis._refreshCurrentModelFromRegistry();\n\t\t\t\t},\n\t\t\t\tunregisterProvider: (name) => {\n\t\t\t\t\tthis._modelRegistry.unregisterProvider(name);\n\t\t\t\t\tthis._refreshCurrentModelFromRegistry();\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Connect configured MCP servers and register their tools for this session.\n\t *\n\t * Connection is lazy by construction: nothing is spawned until this is called,\n\t * and a server that fails to start is reported in the returned status instead\n\t * of taking the session down. Calling it twice replaces the previously\n\t * attached MCP tools rather than duplicating them.\n\t *\n\t * Returns per-server status so a caller can surface failures; an empty\n\t * configuration returns an empty array without spawning anything.\n\t */\n\tasync attachMcpServers(options?: {\n\t\tservers?: readonly McpServerConfig[];\n\t\tcallTimeoutMs?: number;\n\t}): Promise<McpServerStatus[]> {\n\t\tconst servers = options?.servers ?? loadMcpServerConfigs(this._cwd);\n\t\tthis._mcpManager?.close();\n\t\tthis._mcpManager = undefined;\n\t\tthis._customTools = this._customTools.filter((definition) => !this._mcpToolNames.has(definition.name));\n\t\tthis._mcpToolNames = new Set();\n\t\tif (servers.length === 0) {\n\t\t\tthis._refreshToolRegistry();\n\t\t\treturn [];\n\t\t}\n\n\t\tconst manager = new McpManager({\n\t\t\tservers,\n\t\t\tcwd: this._cwd,\n\t\t\tclientInfo: { name: APP_NAME, version: VERSION },\n\t\t\tcallTimeoutMs: options?.callTimeoutMs,\n\t\t});\n\t\tthis._mcpManager = manager;\n\t\tconst definitions = await manager.listToolDefinitions();\n\t\t// A builtin always wins a name collision; MCP must never shadow `bash`.\n\t\tconst usable = definitions.filter((definition) => !this._baseToolDefinitions.has(definition.name));\n\t\tthis._mcpToolNames = new Set(usable.map((definition) => definition.name));\n\t\tthis._customTools = [...this._customTools, ...(usable as ToolDefinition[])];\n\t\tthis._refreshToolRegistry();\n\t\treturn manager.status();\n\t}\n\n\t/** Status of MCP servers attached to this session. Empty when none were attached. */\n\tmcpServerStatus(): McpServerStatus[] {\n\t\treturn this._mcpManager?.status() ?? [];\n\t}\n\n\t/**\n\t * Ping attached MCP servers and return fresh status, so callers (e.g. the\n\t * status rail) can keep displaying real connectivity instead of the last\n\t * handshake result. Resolves to [] when no manager is attached.\n\t */\n\tmcpCheckHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\treturn this._mcpManager?.checkHealth(options) ?? Promise.resolve([]);\n\t}\n\n\tprivate _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {\n\t\tconst previousRegistryNames = new Set(this._toolRegistry.keys());\n\t\tconst previousActiveToolNames = this.getActiveToolNames();\n\t\tconst allowedToolNames = this._allowedToolNames;\n\t\tconst excludedToolNames = this._excludedToolNames;\n\t\tconst isAllowedTool = (name: string): boolean =>\n\t\t\t(!allowedToolNames || allowedToolNames.has(name)) && !excludedToolNames?.has(name);\n\n\t\tconst registeredTools = this._extensionRunner.getAllRegisteredTools();\n\t\tconst allCustomTools = [\n\t\t\t...registeredTools,\n\t\t\t...this._customTools.map((definition) => ({\n\t\t\t\tdefinition,\n\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: \"sdk\" }),\n\t\t\t})),\n\t\t].filter((tool) => isAllowedTool(tool.definition.name));\n\t\tif (this._loadoutAccessPolicy) {\n\t\t\tconst shadowedBuiltins = normalizeToolNames(\n\t\t\t\tallCustomTools.map((tool) => tool.definition.name).filter((name) => this._baseToolDefinitions.has(name)),\n\t\t\t);\n\t\t\tif (shadowedBuiltins.length > 0) {\n\t\t\t\tthrow new Error(`loadout extension tool shadows builtin: ${shadowedBuiltins.join(\", \")}`);\n\t\t\t}\n\t\t}\n\n\t\tconst definitionRegistry = new Map<string, ToolDefinitionEntry>(\n\t\t\tArray.from(this._baseToolDefinitions.entries()).flatMap(([name, definition]) =>\n\t\t\t\tisAllowedTool(name)\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdefinition,\n\t\t\t\t\t\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: \"builtin\" }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t] as [string, ToolDefinitionEntry],\n\t\t\t\t\t\t]\n\t\t\t\t\t: [],\n\t\t\t),\n\t\t);\n\t\tfor (const tool of allCustomTools) {\n\t\t\tdefinitionRegistry.set(tool.definition.name, {\n\t\t\t\tdefinition: tool.definition,\n\t\t\t\tsourceInfo: tool.sourceInfo,\n\t\t\t});\n\t\t}\n\t\tthis._toolDefinitions = definitionRegistry;\n\t\tthis._toolPromptSnippets = new Map(\n\t\t\tArray.from(definitionRegistry.values()).flatMap(({ definition }) => {\n\t\t\t\tconst snippet = this._normalizePromptSnippet(definition.promptSnippet);\n\t\t\t\treturn snippet ? [[definition.name, snippet] as const] : [];\n\t\t\t}),\n\t\t);\n\t\tthis._toolPromptGuidelines = new Map(\n\t\t\tArray.from(definitionRegistry.values()).flatMap(({ definition }) => {\n\t\t\t\tconst guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);\n\t\t\t\treturn guidelines.length > 0 ? [[definition.name, guidelines] as const] : [];\n\t\t\t}),\n\t\t);\n\t\t// Tools contributed by extensions the harness itself loads (`<builtin:*>`),\n\t\t// as opposed to user/project/SDK extensions.\n\t\tconst builtinExtensionToolNames = new Set(\n\t\t\tallCustomTools.flatMap((tool) => (tool.sourceInfo.path.startsWith(\"<builtin:\") ? [tool.definition.name] : [])),\n\t\t);\n\t\tconst runner = this._extensionRunner;\n\t\tconst wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);\n\t\tconst wrappedBuiltInTools = wrapRegisteredTools(\n\t\t\tArray.from(this._baseToolDefinitions.values()).flatMap((definition) =>\n\t\t\t\tisAllowedTool(definition.name)\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdefinition,\n\t\t\t\t\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, {\n\t\t\t\t\t\t\t\t\tsource: \"builtin\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: [],\n\t\t\t),\n\t\t\trunner,\n\t\t);\n\n\t\tconst toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));\n\t\tfor (const tool of wrappedExtensionTools as AgentTool[]) {\n\t\t\ttoolRegistry.set(tool.name, tool);\n\t\t}\n\t\tthis._toolRegistry = toolRegistry;\n\n\t\tif (this._loadoutAccessPolicy) {\n\t\t\tconst missingLockedTools = this._loadoutAccessPolicy.activeTools.filter(\n\t\t\t\t(toolName) => !isAllowedTool(toolName) || !this._toolRegistry.has(toolName),\n\t\t\t);\n\t\t\tif (missingLockedTools.length > 0) {\n\t\t\t\tthrow new Error(`loadout locked tool unavailable: ${missingLockedTools.join(\", \")}`);\n\t\t\t}\n\t\t\tthis.setActiveToolsByName([...this._loadoutAccessPolicy.activeTools]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst nextActiveToolNames = (\n\t\t\toptions?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]\n\t\t).filter((name) => isAllowedTool(name));\n\n\t\tif (allowedToolNames) {\n\t\t\tfor (const toolName of this._toolRegistry.keys()) {\n\t\t\t\tif (allowedToolNames.has(toolName)) {\n\t\t\t\t\tnextActiveToolNames.push(toolName);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (options?.includeAllExtensionTools) {\n\t\t\t// \"Keep extension tools on\" is about the *user's* extensions. Tools shipped\n\t\t\t// by a built-in extension (e.g. update_todo) are part of the harness's own\n\t\t\t// tool surface, so they follow the built-in policy: when the caller asked\n\t\t\t// for no built-in tools, they stay off too.\n\t\t\tconst builtinToolsDisabled = options.activeToolNames?.length === 0;\n\t\t\tfor (const tool of wrappedExtensionTools) {\n\t\t\t\tif (builtinToolsDisabled && builtinExtensionToolNames.has(tool.name)) continue;\n\t\t\t\tnextActiveToolNames.push(tool.name);\n\t\t\t}\n\t\t} else if (!options?.activeToolNames) {\n\t\t\tfor (const toolName of this._toolRegistry.keys()) {\n\t\t\t\tif (!previousRegistryNames.has(toolName)) {\n\t\t\t\t\tnextActiveToolNames.push(toolName);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.setActiveToolsByName([...new Set(nextActiveToolNames)]);\n\t}\n\n\tprivate _buildRuntime(options: {\n\t\tactiveToolNames?: string[];\n\t\tflagValues?: Map<string, boolean | string>;\n\t\tincludeAllExtensionTools?: boolean;\n\t}): void {\n\t\tconst autoResizeImages = this.settingsManager.getImageAutoResize();\n\t\tconst shellCommandPrefix = this.settingsManager.getShellCommandPrefix();\n\t\tconst shellPath = this.settingsManager.getShellPath();\n\t\tconst loadoutAccessPolicy = this._loadoutAccessPolicy;\n\t\tconst loadoutAccessGuard = loadoutAccessPolicy\n\t\t\t? (request: Parameters<typeof decideLoadoutAccess>[1]) => decideLoadoutAccess(loadoutAccessPolicy, request)\n\t\t\t: undefined;\n\t\tconst loadoutReadOptions = loadoutAccessGuard\n\t\t\t? {\n\t\t\t\t\tcanReadPath: (path: string) => loadoutAccessGuard({ operation: \"read\", toolName: \"read\", path }).allowed,\n\t\t\t\t}\n\t\t\t: {};\n\t\tconst loadoutWriteOptions = loadoutAccessGuard\n\t\t\t? {\n\t\t\t\t\tcanWritePath: (path: string) =>\n\t\t\t\t\t\tloadoutAccessGuard({ operation: \"write\", toolName: \"write\", path }).allowed,\n\t\t\t\t}\n\t\t\t: {};\n\t\tconst baseToolDefinitions = (() => {\n\t\t\tif (this._baseToolsOverride) {\n\t\t\t\treturn Object.fromEntries(\n\t\t\t\t\tObject.entries(this._baseToolsOverride).map(([name, tool]) => [\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tcreateToolDefinitionFromAgentTool(tool),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn createAllToolDefinitions(this._cwd, {\n\t\t\t\tread: { autoResizeImages, ...loadoutReadOptions },\n\t\t\t\tbash: {\n\t\t\t\t\tcommandPrefix: shellCommandPrefix,\n\t\t\t\t\tshellPath,\n\t\t\t\t\t...(loadoutAccessGuard ? { loadoutAccessGuard } : {}),\n\t\t\t\t\t...(() => {\n\t\t\t\t\t\tconst sandboxPolicy = this._getBashSandboxPreflight();\n\t\t\t\t\t\treturn sandboxPolicy ? { sandboxPolicy } : {};\n\t\t\t\t\t})(),\n\t\t\t\t\t...(() => {\n\t\t\t\t\t\tconst evidenceExecutor = this._getVerifiedEvidenceExecutor();\n\t\t\t\t\t\tif (!evidenceExecutor || !this._replayGoalId) return {};\n\t\t\t\t\t\tconst { shell } = getShellConfig(shellPath);\n\t\t\t\t\t\tconst inner = createLocalBashOperations({\n\t\t\t\t\t\t\t...(shellPath !== undefined ? { shellPath } : {}),\n\t\t\t\t\t\t\tsandboxPolicy: this._getBashSandboxPreflight(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\toperations: createVerifiedBashOperations(inner, {\n\t\t\t\t\t\t\t\tevidenceExecutor,\n\t\t\t\t\t\t\t\tgoalId: this._replayGoalId,\n\t\t\t\t\t\t\t\t...(this._replayLaneId !== undefined ? { laneId: this._replayLaneId } : {}),\n\t\t\t\t\t\t\t\tshell,\n\t\t\t\t\t\t\t\tworkspaceScope: resolveSessionWorkspaceScope(this._cwd),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t};\n\t\t\t\t\t})(),\n\t\t\t\t},\n\t\t\t\tedit: loadoutWriteOptions,\n\t\t\t\twrite: loadoutWriteOptions,\n\t\t\t});\n\t\t})();\n\n\t\tthis._baseToolDefinitions = new Map(\n\t\t\tObject.entries(baseToolDefinitions).map(([name, tool]) => [name, tool as ToolDefinition]),\n\t\t);\n\n\t\tconst extensionsResult = this._resourceLoader.getExtensions();\n\t\tif (options.flagValues) {\n\t\t\tfor (const [name, value] of options.flagValues) {\n\t\t\t\textensionsResult.runtime.flagValues.set(name, value);\n\t\t\t}\n\t\t}\n\n\t\tthis._extensionRunner = new ExtensionRunner(\n\t\t\textensionsResult.extensions,\n\t\t\textensionsResult.runtime,\n\t\t\tthis._cwd,\n\t\t\tthis.sessionManager,\n\t\t\tthis._modelRegistry,\n\t\t);\n\t\tif (this._extensionRunnerRef) {\n\t\t\tthis._extensionRunnerRef.current = this._extensionRunner;\n\t\t}\n\t\tthis._bindExtensionCore(this._extensionRunner);\n\t\tthis._applyExtensionBindings(this._extensionRunner);\n\n\t\tconst defaultActiveToolNames = this._baseToolsOverride\n\t\t\t? Object.keys(this._baseToolsOverride)\n\t\t\t: [\"read\", \"bash\", \"edit\", \"write\", \"diagnostics\"];\n\t\tconst baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;\n\t\tthis._refreshToolRegistry({\n\t\t\tactiveToolNames: baseActiveToolNames,\n\t\t\tincludeAllExtensionTools: options.includeAllExtensionTools,\n\t\t});\n\t}\n\n\tasync reload(): Promise<void> {\n\t\tconst previousFlagValues = this._extensionRunner.getFlagValues();\n\t\tawait emitSessionShutdownEvent(this._extensionRunner, { type: \"session_shutdown\", reason: \"reload\" });\n\t\tawait this.settingsManager.reload();\n\t\tthis._invalidateContextBudgetCache({ type: \"settings\" });\n\t\tresetApiProviders();\n\t\tawait this._resourceLoader.reload();\n\t\tthis._buildRuntime({\n\t\t\tactiveToolNames: this.getActiveToolNames(),\n\t\t\tflagValues: previousFlagValues,\n\t\t\tincludeAllExtensionTools: true,\n\t\t});\n\n\t\tconst hasBindings =\n\t\t\tthis._extensionUIContext ||\n\t\t\tthis._extensionCommandContextActions ||\n\t\t\tthis._extensionShutdownHandler ||\n\t\t\tthis._extensionErrorListener;\n\t\tif (hasBindings) {\n\t\t\tawait this._extensionRunner.emit({ type: \"session_start\", reason: \"reload\" });\n\t\t\tawait this.extendResourcesFromExtensions(\"reload\");\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Auto-Retry\n\t// =========================================================================\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tprivate _isRetryableError(message: AssistantMessage): boolean {\n\t\treturn isRetryableAssistantError(message, this.model?.contextWindow ?? 0);\n\t}\n\n\t/**\n\t * Retry policy + callbacks shared by compaction and branch-summary summarization calls.\n\t * Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient\n\t * stream drop no longer fails the whole operation. `source` carries the context\n\t * the TUI needs to render the retry and recreate the underlying indicator.\n\t */\n\tprivate _summarizationRetryCallbacks(\n\t\tsource: { source: \"branchSummary\" } | { source: \"compaction\"; reason: \"manual\" | \"threshold\" | \"overflow\" },\n\t): RetryCallbacks {\n\t\treturn {\n\t\t\tonRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"summarization_retry_scheduled\",\n\t\t\t\t\tattempt,\n\t\t\t\t\tmaxAttempts,\n\t\t\t\t\tdelayMs,\n\t\t\t\t\terrorMessage,\n\t\t\t\t});\n\t\t\t},\n\t\t\tonRetryAttemptStart: () => {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"summarization_retry_attempt_start\",\n\t\t\t\t\t...source,\n\t\t\t\t});\n\t\t\t},\n\t\t\tonRetryFinished: () => {\n\t\t\t\tthis._emit({ type: \"summarization_retry_finished\" });\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Prepare a retryable error for continuation with exponential backoff.\n\t * @returns true if the caller should continue the agent, false otherwise\n\t */\n\t/** Eject sticky safety model at prompt boundary (session resume / leftover default). */\n\tprivate async _ejectStickySafetyModelIfNeeded(): Promise<void> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\t!shouldEjectStickySafetyModel({\n\t\t\t\tblockStickySafetyModels: resilience.blockStickySafetyModels,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t})\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tconst current = this.model;\n\t\tif (!current || !isStickySafetyModel(current.id, current.provider)) return;\n\n\t\tconst pick = pickFailoverCandidate(\n\t\t\tresilience.failoverCandidates,\n\t\t\t{ provider: current.provider, id: current.id },\n\t\t\t(c) => {\n\t\t\t\tconst next = this._modelRegistry.find(c.provider, c.id);\n\t\t\t\tif (!next) return false;\n\t\t\t\treturn this._modelRegistry.hasConfiguredAuth(next);\n\t\t\t},\n\t\t);\n\t\tif (!pick) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(current.id, current.provider));\n\t\t}\n\t\tconst next = this._modelRegistry.find(pick.provider, pick.id);\n\t\tif (!next) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(current.id, current.provider));\n\t\t}\n\t\t// Bypass setModel sticky check by temporarily allowing via direct state switch path:\n\t\t// setModel itself blocks sticky targets, not sources — safe.\n\t\tawait this.setModel(next);\n\t}\n\n\t/**\n\t * Content/safety stops (stop_reason=refusal) are often false positives on coding turns\n\t * for Fable AND Claude Opus/Sonnet. Retrying the same model usually reprints the refusal —\n\t * switch via provider-resilience chain first. Failover targets still skip sticky models.\n\t */\n\tprivate async _maybeFailoverFromSafetyStop(message: AssistantMessage): Promise<string | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\t!shouldHonorSafetyFailover({\n\t\t\t\tautoFailoverOnSafetyStop: resilience.autoFailoverOnSafetyStop,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t\tmodelId: this.model?.id,\n\t\t\t\tprovider: this.model?.provider,\n\t\t\t})\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\t// Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the\n\t\t// current model cannot finish this turn and same-model retry is useless.\n\t\tif (!isFailoverTriggerError(message.errorMessage)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst current = this.model;\n\t\t// v10.0-Ω: do NOT gate on isStickySafetyModel(source).\n\t\t// claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.\n\n\t\t// v10.3-Ω: mark the model that just refused so we never re-pick it this turn.\n\t\tif (current) this._refusedModels.add(failoverModelKey(current.provider, current.id));\n\n\t\tconst pick = pickFailoverCandidate(\n\t\t\tresilience.failoverCandidates,\n\t\t\tcurrent ? { provider: current.provider, id: current.id } : undefined,\n\t\t\t(c) => {\n\t\t\t\t// Skip any model that already refused/failed this turn → advance the chain.\n\t\t\t\tif (this._refusedModels.has(failoverModelKey(c.provider, c.id))) return false;\n\t\t\t\tconst next = this._modelRegistry.find(c.provider, c.id);\n\t\t\t\tif (!next) return false;\n\t\t\t\treturn this._modelRegistry.hasConfiguredAuth(next);\n\t\t\t},\n\t\t);\n\t\tif (!pick) return undefined;\n\n\t\tconst next = this._modelRegistry.find(pick.provider, pick.id);\n\t\tif (!next) return undefined;\n\t\ttry {\n\t\t\tawait this.setModel(next);\n\t\t\treturn `${next.provider}/${next.id}`;\n\t\t} catch {\n\t\t\t// setModel failed (auth/guard) — blacklist so next retry advances further.\n\t\t\tthis._refusedModels.add(failoverModelKey(pick.provider, pick.id));\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async _prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getRetrySettings();\n\t\tconst maxRetries = retryBudgetForAssistantError(message, settings.maxRetries);\n\t\tconst attempt = nextRetryAttempt({\n\t\t\tenabled: settings.enabled,\n\t\t\tcompletedAttempts: this._retryAttempt,\n\t\t\tmaxRetries,\n\t\t});\n\t\tif (attempt === undefined) return false;\n\t\tthis._retryAttempt = attempt;\n\n\t\t// Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.\n\t\tconst failoverTo = await this._maybeFailoverFromSafetyStop(message);\n\t\t// Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.\n\t\tconst delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, failoverTo !== undefined);\n\t\tconst errorMessage = failoverTo\n\t\t\t? `${message.errorMessage || \"content/safety stop\"} → failover ${failoverTo}`\n\t\t\t: message.errorMessage || \"Unknown error\";\n\n\t\tthis._emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt,\n\t\t\tmaxAttempts: maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage,\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._retryAbortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._retryAbortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._retryAttempt;\n\t\t\tthis._retryAttempt = 0;\n\t\t\tthis._refusedModels.clear();\n\t\t\tthis._emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._retryAbortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabortRetry(): void {\n\t\tthis._retryAbortController?.abort();\n\t}\n\n\t/** Whether auto-retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._retryAbortController !== undefined;\n\t}\n\n\t/** Whether auto-retry is enabled */\n\tget autoRetryEnabled(): boolean {\n\t\treturn this.settingsManager.getRetryEnabled();\n\t}\n\n\t/**\n\t * Toggle auto-retry setting.\n\t */\n\tsetAutoRetryEnabled(enabled: boolean): void {\n\t\tthis.settingsManager.setRetryEnabled(enabled);\n\t}\n\n\t// =========================================================================\n\t// Bash Execution\n\t// =========================================================================\n\n\t/**\n\t * Execute a bash command.\n\t * Adds result to agent context and session.\n\t * @param command The bash command to execute\n\t * @param onChunk Optional streaming callback for output\n\t * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)\n\t * @param options.operations Custom BashOperations for remote execution\n\t * @param options.safetyGate When \"headless\", pre-classify the command and deny confirm/block-tier verdicts without interactive confirmation\n\t * @param options.sandboxPolicy Trusted sandbox preflight for local bash execution (never sourced from RPC payloads)\n\t */\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\treturn this._bashService.executeBash(command, onChunk, options);\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tthis._bashService.recordBashResult(command, result, options);\n\t}\n\n\t/**\n\t * Cancel running bash command.\n\t */\n\tabortBash(): void {\n\t\tthis._bashService.abortBash();\n\t}\n\n\t/** Whether a bash command is currently running */\n\tget isBashRunning(): boolean {\n\t\treturn this._bashService.isBashRunning;\n\t}\n\n\t/** Whether there are pending bash messages waiting to be flushed */\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this._bashService.hasPendingBashMessages;\n\t}\n\n\t/**\n\t * Flush pending bash messages to agent state and session.\n\t * Called after agent turn completes to maintain proper message ordering.\n\t */\n\tprivate _flushPendingBashMessages(): void {\n\t\tthis._bashService.flushPending();\n\t}\n\n\t// =========================================================================\n\t// Session Management\n\t// =========================================================================\n\n\t/**\n\t * Set a display name for the current session.\n\t */\n\tsetSessionName(name: string): void {\n\t\tthis.sessionManager.appendSessionInfo(name);\n\t\tthis._emit({ type: \"session_info_changed\", name: this.sessionManager.getSessionName() });\n\t}\n\n\t// =========================================================================\n\t// Tree Navigation\n\t// =========================================================================\n\n\t/**\n\t * Navigate to a different node in the session tree.\n\t * Unlike fork() which creates a new session file, this stays in the same file.\n\t *\n\t * @param targetId The entry ID to navigate to\n\t * @param options.summarize Whether user wants to summarize abandoned branch\n\t * @param options.customInstructions Custom instructions for summarizer\n\t * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n\t * @param options.label Label to attach to the branch summary entry\n\t * @returns Result with editorText (if user message) and cancelled status\n\t */\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string } = {},\n\t): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }> {\n\t\tconst oldLeafId = this.sessionManager.getLeafId();\n\n\t\t// No-op if already at target\n\t\tif (targetId === oldLeafId) {\n\t\t\treturn { cancelled: false };\n\t\t}\n\n\t\t// Model required for summarization\n\t\tif (options.summarize && !this.model) {\n\t\t\tthrow new Error(\"No model available for summarization\");\n\t\t}\n\n\t\tconst targetEntry = this.sessionManager.getEntry(targetId);\n\t\tif (!targetEntry) {\n\t\t\tthrow new Error(`Entry ${targetId} not found`);\n\t\t}\n\n\t\t// Collect entries to summarize (from old leaf to common ancestor)\n\t\tconst { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(\n\t\t\tthis.sessionManager,\n\t\t\toldLeafId,\n\t\t\ttargetId,\n\t\t);\n\n\t\t// Prepare event data - mutable so extensions can override\n\t\tlet customInstructions = options.customInstructions;\n\t\tlet replaceInstructions = options.replaceInstructions;\n\t\tlet label = options.label;\n\n\t\tconst preparation: TreePreparation = {\n\t\t\ttargetId,\n\t\t\toldLeafId,\n\t\t\tcommonAncestorId,\n\t\t\tentriesToSummarize,\n\t\t\tuserWantsSummary: options.summarize ?? false,\n\t\t\tcustomInstructions,\n\t\t\treplaceInstructions,\n\t\t\tlabel,\n\t\t};\n\n\t\t// Set up abort controller for summarization\n\t\tthis._branchSummaryAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tlet extensionSummary: { summary: string; details?: unknown } | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\t// Emit session_before_tree event\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_tree\")) {\n\t\t\t\tconst result = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_tree\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t})) as SessionBeforeTreeResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\n\t\t\t\tif (result?.summary && options.summarize) {\n\t\t\t\t\textensionSummary = result.summary;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\n\t\t\t\t// Allow extensions to override instructions and label\n\t\t\t\tif (result?.customInstructions !== undefined) {\n\t\t\t\t\tcustomInstructions = result.customInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.replaceInstructions !== undefined) {\n\t\t\t\t\treplaceInstructions = result.replaceInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.label !== undefined) {\n\t\t\t\t\tlabel = result.label;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Run default summarizer if needed\n\t\t\tlet summaryText: string | undefined;\n\t\t\tlet summaryDetails: unknown;\n\t\t\tif (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {\n\t\t\t\tconst model = this.model;\n\t\t\t\tif (!model) throw new Error(\"No model available for summarization\");\n\t\t\t\tconst { apiKey, headers } = await this._getRequiredRequestAuth(model);\n\t\t\t\tconst branchSummarySettings = this.settingsManager.getBranchSummarySettings();\n\t\t\t\tconst result = await generateBranchSummary(entriesToSummarize, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\treplaceInstructions,\n\t\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\t\tstreamFn: this.agent.streamFn,\n\t\t\t\t\tretry: this.settingsManager.getRetrySettings(),\n\t\t\t\t\tcallbacks: this._summarizationRetryCallbacks({ source: \"branchSummary\" }),\n\t\t\t\t});\n\t\t\t\tif (result.aborted) {\n\t\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t\t}\n\t\t\t\tif (result.error) {\n\t\t\t\t\tthrow new Error(result.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = result.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t\t};\n\t\t\t} else if (extensionSummary) {\n\t\t\t\tsummaryText = extensionSummary.summary;\n\t\t\t\tsummaryDetails = extensionSummary.details;\n\t\t\t}\n\n\t\t\t// Determine the new leaf position based on target type\n\t\t\tlet newLeafId: string | null;\n\t\t\tlet editorText: string | undefined;\n\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText = this._extractUserMessageText(targetEntry.message.content);\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\tif (typeof targetEntry.content === \"string\") {\n\t\t\t\t\teditorText = targetEntry.content;\n\t\t\t\t} else {\n\t\t\t\t\teditorText = targetEntry.content\n\t\t\t\t\t\t.flatMap((item) => {\n\t\t\t\t\t\t\tif (item.type === \"text\") return [item.text];\n\t\t\t\t\t\t\treturn [];\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Non-user message: leaf = selected node\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\n\t\t\t// Switch leaf (with or without summary)\n\t\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\t\tif (summaryText) {\n\t\t\t\t// Create summary at target position (can be null for root)\n\t\t\t\tconst summaryId = this.sessionManager.branchWithSummary(\n\t\t\t\t\tnewLeafId,\n\t\t\t\t\tsummaryText,\n\t\t\t\t\tsummaryDetails,\n\t\t\t\t\tfromExtension,\n\t\t\t\t);\n\t\t\t\tsummaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t\t// Attach label to the summary entry\n\t\t\t\tif (label) {\n\t\t\t\t\tthis.sessionManager.appendLabelChange(summaryId, label);\n\t\t\t\t}\n\t\t\t} else if (newLeafId === null) {\n\t\t\t\t// No summary, navigating to root - reset leaf\n\t\t\t\tthis.sessionManager.resetLeaf();\n\t\t\t} else {\n\t\t\t\t// No summary, navigating to non-root\n\t\t\t\tthis.sessionManager.branch(newLeafId);\n\t\t\t}\n\n\t\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\t\tif (label && !summaryText) {\n\t\t\t\tthis.sessionManager.appendLabelChange(targetId, label);\n\t\t\t}\n\n\t\t\t// Update agent state\n\t\t\tconst sessionContext = this.sessionManager.buildSessionContext();\n\t\t\tthis.agent.state.messages = sessionContext.messages;\n\n\t\t\t// Emit session_tree event\n\t\t\tawait this._extensionRunner.emit({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: this.sessionManager.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromExtension: summaryText ? fromExtension : undefined,\n\t\t\t});\n\n\t\t\t// Emit to custom tools\n\n\t\t\treturn { editorText, cancelled: false, summaryEntry };\n\t\t} finally {\n\t\t\tthis._branchSummaryAbortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Get all user messages from session for fork selector.\n\t */\n\tgetUserMessagesForForking(): Array<{ entryId: string; text: string }> {\n\t\tconst entries = this.sessionManager.getEntries();\n\t\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type !== \"message\") continue;\n\t\t\tif (entry.message.role !== \"user\") continue;\n\n\t\t\tconst text = this._extractUserMessageText(entry.message.content);\n\t\t\tif (text) {\n\t\t\t\tresult.push({ entryId: entry.id, text });\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\t\tif (typeof content === \"string\") return content;\n\t\tif (Array.isArray(content)) {\n\t\t\treturn content\n\t\t\t\t.flatMap((item) => (item.type === \"text\" && typeof item.text === \"string\" ? [item.text] : []))\n\t\t\t\t.join(\"\");\n\t\t}\n\t\treturn \"\";\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.state;\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst providerEligibleInputTokens = totalInput + totalCacheRead + totalCacheWrite;\n\t\treturn {\n\t\t\tsessionFile: this.sessionFile,\n\t\t\tsessionId: this.sessionId,\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tpromptCache: {\n\t\t\t\tproviderEligibleInputTokens,\n\t\t\t\tproviderHitRate: providerEligibleInputTokens > 0 ? totalCacheRead / providerEligibleInputTokens : 0,\n\t\t\t\tkeyChanges: this._promptCacheKeyChanges,\n\t\t\t\tboundaryBypasses: this._promptCacheBoundaryBypasses,\n\t\t\t\tstablePrefixCharacters: this._promptCacheStablePrefixCharacters,\n\t\t\t\t...(this._promptCacheLastBreakReason ? { lastBreakReason: this._promptCacheLastBreakReason } : {}),\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t};\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.model;\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.sessionManager.getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.messages);\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst themeName = this.settingsManager.getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: this.sessionManager.getCwd(),\n\t\t});\n\n\t\treturn exportSessionToHtml(this.sessionManager, this.state, {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: this.sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: this.sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = this.sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t// =========================================================================\n\t// Utilities\n\t// =========================================================================\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tlet lastAssistant: AssistantMessage | undefined;\n\t\tfor (let index = this.messages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = this.messages[index];\n\t\t\tif (message?.role !== \"assistant\") continue;\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (assistant.stopReason === \"aborted\" && assistant.content.length === 0) continue;\n\t\t\tlastAssistant = assistant;\n\t\t\tbreak;\n\t\t}\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of lastAssistant.content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n\n\t// =========================================================================\n\t// Extension System\n\t// =========================================================================\n\n\tcreateReplacedSessionContext(): ReplacedSessionContext {\n\t\tconst context = Object.defineProperties(\n\t\t\t{},\n\t\t\tObject.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext()),\n\t\t) as ReplacedSessionContext;\n\t\tcontext.sendMessage = (message, options) => this.sendCustomMessage(message, options);\n\t\tcontext.sendUserMessage = (content, options) => this.sendUserMessage(content, options);\n\t\treturn context;\n\t}\n\n\t/**\n\t * Check if extensions have handlers for a specific event type.\n\t */\n\thasExtensionHandlers(eventType: string): boolean {\n\t\treturn this._extensionRunner.hasHandlers(eventType);\n\t}\n\n\t/**\n\t * Get the extension runner (for setting UI context and error handlers).\n\t */\n\tget extensionRunner(): ExtensionRunner {\n\t\treturn this._extensionRunner;\n\t}\n}\n\nfunction parsePositiveIntegerEnv(name: string): number | undefined {\n\tconst raw = process.env[name];\n\tif (raw === undefined || raw.trim() === \"\") {\n\t\treturn undefined;\n\t}\n\tconst value = Number.parseInt(raw, 10);\n\treturn Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction parsePositiveFloatEnv(name: string): number | undefined {\n\tconst raw = process.env[name];\n\tif (raw === undefined || raw.trim() === \"\") {\n\t\treturn undefined;\n\t}\n\tconst value = Number.parseFloat(raw);\n\treturn Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction parseTokenizerModeEnv(\n\tvalue: string | undefined,\n): NonNullable<BuildSystemPromptOptions[\"contextBudget\"]>[\"tokenizerMode\"] {\n\tswitch (value) {\n\t\tcase \"fallback\":\n\t\tcase \"openai-js\":\n\t\tcase \"openai-wasm\":\n\t\tcase \"auto\":\n\t\t\treturn value;\n\t\tdefault:\n\t\t\treturn \"fallback\";\n\t}\n}\n\nfunction parseCommaSeparatedEnv(value: string | undefined): string[] {\n\tif (value === undefined || value.trim() === \"\") {\n\t\treturn [];\n\t}\n\treturn Array.from(\n\t\tnew Set(\n\t\t\tvalue\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((item) => item.trim())\n\t\t\t\t.filter((item) => item.length > 0),\n\t\t),\n\t);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../src/core/agent-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE5G,OAAO,KAAK,EAAyB,YAAY,EAAW,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAa/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAgB5E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQrD,OAAO,EACN,KAAK,gBAAgB,EASrB,MAAM,uBAAuB,CAAC;AAY/B,OAAO,EAGN,KAAK,gCAAgC,EAErC,MAAM,2CAA2C,CAAC;AAMnD,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,8BAA8B,EACnC,KAAK,sBAAsB,EAC3B,KAAK,aAAa,EAClB,eAAe,EACf,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAIhB,KAAK,sBAAsB,EAG3B,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EAInB,KAAK,QAAQ,EAKb,MAAM,uBAAuB,CAAC;AAI/B,OAAO,EAA+B,KAAK,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACrG,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAE3F,OAAO,EAAc,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4C1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,OAAO,EAEN,KAAK,kBAAkB,EAIvB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAgClF,OAAO,EAA2B,KAAK,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAElG,OAAO,KAAK,EAA0B,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAenF,OAAO,KAAK,EAAgD,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAE,KAAK,0BAA0B,EAAmB,MAAM,wBAAwB,CAAC;AAK1F,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI/E,OAAO,EAEN,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EAEvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAI7D,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAA6B,MAAM,iBAAiB,CAAC;AAM5G,OAAO,EAAuB,kBAAkB,EAAE,KAAK,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAMrH,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CASrE;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7C,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAC1B,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,GAC1C;IACA,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;CAClB,GACD,kBAAkB,GAClB;IACA,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,GACD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC1D;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GACxD;IACA,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;IAC5C,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACrB,GACD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACzG;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAClF;IAAE,IAAI,EAAE,uBAAuB,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7D;IACA,IAAI,EAAE,+BAA+B,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACpB,GACD;IAAE,IAAI,EAAE,mCAAmC,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GACtE;IACA,IAAI,EAAE,mCAAmC,CAAC;IAC1C,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;CAC3C,GACD;IAAE,IAAI,EAAE,8BAA8B,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,WAAW,EAAE,kBAAkB,CAAA;CAAE,GAChE;IACA;;;;;OAKG;IACH,IAAI,EAAE,oBAAoB,CAAC;IAC3B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;CACnD,CAAC;AAEL,iDAAiD;AACjD,MAAM,MAAM,yBAAyB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAM3E,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,+DAA+D;IAC/D,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAC3E,gFAAgF;IAChF,cAAc,EAAE,cAAc,CAAC;IAC/B,qDAAqD;IACrD,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,gEAAgE;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,6EAA6E;IAC7E,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC9C,iHAAiH;IACjH,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,kGAAkG;IAClG,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,sEAAsE;IACtE,kBAAkB,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,eAAe,CAAA;KAAE,CAAC;IACnD,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,2FAA2F;IAC3F,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IACjC,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,qBAAqB,CAAC,EAAE,8BAA8B,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,OAAO,CAAC,EAAE,sBAAsB,CAAC;CACjC;AAED,UAAU,kBAAkB;IAC3B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8FAA8F;IAC9F,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,aAAa,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,wCAAwC;AACxC,MAAM,WAAW,aAAa;IAC7B,oEAAoE;IACpE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,wBAAwB;IACxB,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,iHAAiH;IACjH,iBAAiB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACzC,qFAAqF;IACrF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0FAA0F;IAC1F,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAcD,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACd,CAAC;IACF,WAAW,EAAE;QACZ,2BAA2B,EAAE,MAAM,CAAC;QACpC,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;QACzB,sBAAsB,EAAE,MAAM,CAAC;QAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAsKD,qBAAa,YAAY;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAE1C,OAAO,CAAC,aAAa,CAA8D;IAEnF;;;OAGG;IACH,OAAO,CAAC,aAAa,CAA0B;IAE/C;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAAqB;IAE9C;;;;;;;;OAQG;IACH,OAAO,CAAC,4BAA4B,CAAmC;IACvE,OAAO,CAAC,kCAAkC,CAAS;IACnD,OAAO,CAAC,uBAAuB,CAAiC;IAEhE;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,6BAA6B,CAAS;IAG9C,OAAO,CAAC,iBAAiB,CAAC,CAAa;IACvC,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,uBAAuB,CAA6E;IAE5G,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB,CAAgB;IACzC,gFAAgF;IAChF,OAAO,CAAC,iBAAiB,CAAgB;IACzC,sFAAsF;IACtF,OAAO,CAAC,wBAAwB,CAAuB;IAGvD,OAAO,CAAC,0BAA0B,CAA0C;IAC5E,OAAO,CAAC,8BAA8B,CAA0C;IAChF,OAAO,CAAC,yBAAyB,CAAK;IACtC,OAAO,CAAC,6BAA6B,CAAS;IAC9C,OAAO,CAAC,0BAA0B,CAAgE;IAGlG,OAAO,CAAC,6BAA6B,CAA0C;IAG/E,OAAO,CAAC,qBAAqB,CAA0C;IACvE,OAAO,CAAC,aAAa,CAAK;IAG1B,OAAO,CAAC,cAAc,CAAqB;IAC3C,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IAGvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA2B;IAG9D,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,UAAU,CAAK;IAEvB,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,oBAAoB,CAA0C;IACtE,OAAO,CAAC,WAAW,CAAyB;IAC5C,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,8BAA8B,CAA4C;IAClF,OAAO,CAAC,mBAAmB,CAAC,CAAgC;IAC5D,OAAO,CAAC,uBAAuB,CAAC,CAAW;IAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAc;IACxC,OAAO,CAAC,kBAAkB,CAAC,CAAc;IACzC,OAAO,CAAC,kBAAkB,CAAC,CAA4B;IACvD,OAAO,CAAC,oBAAoB,CAAC,CAAsB;IACnD,OAAO,CAAC,kBAAkB,CAAoB;IAC9C,OAAO,CAAC,mBAAmB,CAAC,CAAqB;IACjD,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,+BAA+B,CAAC,CAAiC;IACzE,OAAO,CAAC,sBAAsB,CAAC,CAAa;IAC5C,OAAO,CAAC,yBAAyB,CAAC,CAAkB;IACpD,OAAO,CAAC,uBAAuB,CAAC,CAAyB;IACzD,OAAO,CAAC,2BAA2B,CAAC,CAAa;IAGjD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,uBAAuB,CAAgC;IAC/D,OAAO,CAAC,YAAY,CAAuB;IAG3C,OAAO,CAAC,wBAAwB,CAAyC;IACzE,OAAO,CAAC,qBAAqB,CAA2C;IACxE,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAiD;IAC9F,OAAO,CAAC,0BAA0B,CAAuB;IACzD,OAAO,CAAC,sBAAsB,CAA0C;IACxE,OAAO,CAAC,mBAAmB,CAAiC;IAC5D,OAAO,CAAC,+BAA+B,CAAsC;IAC7E,OAAO,CAAC,yBAAyB,CAEpB;IACb,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,iBAAiB,CAAsC;IAC/D,OAAO,CAAC,iBAAiB,CAAmC;IAC5D,OAAO,CAAC,uBAAuB,CAAK;IAGpC,OAAO,CAAC,cAAc,CAAgB;IACtC,uEAAuE;IACvE,OAAO,CAAC,2BAA2B,CAA2C;IAC9E,OAAO,CAAC,iCAAiC,CAAmC;IAG5E,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,gBAAgB,CAA+C;IACvE,OAAO,CAAC,mBAAmB,CAAkC;IAC7D,OAAO,CAAC,qBAAqB,CAAoC;IAGjE,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,8BAA8B,CAAqB;IAC3D,OAAO,CAAC,wBAAwB,CAA4B;IAC5D,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,sBAAsB,CAAK;IACnC,OAAO,CAAC,4BAA4B,CAAK;IACzC,OAAO,CAAC,kCAAkC,CAAK;IAC/C,OAAO,CAAC,2BAA2B,CAAqB;IAExD,YAAY,MAAM,EAAE,kBAAkB,EAkHrC;IAED,gEAAgE;IAChE,IAAI,aAAa,IAAI,aAAa,CAEjC;IAED,+EAA+E;IAC/E,IAAI,gBAAgB,IAAI,uBAAuB,GAAG,SAAS,CAE1D;IAED,kFAAkF;IAClF,IAAI,iBAAiB,IAAI,SAAS,gBAAgB,EAAE,CAEnD;IAED,uEAAuE;IACvE,IAAI,eAAe,IAAI,kBAAkB,GAAG,SAAS,CAEpD;IAED,mFAAmF;IACnF,IAAI,0BAA0B,IAAI,0BAA0B,GAAG,IAAI,CAElE;IAED,6FAA6F;IAC7F,IAAI,gBAAgB,IAAI,QAAQ,GAAG,UAAU,CAE5C;IAED,0FAA0F;IAC1F,IAAI,sBAAsB,IAAI,MAAM,CAEnC;IAED,IAAI,gCAAgC,IAAI,gCAAgC,CAEvE;IAED,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,gCAAgC;IAIxC,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,kCAAkC;IAM1C,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,kBAAkB;IAW1B,2FAA2F;IAC3F,OAAO,CAAC,4BAA4B;IAIpC,gFAAgF;IAChF,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,uBAAuB;IA4B/B,OAAO,CAAC,uBAAuB;IA8D/B,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,sBAAsB;IA+C9B,OAAO,CAAC,wBAAwB;IAwChC;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IA+C7B,OAAO,CAAC,wBAAwB;YAIlB,uBAAuB;YA0BvB,yBAAyB;IAYvC;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAyD9B,qCAAqC;IACrC,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,qBAAqB,CAA2C;IAExE,4EAA4E;IAC5E,OAAO,CAAC,iBAAiB,CAwFvB;IAEF,OAAO,CAAC,uBAAuB;IAe/B,0CAA0C;IAC1C,OAAO,CAAC,mBAAmB;IAQ3B,8EAA8E;IAC9E,OAAO,CAAC,yBAAyB;IAWjC,OAAO,CAAC,wBAAwB;YAuBlB,mBAAmB;IAyFjC;;;;;;OAMG;IACH,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,iBAAiB;IAazB,qFAAqF;IACrF,OAAO,CAAC,kBAAkB;IA2C1B;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAUzD;IAED;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;;OAGG;IACH,OAAO,IAAI,IAAI,CAgCd;IAED;;;;;;;;;OASG;IACH,OAAO,CAAC,iCAAiC;IAkBzC,uBAAuB;IACvB,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,2DAA2D;IAC3D,IAAI,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAElC;IAED,6BAA6B;IAC7B,IAAI,aAAa,IAAI,aAAa,CAEjC;IAED,uFAAuF;IACvF,IAAI,YAAY,IAAI,YAAY,CAE/B;IAED,sDAAsD;IACtD,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,sFAAsF;IACtF,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,gDAAgD;IAChD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,kBAAkB,IAAI,MAAM,EAAE,CAE7B;IAED;;OAEG;IACH,WAAW,IAAI,QAAQ,EAAE,CAQxB;IAED,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAE1D;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CA8B9C;IAED;;;;;OAKG;IACH,OAAO,CAAC,iCAAiC;IAWzC,sEAAsE;IACtE,IAAI,YAAY,IAAI,OAAO,CAM1B;IAED,oEAAoE;IACpE,IAAI,QAAQ,IAAI,YAAY,EAAE,CAE7B;IAED,4BAA4B;IAC5B,IAAI,YAAY,IAAI,KAAK,GAAG,eAAe,CAE1C;IAED,6BAA6B;IAC7B,IAAI,YAAY,IAAI,KAAK,GAAG,eAAe,CAE1C;IAED,uEAAuE;IACvE,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,yBAAyB;IACzB,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,2CAA2C;IAC3C,IAAI,WAAW,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,qDAAqD;IACrD,IAAI,YAAY,IAAI,aAAa,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAEtF;IAED,uCAAuC;IACvC,eAAe,CAAC,YAAY,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,GAAG,IAAI,CAE/F;IAED,kCAAkC;IAClC,IAAI,eAAe,IAAI,aAAa,CAAC,cAAc,CAAC,CAEnD;IAED,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,0BAA0B;IAelC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IA0B5B,OAAO,CAAC,wBAAwB;IAuChC;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,oBAAoB;YA0Bd,eAAe;IAiC7B;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;YAuCnB,yBAAyB;IAwDvC,OAAO,CAAC,yBAAyB;IAmBjC,0FAAyF;IACzF,2BAA2B,CAC1B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,OAAO,uBAAuB,EAAE,qBAAqB,GAC3D,IAAI,CAQN;IAED,yFAAwF;IACxF,2BAA2B,IAAI,yBAAyB,GAAG,IAAI,CAE9D;IAED,4EAA2E;IACrE,uBAAuB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAO7D;IAED,+EAA8E;IAC9E,yBAAyB,IAAI,0BAA0B,GAAG,IAAI,CAE7D;IAED;;;;OAIG;IACH,IAAI,kBAAkB,IAAI,kBAAkB,CAG3C;YASa,0BAA0B;YAuE1B,mBAAmB;IA+BjC;;;;;;;;OAQG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6NjE;YAKa,2BAA2B;IA0BzC;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IA0B3B;;;;;;;OAOG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAahE;IAED;;;;;;OAMG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAanE;YAKa,WAAW;YAkBX,cAAc;IAc5B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAYhC;;;;;;;;;;;OAWG;IACG,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAClC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC,EACjF,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAA;KAAE,GAChF,OAAO,CAAC,IAAI,CAAC,CA+Bf;IAED;;;;;;OAMG;IACG,eAAe,CACpB,OAAO,EAAE,MAAM,GAAG,CAAC,WAAW,GAAG,YAAY,CAAC,EAAE,EAChD,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;KAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CA4Bf;IAED;;;;OAIG;IACH,UAAU,IAAI;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAQvD;IAED,wEAAwE;IACxE,IAAI,mBAAmB,IAAI,MAAM,CAEhC;IAED,gDAAgD;IAChD,mBAAmB,IAAI,SAAS,MAAM,EAAE,CAEvC;IAED,iDAAiD;IACjD,mBAAmB,IAAI,SAAS,MAAM,EAAE,CAEvC;IAED,IAAI,cAAc,IAAI,cAAc,CAEnC;IAED;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAED,uEAAuE;IACvE,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,kBAAkB,CAuBpE;YAMa,gBAAgB;IAe9B;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC/C;IAED;;;;;OAKG;IACG,UAAU,CAAC,SAAS,GAAE,SAAS,GAAG,UAAsB,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAKrG;YAEa,iBAAiB;YA4BjB,oBAAoB;IA2BlC;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAsB3C;IAED,8EAA8E;IAC9E,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAS/C;IAED;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAExC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,8BAA8B;IAOtC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,OAAO,CAAC,yBAAyB;IAiEjC,OAAO,CAAC,iCAAiC;IAOzC,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,2BAA2B;IAgBnC;;;OAGG;IACH,OAAO,CAAC,+BAA+B;IAiBvC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAW9B;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAkDzB;;;OAGG;IACH,kBAAkB,IAAI,aAAa,GAAG,SAAS,CAe9C;IAED;;;OAGG;IACH,0BAA0B,IAAI,aAAa,EAAE,CAG5C;IAED;;OAEG;IACH,gBAAgB,IAAI,OAAO,CAE1B;IAED,OAAO,CAAC,+BAA+B;IAUvC,OAAO,CAAC,mBAAmB;IAQ3B;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAGnD;IAED;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAGnD;IAMD,OAAO,CAAC,2BAA2B;IAInC,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,yBAAyB;IAkCjC,OAAO,CAAC,2BAA2B;IAgBnC,OAAO,CAAC,0BAA0B;YAiBpB,uBAAuB;IASrC,OAAO,CAAC,oCAAoC;IAY5C,OAAO,CAAC,uBAAuB;IAoB/B;;;;;OAKG;IACH,OAAO,CAAC,yBAAyB;IAYjC;;;;OAIG;IACG,OAAO,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAkJpE;IAED;;OAEG;IACH,eAAe,IAAI,IAAI,CAGtB;IAED;;OAEG;IACH,kBAAkB,IAAI,IAAI,CAEzB;IAED,sFAAsF;IACtF,OAAO,CAAC,oBAAoB;IAU5B;;;;;OAKG;IACH,OAAO,CAAC,2BAA2B;YAYrB,yBAAyB;YA6CzB,gBAAgB;IAoF9B,OAAO,CAAC,2BAA2B;YAiBrB,kBAAkB;IAiNhC;;OAEG;IACH,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE/C;IAED,yCAAyC;IACzC,IAAI,qBAAqB,IAAI,OAAO,CAEnC;IAEK,cAAc,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB/D;YAEa,6BAA6B;IA4B3C,OAAO,CAAC,2BAA2B;IAmBnC,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,uBAAuB;IAU/B,OAAO,CAAC,gCAAgC;IAcxC,OAAO,CAAC,kBAAkB;IAuH1B;;;;;;;;;;OAUG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAChC,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;QACrC,aAAa,CAAC,EAAE,MAAM,CAAC;KACvB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAyB7B;IAED,qFAAqF;IACrF,eAAe,IAAI,eAAe,EAAE,CAEnC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAE1G;IAED,OAAO,CAAC,oBAAoB;IAkI5B,OAAO,CAAC,aAAa;IAoGf,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB5B;IAMD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;YA8BtB,+BAA+B;YA2C/B,uBAAuB;YA6BvB,4BAA4B;YAkD5B,aAAa;IAgE3B;;OAEG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED,kDAAkD;IAClD,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,oCAAoC;IACpC,IAAI,gBAAgB,IAAI,OAAO,CAE9B;IAED;;OAEG;IACH,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1C;IAMD;;;;;;;;;OASG;IACG,WAAW,CAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,EACjC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAErB;IAED;;;OAGG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,kBAAkB,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAEtG;IAED;;OAEG;IACH,SAAS,IAAI,IAAI,CAEhB;IAED,kDAAkD;IAClD,IAAI,aAAa,IAAI,OAAO,CAE3B;IAED,oEAAoE;IACpE,IAAI,sBAAsB,IAAI,OAAO,CAEpC;IAED;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IAQjC;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGjC;IAMD;;;;;;;;;;OAUG;IACG,YAAY,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAC/G,OAAO,CAAC;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC,CA4L5G;IAED;;OAEG;IACH,yBAAyB,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAepE;IAED,OAAO,CAAC,uBAAuB;IAU/B;;OAEG;IACH,eAAe,IAAI,YAAY,CAoD9B;IAED,eAAe,IAAI,YAAY,GAAG,SAAS,CA4C1C;IAED;;;;OAIG;IACG,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAevD;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CA+BzC;IAMD;;;;OAIG;IACH,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAsBzC;IAMD,4BAA4B,IAAI,sBAAsB,CAQrD;IAED;;OAEG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE/C;IAED;;OAEG;IACH,IAAI,eAAe,IAAI,eAAe,CAErC;CACD","sourcesContent":["/**\n * AgentSession - Core abstraction for agent lifecycle and session management.\n *\n * This class is shared between all run modes (interactive, print, rpc).\n * It encapsulates:\n * - Agent state access\n * - Event subscription with automatic session persistence\n * - Model and thinking level management\n * - Compaction (manual and auto)\n * - Bash execution\n * - Session switching and branching\n *\n * Modes use this class and add their own I/O layer on top.\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join as joinPath } from \"node:path\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from \"omk-agent-core\";\nimport { getVisionRouteModel } from \"omk-agent-core\";\nimport type { Api, AssistantMessage, ImageContent, Message, Model, TextContent } from \"omk-ai\";\nimport {\n\tclampThinkingLevel,\n\tcleanupSessionResources,\n\tderiveContextPromptCacheKey,\n\tgetSupportedThinkingLevels,\n\tisContextOverflow,\n\tmodelsAreEqual,\n\ttype RetryCallbacks,\n\tresetApiProviders,\n\tstreamSimple,\n} from \"omk-ai\";\nimport { APP_NAME, VERSION } from \"../config.ts\";\nimport type { ReplayLedgerManager } from \"../guardrails/evidence-system.ts\";\nimport type { VerifiedEvidenceExecutor } from \"../guardrails/verified-executor.ts\";\nimport { theme } from \"../modes/interactive/theme/theme.ts\";\nimport type { ReplayEventType } from \"../types/evidence.ts\";\nimport { stripFrontmatter } from \"../utils/frontmatter.ts\";\nimport { resolvePath } from \"../utils/paths.ts\";\nimport { getShellConfig } from \"../utils/shell.ts\";\nimport { sleep } from \"../utils/sleep.ts\";\nimport { type AdaptorchBridge, type AdaptorchConsultPayload, createAdaptorchBridge } from \"./adaptorch-bridge.ts\";\nimport {\n\tapplyCategoryTimeoutDefaults,\n\tresolveAgentToolSettings,\n\tresolveToolTimeoutCategory,\n} from \"./agent-tool-settings.ts\";\nimport { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from \"./auth-guidance.ts\";\nimport { parseBangInvocation } from \"./bang-skill-invocation.ts\";\nimport type { BashResult } from \"./bash-executor.ts\";\nimport { type CompactionSettings, getCompactionHeadroomThreshold } from \"./compaction/compaction.ts\";\nimport {\n\ttype CompactionHysteresisState,\n\tcreateCompactionHysteresisConfig,\n\tcreateCompactionHysteresisState,\n\tstepCompactionHysteresis,\n} from \"./compaction/hysteresis.ts\";\nimport {\n\ttype CompactionResult,\n\tcalculateContextTokens,\n\tcollectEntriesForBranchSummary,\n\tcompact,\n\testimateContextTokens,\n\testimateProjectedContextTokens,\n\tgenerateBranchSummary,\n\tprepareCompaction,\n\tresolveCompactionModel,\n} from \"./compaction/index.ts\";\nimport { compactionEmitWillRetry } from \"./compaction/resume-policy.ts\";\nimport { isSessionModelOverflow, shouldSkipCompactionCheck } from \"./compaction-gate.ts\";\nimport {\n\testimateToolResultReserve,\n\ttype ToolResultClass,\n\ttype ToolResultReserveRequest,\n} from \"./context-budget-reserved-tokens.ts\";\nimport {\n\tcreateDiskContextBudgetCacheProviderV2,\n\tDiskContextBudgetCacheProviderV2,\n} from \"./context-budget-v2-cache-disk.ts\";\nimport {\n\tapplyContextCacheInvalidation,\n\ttype ContextCacheInvalidationEvent,\n\ttype ContextCacheInvalidationSnapshot,\n\tcreateContextCacheInvalidationSnapshot,\n} from \"./context-budget-v2-cache-invalidation.ts\";\nimport { createMemoryContextBudgetCacheProviderV2 } from \"./context-budget-v2-cache-provider.ts\";\nimport type { ContextBudgetCacheProviderV2 } from \"./context-budget-v2-types.ts\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.ts\";\nimport { exportSessionToHtml, type ToolHtmlRenderer } from \"./export-html/index.ts\";\nimport { createToolHtmlRenderer } from \"./export-html/tool-renderer.ts\";\nimport {\n\ttype ContextUsage,\n\ttype ExtensionCommandContextActions,\n\ttype ExtensionErrorListener,\n\ttype ExtensionMode,\n\tExtensionRunner,\n\ttype ExtensionUIContext,\n\ttype InputSource,\n\ttype MessageEndEvent,\n\ttype MessageStartEvent,\n\ttype MessageUpdateEvent,\n\ttype ReplacedSessionContext,\n\ttype SessionBeforeCompactResult,\n\ttype SessionBeforeTreeResult,\n\ttype SessionStartEvent,\n\ttype ShutdownHandler,\n\ttype ToolDefinition,\n\ttype ToolExecutionEndEvent,\n\ttype ToolExecutionStartEvent,\n\ttype ToolExecutionUpdateEvent,\n\ttype ToolInfo,\n\ttype TreePreparation,\n\ttype TurnEndEvent,\n\ttype TurnStartEvent,\n\twrapRegisteredTools,\n} from \"./extensions/index.ts\";\nimport { emitSessionShutdownEvent } from \"./extensions/runner.ts\";\nimport { assertTextChatModelForCompletion } from \"./grok-harness.ts\";\nimport { grokPlaybookAppendForProvider } from \"./grok-playbook.ts\";\nimport { captureHostResourceSnapshot, type HostResourceSnapshot } from \"./host-resource-snapshot.ts\";\nimport { decideLoadoutAccess, type LoadoutAccessPolicy } from \"./loadout-access-policy.ts\";\nimport { loadMcpServerConfigs } from \"./mcp/config.ts\";\nimport { McpManager, type McpServerConfig, type McpServerStatus } from \"./mcp/manager.ts\";\nimport { buildRuntimeProvenance } from \"./runtime-provenance.ts\";\nimport { type ToolCallMetric, TurnMetricsSink } from \"./turn-metrics.ts\";\n\n/** Values that disable a session-level feature through its environment variable. */\nconst DISABLED_ENV_VALUES = new Set([\"0\", \"false\", \"off\", \"disable\", \"disabled\"]);\nconst MAX_OVERFLOW_RECOVERY_ATTEMPTS = 2;\nconst OVERFLOW_RECOVERY_EMERGENCY_TOKENS = 4_096;\nconst MAX_RESOURCE_OBSERVATION_JOURNALS = 32;\n\nfunction isDisabledEnvValue(value: string | undefined): boolean {\n\treturn value !== undefined && DISABLED_ENV_VALUES.has(value.trim().toLowerCase());\n}\n\n/** In-flight metric accumulation for the current turn. */\ninterface TurnMetricsState {\n\treadonly startedAtEpochMs: number;\n\treadonly toolCalls: ToolCallMetric[];\n\treadonly toolStarts: Map<string, number>;\n\t/** `provider/model` the turn was requested with, captured at turn start. */\n\treadonly requestedModel: string | null;\n\t/** `\"auto\"` in auto mode, else the thinking level captured at turn start. */\n\treadonly requestedThinking: string;\n}\n\ntype RouterAutoDecision = Pick<\n\tRouterFeedbackRecord,\n\t\"routerVersion\" | \"laneType\" | \"predictedClass\" | \"resolvedLevel\" | \"lenBucket\" | \"hadFence\" | \"hadDiff\"\n>;\n\n/** Extract the first text block of a tool result, for failure classification only. */\nfunction firstTextContent(result: unknown): string | undefined {\n\tconst content = (result as { content?: unknown } | undefined)?.content;\n\tif (!Array.isArray(content)) return undefined;\n\tfor (const block of content) {\n\t\tif (block && typeof block === \"object\" && (block as { type?: unknown }).type === \"text\") {\n\t\t\tconst text = (block as { text?: unknown }).text;\n\t\t\tif (typeof text === \"string\") return text;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nimport { redactCredentialShapedContent } from \"./compaction/transaction.ts\";\nimport type { CustomMessage } from \"./messages.ts\";\nimport { selectContextFilesForModel } from \"./model-prompt-policy.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { findExactModelReferenceMatch } from \"./model-resolver.ts\";\nimport { computePromptTokenBudget } from \"./prompt-budget.ts\";\nimport { classifyPromptCacheTransition } from \"./prompt-cache.ts\";\nimport {\n\tcreatePromptSettlementState,\n\ttype PromptSettledEvent,\n\ttype PromptSettlementOutcome,\n\treducePromptSettlement,\n\tsettlePromptIfReady,\n} from \"./prompt-settlement.ts\";\nimport { expandPromptTemplate, type PromptTemplate } from \"./prompt-templates.ts\";\nimport {\n\tisQuotaExhaustionMessage,\n\tisStickySafetyModel,\n\tisUpstreamUnavailableMessage,\n\tpickFailoverCandidate,\n\tresolveFailoverCandidates,\n\tresolveProviderResilience,\n\tsameModelRouteCandidates,\n\tshouldEjectStickySafetyModel,\n\tshouldHonorSafetyFailover,\n\tstickySafetyBlockMessage,\n} from \"./provider-resilience.ts\";\nimport {\n\tcomputeRetryDelayMs,\n\tfailoverModelKey,\n\tisFailoverTriggerError,\n\tisRetryableAssistantError,\n\tnextRetryAttempt,\n\tretryBudgetForAssistantError,\n} from \"./provider-retry.ts\";\nimport { getBiasStepsForCell, parseRouterBiasSnapshot, type RouterBiasSnapshot } from \"./reasoning-router-bias.ts\";\nimport {\n\tclassifyTaskV4,\n\tderiveRouterFeedbackFeaturesV4,\n\ttype RouterFeedbackFeaturesV4,\n\tresolveThinkingLevelV4WithUncertainty,\n\tTASK_CLASS_THINKING_LEVELS_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4.ts\";\nimport { redactSensitiveText, redactSensitiveTextForced } from \"./redaction.ts\";\nimport { getRepositoryRouterLearningPaths, type RepositoryRouterLearningPaths } from \"./repository-learning-scope.ts\";\nimport { decideResourceAdmission, type ResourceAdmissionDecision } from \"./resource-admission.ts\";\nimport { resolveResourceGovernorSettings } from \"./resource-governor-settings.ts\";\nimport type { ResourceExtensionPaths, ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tadmissionObservationFacts,\n\tclassificationObservationFacts,\n\tResourceObservationJournal,\n\tsettledObservationFacts,\n\tsnapshotObservationFacts,\n\tsoundObservationFacts,\n} from \"./resource-observation-journal.ts\";\nimport { decideResourceSafetyGate, RESOURCE_PRESSURE_REQUIRED_ACTION } from \"./resource-safety-gate.ts\";\nimport {\n\tappendRouterFeedbackRecord,\n\tROUTER_FEEDBACK_LEVELS,\n\ttype RouterFeedbackRecord,\n} from \"./router-feedback-collector.ts\";\nimport type { RunJournalAuditDetails, RunJournalAuditEvent, RunJournalRecord } from \"./run-journal.ts\";\nimport { type RunJournalQuarantineReport, RunJournalStore } from \"./run-journal-store.ts\";\nimport { type RunResourceLease, RunResourceLeaseController } from \"./run-resource-lease.ts\";\nimport { SessionBashRuntime } from \"./session-bash-runtime.ts\";\nimport { type BashResourcePermitGrant, SessionBashService } from \"./session-bash-service.ts\";\nimport { SessionCompactionService } from \"./session-compaction-service.ts\";\nimport type { BranchSummaryEntry, SessionManager } from \"./session-manager.ts\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.ts\";\nimport { acquireSessionOwnerLeaseSync, type SessionOwnerLease } from \"./session-owner-lease.ts\";\nimport { assembleSessionSystemPrompt } from \"./session-system-prompt.ts\";\nimport {\n\tclassifySessionTermination,\n\ttype SessionProcessSignal,\n\ttype SessionTermination,\n\ttype SessionTerminationCause,\n} from \"./session-termination.ts\";\nimport type { SettingsManager } from \"./settings-manager.ts\";\nimport type { SlashCommandInfo } from \"./slash-commands.ts\";\nimport { createSyntheticSourceInfo, type SourceInfo } from \"./source-info.ts\";\nimport { type BuildSystemPromptOptions, buildSystemPromptPlan } from \"./system-prompt.ts\";\nimport { type BashOperations, type BashSandboxPreflight, createLocalBashOperations } from \"./tools/bash.ts\";\nimport { createAllToolDefinitions } from \"./tools/index.ts\";\nimport { createToolDefinitionFromAgentTool } from \"./tools/tool-definition-wrapper.ts\";\nimport { createVerifiedBashOperations } from \"./verified-bash-adapter.ts\";\nimport { resolveSessionWorkspaceScope } from \"./verified-bash-runtime.ts\";\nimport { classifyWorkloadCommand } from \"./workload-classifier.ts\";\nimport { WorkloadPermitError, WorkloadPermitPool, type WorkloadPermitPoolSnapshot } from \"./workload-permit-pool.ts\";\n\n// ============================================================================\n// Skill Block Parsing\n// ============================================================================\n\n/** Parsed skill block from a user message */\nexport interface ParsedSkillBlock {\n\tname: string;\n\tlocation: string;\n\tcontent: string;\n\tuserMessage: string | undefined;\n}\n\n/**\n * Parse a skill block from message text.\n * Returns null if the text doesn't contain a skill block.\n */\nexport function parseSkillBlock(text: string): ParsedSkillBlock | null {\n\tconst match = text.match(/^<skill name=\"([^\"]+)\" location=\"([^\"]+)\">\\n([\\s\\S]*?)\\n<\\/skill>(?:\\n\\n([\\s\\S]+))?$/);\n\tif (!match) return null;\n\treturn {\n\t\tname: match[1],\n\t\tlocation: match[2],\n\t\tcontent: match[3],\n\t\tuserMessage: match[4]?.trim() || undefined,\n\t};\n}\n\n/**\n * Thinking mode: \"manual\" keeps the user-selected level; \"auto\" resolves the\n * level per turn via the reasoning router. Manual `/think <level>` always wins\n * because the router only runs in auto mode.\n */\nexport type ThinkingMode = \"manual\" | \"auto\";\n\n/** Session-specific events that extend the core AgentEvent */\nexport type AgentSessionEvent =\n\t| Exclude<AgentEvent, { type: \"agent_end\" }>\n\t| {\n\t\t\ttype: \"agent_end\";\n\t\t\tmessages: AgentMessage[];\n\t\t\twillRetry: boolean;\n\t }\n\t| PromptSettledEvent\n\t| {\n\t\t\ttype: \"queue_update\";\n\t\t\tsteering: readonly string[];\n\t\t\tfollowUp: readonly string[];\n\t }\n\t| { type: \"compaction_start\"; reason: \"manual\" | \"threshold\" | \"overflow\" }\n\t| { type: \"session_info_changed\"; name: string | undefined }\n\t| { type: \"thinking_level_changed\"; level: ThinkingLevel }\n\t| {\n\t\t\ttype: \"compaction_end\";\n\t\t\treason: \"manual\" | \"threshold\" | \"overflow\";\n\t\t\tresult: CompactionResult | undefined;\n\t\t\taborted: boolean;\n\t\t\twillRetry: boolean;\n\t\t\terrorMessage?: string;\n\t }\n\t| { type: \"auto_retry_start\"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }\n\t| { type: \"auto_retry_end\"; success: boolean; attempt: number; finalError?: string }\n\t| { type: \"bash_execution_update\"; id?: string; delta: string }\n\t| {\n\t\t\ttype: \"summarization_retry_scheduled\";\n\t\t\tattempt: number;\n\t\t\tmaxAttempts: number;\n\t\t\tdelayMs: number;\n\t\t\terrorMessage: string;\n\t }\n\t| { type: \"summarization_retry_attempt_start\"; source: \"branchSummary\" }\n\t| {\n\t\t\ttype: \"summarization_retry_attempt_start\";\n\t\t\tsource: \"compaction\";\n\t\t\treason: \"manual\" | \"threshold\" | \"overflow\";\n\t }\n\t| { type: \"summarization_retry_finished\" }\n\t| { type: \"session_termination\"; termination: SessionTermination }\n\t| {\n\t\t\t/**\n\t\t\t * A late-settling potentially-writing tool may have mutated the\n\t\t\t * workspace after its terminal result was committed. Evidence\n\t\t\t * freshness consumers must treat affected scopes as stale\n\t\t\t * (empty `paths` means the whole workspace root).\n\t\t\t */\n\t\t\ttype: \"workspace_mutation\";\n\t\t\tsource: \"tool_late_settlement\";\n\t\t\ttoolCallId: string;\n\t\t\ttoolName: string;\n\t\t\tpayload: { root: string; paths: readonly string[] };\n\t };\n\n/** Listener function for agent session events */\nexport type AgentSessionEventListener = (event: AgentSessionEvent) => void;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AgentSessionConfig {\n\tagent: Agent;\n\tsessionManager: SessionManager;\n\tsettingsManager: SettingsManager;\n\tcwd: string;\n\t/** Models to cycle through with Ctrl+P (from --models flag) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\t/** Resource loader for skills, prompts, themes, context files, system prompt */\n\tresourceLoader: ResourceLoader;\n\t/** SDK custom tools registered outside extensions */\n\tcustomTools?: ToolDefinition[];\n\t/** Model registry for API key resolution and model discovery */\n\tmodelRegistry: ModelRegistry;\n\t/** Initial active built-in tool names. Default: [read, bash, edit, write] */\n\tinitialActiveToolNames?: string[];\n\t/** Optional allowlist of tool names. When provided, only these tool names are exposed. */\n\tallowedToolNames?: string[];\n\t/** Optional denylist of tool names. When provided, these tool names are not exposed. */\n\texcludedToolNames?: string[];\n\t/**\n\t * Override base tools (useful for custom runtimes).\n\t *\n\t * These are synthesized into minimal ToolDefinitions internally so AgentSession can keep\n\t * a definition-first registry even when callers provide plain AgentTool instances.\n\t */\n\tbaseToolsOverride?: Record<string, AgentTool>;\n\t/** Trusted sandbox preflight used for built-in local bash execution. Never sourced from RPC command payloads. */\n\tbashSandboxPreflight?: BashSandboxPreflight;\n\t/** Optional immutable loadout policy used to lock active tools and scope built-in tool access. */\n\tloadoutAccessPolicy?: LoadoutAccessPolicy;\n\t/** Mutable ref used by Agent to access the current ExtensionRunner */\n\textensionRunnerRef?: { current?: ExtensionRunner };\n\t/** Session start event metadata emitted when extensions bind to this runtime. */\n\tsessionStartEvent?: SessionStartEvent;\n\t/** Transcript repair applied by the SDK while opening/resuming this session (ALG001-A). */\n\ttranscriptRepair?: SessionTranscriptRepair;\n\t/** Optional shared replay ledger used by evidence receipts and runtime mutation audits. */\n\treplayLedger?: ReplayLedgerManager;\n\t/** Goal binding for replay events. Defaults to the ledger's own goal id. */\n\treplayGoalId?: string;\n\t/** Optional lane binding for replay events. */\n\treplayLaneId?: string;\n\t/** True when the user pinned `--model` / `--provider` for this process. */\n\tmodelPinned?: boolean;\n}\n\n/** Summary of a missing-only transcript auto-repair applied on session open/resume. */\nexport interface SessionTranscriptRepair {\n\treadonly insertedToolCallIds: readonly string[];\n\treadonly reason: \"resume\";\n}\n\nexport interface ExtensionBindings {\n\tuiContext?: ExtensionUIContext;\n\tmode?: ExtensionMode;\n\tcommandContextActions?: ExtensionCommandContextActions;\n\tabortHandler?: () => void;\n\tshutdownHandler?: ShutdownHandler;\n\tonError?: ExtensionErrorListener;\n}\n\ninterface ExecuteBashOptions {\n\texcludeFromContext?: boolean;\n\t/** Optional identifier echoed in `bash_execution_update` events (RPC request correlation). */\n\tid?: string;\n\toperations?: BashOperations;\n\tsafetyGate?: \"headless\";\n\t/** Trusted internal/test override for local bash sandboxing. */\n\tsandboxPolicy?: BashSandboxPreflight;\n}\n\n/** Options for AgentSession.prompt() */\nexport interface PromptOptions {\n\t/** Whether to expand file-based prompt templates (default: true) */\n\texpandPromptTemplates?: boolean;\n\t/** Image attachments */\n\timages?: ImageContent[];\n\t/** When streaming, how to queue the message: \"steer\" (interrupt) or \"followUp\" (wait). Required if streaming. */\n\tstreamingBehavior?: \"steer\" | \"followUp\";\n\t/** Source of input for extension input event handlers. Defaults to \"interactive\". */\n\tsource?: InputSource;\n\tactiveSkillNames?: readonly string[];\n\tactiveSkillSource?: string;\n\t/** Internal hook used by RPC mode to observe prompt preflight acceptance or rejection. */\n\tpreflightResult?: (success: boolean) => void;\n}\n\nfunction mergePromptActiveSkillNames(first: readonly string[], second: readonly string[]): string[] {\n\tconst names: string[] = [];\n\tconst seen = new Set<string>();\n\tfor (const name of [...first, ...second]) {\n\t\tif (!seen.has(name)) {\n\t\t\tseen.add(name);\n\t\t\tnames.push(name);\n\t\t}\n\t}\n\treturn names;\n}\n\n/** Result from cycleModel() */\nexport interface ModelCycleResult {\n\tmodel: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\t/** Whether cycling through scoped models (--models flag) or all available */\n\tisScoped: boolean;\n}\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tpromptCache: {\n\t\tproviderEligibleInputTokens: number;\n\t\tproviderHitRate: number;\n\t\tkeyChanges: number;\n\t\tboundaryBypasses: number;\n\t\tstablePrefixCharacters: number;\n\t\tlastBreakReason?: string;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\ninterface ToolDefinitionEntry {\n\tdefinition: ToolDefinition;\n\tsourceInfo: SourceInfo;\n}\n\ntype BegunCompaction = import(\"./session-compaction-service.ts\").BegunCompaction;\ntype CommittedCompaction = import(\"./session-compaction-service.ts\").CommittedCompaction;\n\nconst PENDING_TOOL_RESULT_TOKENS = {\n\ttext: 1024,\n\timage: 4096,\n\t\"large-output\": 16_384,\n} as const satisfies Record<ToolResultClass, number>;\n\nconst TEXT_RESULT_TOOLS = new Set([\"edit\", \"find\", \"grep\", \"ls\", \"read\", \"write\"]);\nconst IMAGE_PATH_PATTERN = /\\.(?:avif|gif|jpe?g|png|webp)$/iu;\n\nfunction pendingToolResultClass(name: string, args: unknown): ToolResultClass {\n\tif (\n\t\tname === \"read\" &&\n\t\ttypeof args === \"object\" &&\n\t\targs !== null &&\n\t\ttypeof Reflect.get(args, \"path\") === \"string\" &&\n\t\tIMAGE_PATH_PATTERN.test(Reflect.get(args, \"path\"))\n\t) {\n\t\treturn \"image\";\n\t}\n\treturn TEXT_RESULT_TOOLS.has(name) ? \"text\" : \"large-output\";\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Standard thinking levels */\nconst THINKING_LEVELS: ThinkingLevel[] = [\"off\", \"minimal\", \"low\", \"medium\", \"high\"];\n\nfunction normalizeToolNames(toolNames: readonly string[]): string[] {\n\treturn [...new Set(toolNames.map((name) => name.trim()).filter((name) => name !== \"\"))].sort((left, right) => {\n\t\tif (left < right) return -1;\n\t\tif (left > right) return 1;\n\t\treturn 0;\n\t});\n}\n\nfunction toolNameSetsEqual(left: readonly string[], right: readonly string[]): boolean {\n\tconst normalizedLeft = normalizeToolNames(left);\n\tconst normalizedRight = normalizeToolNames(right);\n\treturn (\n\t\tnormalizedLeft.length === normalizedRight.length &&\n\t\tnormalizedLeft.every((toolName, index) => toolName === normalizedRight[index])\n\t);\n}\n\nfunction snapshotContractError(reason: string): Error {\n\treturn new Error(`Finalized message replacement must be a plain serializable snapshot: ${reason}`);\n}\n\n/**\n * Clone a finalized message replacement into the JSON-compatible snapshot\n * persisted by SessionManager. Undefined remains allowed as ordinary optional\n * message data: JSON omits object properties with undefined values and writes\n * undefined array elements as null.\n */\nfunction clonePlainSnapshot(\n\tvalue: unknown,\n\tancestors = new WeakSet<object>(),\n\tcopies = new WeakMap<object, unknown>(),\n): unknown {\n\tif (value === null) return value;\n\n\tswitch (typeof value) {\n\t\tcase \"string\":\n\t\tcase \"boolean\":\n\t\tcase \"undefined\":\n\t\t\treturn value;\n\t\tcase \"number\":\n\t\t\tif (!Number.isFinite(value)) {\n\t\t\t\tthrow snapshotContractError(\"non-finite number values are not allowed\");\n\t\t\t}\n\t\t\treturn value;\n\t\tcase \"bigint\":\n\t\t\tthrow snapshotContractError(\"bigint values are not allowed\");\n\t\tcase \"function\":\n\t\tcase \"symbol\":\n\t\t\tthrow snapshotContractError(`${typeof value} values are not allowed`);\n\t\tcase \"object\":\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow snapshotContractError(`${typeof value} values are not allowed`);\n\t}\n\n\tif (ancestors.has(value)) {\n\t\tthrow snapshotContractError(\"cyclic values are not allowed\");\n\t}\n\tif (copies.has(value)) {\n\t\treturn copies.get(value);\n\t}\n\tancestors.add(value);\n\n\ttry {\n\t\tif (Array.isArray(value)) {\n\t\t\tconst copy: unknown[] = [];\n\t\t\tcopy.length = value.length;\n\t\t\tcopies.set(value, copy);\n\t\t\tfor (const key of Reflect.ownKeys(value)) {\n\t\t\t\tif (key === \"length\") continue;\n\t\t\t\tif (typeof key !== \"string\" || !/^(0|[1-9]\\d*)$/.test(key) || Number(key) >= value.length) {\n\t\t\t\t\tthrow snapshotContractError(\"arrays may only contain indexed values\");\n\t\t\t\t}\n\t\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\t\t\tif (!descriptor || !(\"value\" in descriptor) || !descriptor.enumerable) {\n\t\t\t\t\tthrow snapshotContractError(\"accessor or non-enumerable properties are not allowed\");\n\t\t\t\t}\n\t\t\t\tcopy[Number(key)] = clonePlainSnapshot(descriptor.value, ancestors, copies);\n\t\t\t}\n\t\t\treturn copy;\n\t\t}\n\n\t\tconst prototype = Object.getPrototypeOf(value);\n\t\tif (prototype !== Object.prototype && prototype !== null) {\n\t\t\tthrow snapshotContractError(\"non-plain objects are not allowed\");\n\t\t}\n\n\t\tconst copy = Object.create(prototype) as Record<string, unknown>;\n\t\tcopies.set(value, copy);\n\t\tfor (const key of Reflect.ownKeys(value)) {\n\t\t\tif (typeof key !== \"string\") {\n\t\t\t\tthrow snapshotContractError(\"symbol-keyed properties are not allowed\");\n\t\t\t}\n\t\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, key);\n\t\t\tif (!descriptor || !(\"value\" in descriptor) || !descriptor.enumerable) {\n\t\t\t\tthrow snapshotContractError(\"accessor or non-enumerable properties are not allowed\");\n\t\t\t}\n\t\t\tObject.defineProperty(copy, key, {\n\t\t\t\tvalue: clonePlainSnapshot(descriptor.value, ancestors, copies),\n\t\t\t\tenumerable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t\twritable: true,\n\t\t\t});\n\t\t}\n\t\treturn copy;\n\t} finally {\n\t\tancestors.delete(value);\n\t}\n}\n\nfunction freezeSnapshot<T>(value: T, seen = new WeakSet<object>()): T {\n\tif (value === null || typeof value !== \"object\" || seen.has(value)) return value;\n\tseen.add(value);\n\tfor (const child of Object.values(value as Record<string, unknown>)) {\n\t\tfreezeSnapshot(child, seen);\n\t}\n\treturn Object.freeze(value);\n}\n\nfunction createImmutableMessageSnapshot(message: AgentMessage): AgentMessage {\n\treturn freezeSnapshot(clonePlainSnapshot(message) as AgentMessage);\n}\n\n// ============================================================================\n// AgentSession Class\n// ============================================================================\n\nexport class AgentSession {\n\treadonly agent: Agent;\n\treadonly sessionManager: SessionManager;\n\treadonly settingsManager: SettingsManager;\n\n\tprivate _scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/**\n\t * Thinking mode (default \"manual\"). Persists across turns within the session;\n\t * auto-resolved levels are never written to the user's persisted settings.\n\t */\n\tprivate _thinkingMode: ThinkingMode = \"manual\";\n\n\t/**\n\t * N=8 ring buffer of recent auto-turn task classes (newest first), feeding\n\t * v4's multi-turn prior feature. Never persisted to settings.\n\t */\n\tprivate _taskClassHistory: TaskClassV4[] = [];\n\n\t/**\n\t * Compiled reasoning-router bias snapshot for the opt-in v4 learning path\n\t * (Goal 010 Lane I). Loaded at most once per session and pinned thereafter:\n\t * `_reasoningRouterBiasSnapshotLoaded` flips to `true` on the first v4\n\t * auto-turn that has learning enabled, and `_reasoningRouterBiasSnapshot`\n\t * then stays fixed for the rest of the session (even across later settings\n\t * reloads, and even if the on-disk file changes or the load failed/was\n\t * invalid, in which case it stays `null`). See `_getReasoningRouterBiasSnapshot`.\n\t */\n\tprivate _reasoningRouterBiasSnapshot: RouterBiasSnapshot | null = null;\n\tprivate _reasoningRouterBiasSnapshotLoaded = false;\n\tprivate _lastAutoRouterDecision: RouterAutoDecision | undefined;\n\n\t/**\n\t * AdaptOrch advisory bridge (default-off, global-only opt-in via\n\t * `adaptorchBridge.enabled` in ~/.omk/agent/settings.json). Lazily\n\t * constructed on the first v4 auto-turn when enabled; stays `null`\n\t * otherwise. The bridge is advisory-only: its hint is fused into the\n\t * resolver as a bounded ±2 step nudge, never as an override.\n\t */\n\tprivate _adaptorchBridge: AdaptorchBridge | null = null;\n\tprivate _adaptorchBridgeInitAttempted = false;\n\n\t// Event subscription state\n\tprivate _unsubscribeAgent?: () => void;\n\tprivate _eventListeners: AgentSessionEventListener[] = [];\n\tprivate _messageEndReplacements = new WeakMap<Extract<AgentEvent, { type: \"message_end\" }>, AgentMessage>();\n\n\t/** Tracks pending steering messages for UI display. Removed when delivered. */\n\tprivate _steeringMessages: string[] = [];\n\t/** Tracks pending follow-up messages for UI display. Removed when delivered. */\n\tprivate _followUpMessages: string[] = [];\n\t/** Messages queued to be included with the next user prompt as context (\"asides\"). */\n\tprivate _pendingNextTurnMessages: CustomMessage[] = [];\n\n\t// Compaction state\n\tprivate _compactionAbortController: AbortController | undefined = undefined;\n\tprivate _autoCompactionAbortController: AbortController | undefined = undefined;\n\tprivate _overflowRecoveryAttempts = 0;\n\tprivate _thresholdCompactionEmergency = false;\n\tprivate _compactionHysteresisState: CompactionHysteresisState = createCompactionHysteresisState();\n\n\t// Branch summarization state\n\tprivate _branchSummaryAbortController: AbortController | undefined = undefined;\n\n\t// Retry state\n\tprivate _retryAbortController: AbortController | undefined = undefined;\n\tprivate _retryAttempt = 0;\n\t// v10.3-Ω: models that already refused/failed this turn — failover must advance,\n\t// not re-pick the same candidate (claude-opus-5 → grok → grok → grok loop fix).\n\tprivate _refusedModels = new Set<string>();\n\t/** True when `--model` / `--provider` pinned this process to one model. */\n\tprivate readonly _modelPinned: boolean;\n\n\t// Bash execution state\n\tprivate readonly _bashService: SessionBashService;\n\tprivate readonly _compactionService: SessionCompactionService;\n\n\t// Extension system\n\tprivate _extensionRunner!: ExtensionRunner;\n\tprivate _turnIndex = 0;\n\n\tprivate _resourceLoader: ResourceLoader;\n\tprivate _customTools: ToolDefinition[];\n\tprivate _baseToolDefinitions: Map<string, ToolDefinition> = new Map();\n\tprivate _mcpManager: McpManager | undefined;\n\tprivate _mcpToolNames: Set<string> = new Set();\n\tprivate _turnMetricsSink: TurnMetricsSink | undefined;\n\tprivate _turnMetricsState: TurnMetricsState | undefined;\n\tprivate _cwd: string;\n\tprivate _repositoryRouterLearningPaths: RepositoryRouterLearningPaths | undefined;\n\tprivate _extensionRunnerRef?: { current?: ExtensionRunner };\n\tprivate _initialActiveToolNames?: string[];\n\tprivate _allowedToolNames?: Set<string>;\n\tprivate _excludedToolNames?: Set<string>;\n\tprivate _baseToolsOverride?: Record<string, AgentTool>;\n\tprivate _loadoutAccessPolicy?: LoadoutAccessPolicy;\n\tprivate _sessionStartEvent: SessionStartEvent;\n\tprivate _extensionUIContext?: ExtensionUIContext;\n\tprivate _extensionMode: ExtensionMode = \"print\";\n\tprivate _extensionCommandContextActions?: ExtensionCommandContextActions;\n\tprivate _extensionAbortHandler?: () => void;\n\tprivate _extensionShutdownHandler?: ShutdownHandler;\n\tprivate _extensionErrorListener?: ExtensionErrorListener;\n\tprivate _extensionErrorUnsubscriber?: () => void;\n\n\t// Required durable run/audit chain, persisted next to the session file.\n\tprivate readonly _runJournalStore: RunJournalStore;\n\tprivate _ownedSessionOwnerLease: SessionOwnerLease | undefined;\n\tprivate _activeRunId: string | null = null;\n\t// Resource governor (roadmap M2): lease controller owns the temporary tool\n\t// cap for governed runs; the last decision backs the §19.4 read-only API.\n\tprivate _resourceLeaseController: RunResourceLeaseController | undefined;\n\tprivate _resourceObservations: ResourceObservationJournal | null = null;\n\tprivate readonly _resourceObservationJournals = new Map<string, ResourceObservationJournal>();\n\tprivate _latestResourcePromptRunId: string | null = null;\n\tprivate _lastResourceAdmission: ResourceAdmissionDecision | null = null;\n\tprivate _workloadPermitPool: WorkloadPermitPool | undefined;\n\tprivate _pendingRuntimeTerminationCause: SessionTerminationCause | undefined;\n\tprivate _activeRunToolTermination:\n\t\t| { toolCallId: string; toolName: string; timeoutMs?: number; executionStarted: boolean }\n\t\t| undefined;\n\tprivate _lastTermination: SessionTermination | undefined;\n\tprivate _userAbortRequested = false;\n\tprivate readonly _replayLedger: ReplayLedgerManager | undefined;\n\tprivate readonly _replayGoalId: string | undefined;\n\tprivate readonly _replayLaneId: string | undefined;\n\tprivate readonly _bashRuntime: SessionBashRuntime;\n\tprivate _transcriptRepair: SessionTranscriptRepair | undefined;\n\tprivate _sessionRiskLevel: \"normal\" | \"elevated\" = \"normal\";\n\tprivate _workspaceMutationCount = 0;\n\n\t// Model registry for API key resolution\n\tprivate _modelRegistry: ModelRegistry;\n\t/** Lazy, in-memory only; never shared across sessions or persisted. */\n\tprivate _contextBudgetCacheProvider: ContextBudgetCacheProviderV2 | undefined;\n\tprivate _contextCacheInvalidationSnapshot: ContextCacheInvalidationSnapshot;\n\n\t// Tool registry for extension getTools/setTools\n\tprivate _toolRegistry: Map<string, AgentTool> = new Map();\n\tprivate _toolDefinitions: Map<string, ToolDefinitionEntry> = new Map();\n\tprivate _toolPromptSnippets: Map<string, string> = new Map();\n\tprivate _toolPromptGuidelines: Map<string, string[]> = new Map();\n\n\t// Base system prompt (without extension appends) - used to apply fresh appends each turn\n\tprivate _baseSystemPrompt = \"\";\n\tprivate _baseSystemPromptCacheBoundary: number | undefined;\n\tprivate _baseSystemPromptOptions!: BuildSystemPromptOptions;\n\tprivate _promptCacheKey: string | undefined;\n\tprivate _promptCacheKeyChanges = 0;\n\tprivate _promptCacheBoundaryBypasses = 0;\n\tprivate _promptCacheStablePrefixCharacters = 0;\n\tprivate _promptCacheLastBreakReason: string | undefined;\n\n\tconstructor(config: AgentSessionConfig) {\n\t\tthis.agent = config.agent;\n\t\tthis.sessionManager = config.sessionManager;\n\t\tthis.settingsManager = config.settingsManager;\n\t\tthis._scopedModels = config.scopedModels ?? [];\n\t\tthis._modelPinned = config.modelPinned === true;\n\t\tthis._resourceLoader = config.resourceLoader;\n\t\tthis._customTools = config.customTools ?? [];\n\t\tthis._cwd = config.cwd;\n\t\tthis._modelRegistry = config.modelRegistry;\n\t\tconst initialModelId = this._contextCacheModelId(this.agent.state.model);\n\t\tthis._contextCacheInvalidationSnapshot = createContextCacheInvalidationSnapshot({\n\t\t\tforkId: this.sessionManager.getSessionId(),\n\t\t\tworktreeFingerprint: this._contextCacheWorktreeFingerprint(),\n\t\t\tactiveModelId: initialModelId,\n\t\t\tcompactionModelId: initialModelId,\n\t\t});\n\t\tthis._extensionRunnerRef = config.extensionRunnerRef;\n\t\tthis._initialActiveToolNames = config.initialActiveToolNames;\n\t\tthis._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined;\n\t\tthis._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined;\n\t\tthis._baseToolsOverride = config.baseToolsOverride;\n\t\tthis._loadoutAccessPolicy = config.loadoutAccessPolicy;\n\n\t\tthis._sessionStartEvent = config.sessionStartEvent ?? { type: \"session_start\", reason: \"startup\" };\n\t\tthis._transcriptRepair = config.transcriptRepair;\n\t\tthis._replayLedger = config.replayLedger;\n\t\tthis._replayGoalId = config.replayGoalId ?? config.replayLedger?.getLedger().goalId;\n\t\tthis._replayLaneId = config.replayLaneId;\n\t\tif (this._replayLedger && this._replayGoalId !== this._replayLedger.getLedger().goalId) {\n\t\t\tthrow new Error(\"AgentSession replayGoalId does not match the replay ledger goal id\");\n\t\t}\n\t\tthis._bashRuntime = new SessionBashRuntime({\n\t\t\tcwd: this._cwd,\n\t\t\tgetExecutionCwd: () => this.sessionManager.getCwd(),\n\t\t\tgetSessionFile: () => this.sessionManager.getSessionFile(),\n\t\t\treplayLedger: this._replayLedger,\n\t\t\treplayGoalId: this._replayGoalId,\n\t\t\treplayLaneId: this._replayLaneId,\n\t\t\tconfiguredSandboxPreflight: config.bashSandboxPreflight,\n\t\t\tappendReplayEvent: (type, payload) => this._appendReplayEvent(type, payload),\n\t\t});\n\t\tthis._bashService = new SessionBashService({\n\t\t\tsettings: this.settingsManager,\n\t\t\truntime: this._bashRuntime,\n\t\t\tloadoutPolicy: this._loadoutAccessPolicy,\n\t\t\tgetCwd: () => this.sessionManager.getCwd(),\n\t\t\temit: (event) => this._emit(event),\n\t\t\tisStreaming: () => this.isStreaming,\n\t\t\tpushMessage: (message) => this.agent.state.messages.push(message),\n\t\t\tappendMessage: (message) => this.sessionManager.appendMessage(message),\n\t\t\tacquireResourcePermit: (command, signal) => this._acquireBashResourcePermit(command, signal),\n\t\t});\n\t\tthis._compactionService = new SessionCompactionService({\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tpendingToolCallIds: () => this.agent.state.pendingToolCalls,\n\t\t\tgetUserMessageText: (message) => this._getUserMessageText(message),\n\t\t\tcwd: this._cwd,\n\t\t\tinvalidateContextBudget: () => this._invalidateContextBudgetCache({ type: \"transcriptRepair\" }),\n\t\t\trefreshAgentMessages: () => {\n\t\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\t\t\t},\n\t\t\trecordCommit: () => this._recordCompactionCommitForHysteresis(),\n\t\t});\n\t\tconst sessionFile = this.sessionManager.getSessionFile();\n\t\tlet ownerLease = this.sessionManager.getOwnerLease();\n\t\ttry {\n\t\t\tif (sessionFile && !ownerLease) {\n\t\t\t\townerLease = acquireSessionOwnerLeaseSync(sessionFile);\n\t\t\t\tthis._ownedSessionOwnerLease = ownerLease;\n\t\t\t\tthis.sessionManager.setOwnerLease(ownerLease);\n\t\t\t}\n\t\t\tthis._runJournalStore = RunJournalStore.open({\n\t\t\t\t...(sessionFile ? { journalPath: `${sessionFile}.runjournal` } : {}),\n\t\t\t\tsessionId: this.sessionManager.getSessionId(),\n\t\t\t\t...(ownerLease ? { ownerLease } : {}),\n\t\t\t});\n\t\t\tconst startupRecords = this._runJournalStore.records;\n\t\t\tconst startupTerminal = startupRecords[startupRecords.length - 1];\n\t\t\tif (startupTerminal?.event === \"run_recovered\") {\n\t\t\t\tthis._lastTermination = startupTerminal.termination;\n\t\t\t}\n\n\t\t\t// Always subscribe to agent events for internal handling\n\t\t\t// (session persistence, extensions, auto-compaction, retry logic)\n\t\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t\t\tthis._installAgentToolHooks();\n\n\t\t\t// Durable transcript-repair audit for a repair applied on open/resume.\n\t\t\tif (this._transcriptRepair) {\n\t\t\t\tthis._invalidateContextBudgetCache({ type: \"transcriptRepair\" });\n\t\t\t\tthis._appendRunJournalAudit(\"transcript_repaired\", {\n\t\t\t\t\tinsertedToolCallIds: [...this._transcriptRepair.insertedToolCallIds],\n\t\t\t\t\treason: this._transcriptRepair.reason,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tthis._buildRuntime({\n\t\t\t\tactiveToolNames: this._initialActiveToolNames,\n\t\t\t\tincludeAllExtensionTools: true,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tconst ownedLease = this._ownedSessionOwnerLease;\n\t\t\tif (ownedLease) {\n\t\t\t\ttry {\n\t\t\t\t\townedLease.release();\n\t\t\t\t\tif (this.sessionManager.getOwnerLease() === ownedLease) this.sessionManager.setOwnerLease(undefined);\n\t\t\t\t\tthis._ownedSessionOwnerLease = undefined;\n\t\t\t\t} catch {\n\t\t\t\t\tthrow new Error(\"AgentSession initialization cleanup failed\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/** Model registry for API key resolution and model discovery */\n\tget modelRegistry(): ModelRegistry {\n\t\treturn this._modelRegistry;\n\t}\n\n\t/** Transcript repair applied while this session was opened/resumed, if any. */\n\tget transcriptRepair(): SessionTranscriptRepair | undefined {\n\t\treturn this._transcriptRepair;\n\t}\n\n\t/** Durable lifecycle and audit records appended by this session's run journal. */\n\tget runJournalRecords(): readonly RunJournalRecord[] {\n\t\treturn this._runJournalStore.records;\n\t}\n\n\t/** Most recently observed or inferred termination for this session. */\n\tget lastTermination(): SessionTermination | undefined {\n\t\treturn this._lastTermination;\n\t}\n\n\t/** Exact trailing journal fragment quarantine performed during startup, if any. */\n\tget runJournalQuarantineReport(): RunJournalQuarantineReport | null {\n\t\treturn this._runJournalStore.quarantineReport;\n\t}\n\n\t/** Elevated once a late-settling potentially-writing tool may have mutated the workspace. */\n\tget sessionRiskLevel(): \"normal\" | \"elevated\" {\n\t\treturn this._sessionRiskLevel;\n\t}\n\n\t/** Monotonic count of workspace mutation/invalidation signals emitted by this session. */\n\tget workspaceMutationCount(): number {\n\t\treturn this._workspaceMutationCount;\n\t}\n\n\tget contextCacheInvalidationSnapshot(): ContextCacheInvalidationSnapshot {\n\t\treturn this._contextCacheInvalidationSnapshot;\n\t}\n\n\tprivate _contextCacheModelId(model: Model<any> | undefined): string {\n\t\treturn model\n\t\t\t? `model-${createHash(\"sha256\").update(`${model.provider}\\0${model.id}`, \"utf8\").digest(\"hex\")}`\n\t\t\t: \"unknown\";\n\t}\n\n\tprivate _contextCacheWorktreeFingerprint(): string {\n\t\treturn createHash(\"sha256\").update(`${this._cwd}\\0${this._workspaceMutationCount}`, \"utf8\").digest(\"hex\");\n\t}\n\n\tprivate _invalidateContextBudgetCache(event: ContextCacheInvalidationEvent): void {\n\t\tconst result = applyContextCacheInvalidation(this._contextCacheInvalidationSnapshot, event);\n\t\tthis._contextCacheInvalidationSnapshot = result.snapshot;\n\t\tif (result.status === \"overflow\") {\n\t\t\tthis._contextBudgetCacheProvider = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis._contextBudgetCacheProvider?.setInvalidationSnapshot?.(result.snapshot);\n\t}\n\n\tprivate _recordEvidenceReceiptInvalidation(customType: string): void {\n\t\tif (customType === \"evidence_receipt\" || customType === \"evidence-receipt\") {\n\t\t\tthis._invalidateContextBudgetCache({ type: \"evidenceReceipt\" });\n\t\t}\n\t}\n\n\tprivate _sessionRevision(): number {\n\t\treturn this.sessionManager.getEntries().length;\n\t}\n\n\tprivate _appendReplayEvent(type: ReplayEventType, payload: unknown): void {\n\t\tif (!this._replayLedger || !this._replayGoalId) return;\n\t\tthis._replayLedger.append({\n\t\t\ttype,\n\t\t\tgoalId: this._replayGoalId,\n\t\t\t...(this._replayLaneId ? { laneId: this._replayLaneId } : {}),\n\t\t\tpayload,\n\t\t});\n\t\tthis._replayLedger.persist();\n\t}\n\n\t/** Lazy verified-bash executor bound to this session's replay ledger (default-on path). */\n\tprivate _getVerifiedEvidenceExecutor(): VerifiedEvidenceExecutor | undefined {\n\t\treturn this._bashRuntime.verifiedEvidenceExecutor();\n\t}\n\n\t/** Append one required durable audit record. Persistence failures propagate. */\n\tprivate _appendRunJournalAudit(event: RunJournalAuditEvent, details: RunJournalAuditDetails): void {\n\t\tthis._runJournalStore.audit({\n\t\t\tevent,\n\t\t\tdetails,\n\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t});\n\t\tthis._appendReplayEvent(event, details);\n\t}\n\n\tprivate _terminationMessage(value: string | undefined, fallback: string): string {\n\t\tconst redacted = redactSensitiveText(value?.trim() || fallback)\n\t\t\t.replace(/\\0/g, \"\")\n\t\t\t.slice(0, 512);\n\t\treturn redacted || fallback;\n\t}\n\n\tprivate _providerFailureCause(message: AssistantMessage): SessionTerminationCause {\n\t\tconst text = message.errorMessage ?? \"\";\n\t\tif (isContextOverflow(message, this.model?.contextWindow ?? 0)) {\n\t\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t\t}\n\t\t// Fable/Claude often emit stop_reason=refusal on benign turns (false positive).\n\t\t// Match broadly so we never mislabel these as provider.protocol.\n\t\tif (\n\t\t\t/stop_reason\\s*=\\s*(refusal|sensitive)|content\\/safety stop|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\t\t\ttext,\n\t\t\t)\n\t\t) {\n\t\t\treturn { area: \"provider\", code: \"refusal\" };\n\t\t}\n\t\t// Quota/billing exhaustion is checked BEFORE the generic 401/403 auth\n\t\t// patterns: \"403 ... usage limit for this billing cycle\" is transient per\n\t\t// cycle and must fail over, not terminate the turn as an auth error.\n\t\tif (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t\t}\n\t\tif (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\\/login/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"auth\" };\n\t\t}\n\t\t// Gateway/upstream 5xx and dropped streams are transport failures, not\n\t\t// transcript-shape problems. Classify as network so the guidance points to\n\t\t// retry/model-switch instead of the protocol sanitize path.\n\t\tif (isUpstreamUnavailableMessage(text)) {\n\t\t\treturn { area: \"provider\", code: \"network\" };\n\t\t}\n\t\t// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.\n\t\t// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.\n\t\tif (/tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"protocol\" };\n\t\t}\n\t\tif (/tool.+timed? out|tool.+timeout/i.test(text)) return { area: \"tool\", code: \"timeout\" };\n\t\tif (/\\btool\\b/i.test(text) && !/tool_call_id/i.test(text)) return { area: \"tool\", code: \"fatal\" };\n\t\tif (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {\n\t\t\treturn { area: \"provider\", code: \"network\" };\n\t\t}\n\t\treturn { area: \"provider\", code: \"protocol\" };\n\t}\n\n\tprivate _classifyPreflightCause(message: string): SessionTerminationCause {\n\t\tif (!this.model || /no model|model selected|model is required/i.test(message)) {\n\t\t\treturn { area: \"configuration\", code: \"invalid\" };\n\t\t}\n\t\tif (isQuotaExhaustionMessage(message)) {\n\t\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t\t}\n\t\tif (/auth|api key|unauthori[sz]ed|forbidden|401|403|\\/login/i.test(message)) {\n\t\t\treturn { area: \"provider\", code: \"auth\" };\n\t\t}\n\t\tif (isUpstreamUnavailableMessage(message)) {\n\t\t\treturn { area: \"provider\", code: \"network\" };\n\t\t}\n\t\tif (/context.+overflow|context window|too many tokens/i.test(message)) {\n\t\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t\t}\n\t\tif (/duplicate.?result/i.test(message)) return { area: \"transcript\", code: \"duplicate_result\" };\n\t\tif (/orphan.?result/i.test(message)) return { area: \"transcript\", code: \"orphan_result\" };\n\t\tif (/duplicate.?call/i.test(message)) return { area: \"transcript\", code: \"duplicate_call_id\" };\n\t\tif (/transcript|missing.?result/i.test(message)) return { area: \"transcript\", code: \"missing_result\" };\n\t\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\t\tif (/fsync/i.test(message)) return { area: \"persistence\", code: \"fsync_failed\" };\n\t\tif (/lock/i.test(message)) return { area: \"persistence\", code: \"lock_failed\" };\n\t\tif (/append|persist|write/i.test(message)) return { area: \"persistence\", code: \"append_failed\" };\n\t\tif (/tool/i.test(message)) return { area: \"tool\", code: \"fatal\" };\n\t\treturn { area: \"internal\", code: \"unclassified\" };\n\t}\n\n\tprivate _classifyRunTermination(\n\t\trunId: string,\n\t\tevent: Extract<AgentEvent, { type: \"agent_end\" }>,\n\t): SessionTermination {\n\t\tconst timestamp = new Date().toISOString();\n\t\tlet assistant: AssistantMessage | undefined;\n\t\tfor (let index = event.messages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = event.messages[index];\n\t\t\tif (message?.role === \"assistant\") {\n\t\t\t\tassistant = message;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tlet cause: SessionTerminationCause;\n\t\tlet message: string;\n\t\tlet sideEffects: \"none\" | \"possible\" = this._sessionRiskLevel === \"elevated\" ? \"possible\" : \"none\";\n\t\tlet toolCallId: string | undefined;\n\t\tlet toolName: string | undefined;\n\n\t\tif (this._activeRunToolTermination) {\n\t\t\tcause = { area: \"tool\", code: \"timeout\" };\n\t\t\tmessage = `Tool ${this._activeRunToolTermination.toolName} timed out.`;\n\t\t\ttoolCallId = this._activeRunToolTermination.toolCallId;\n\t\t\ttoolName = this._activeRunToolTermination.toolName;\n\t\t\tsideEffects = this._activeRunToolTermination.executionStarted ? \"possible\" : sideEffects;\n\t\t} else if (!assistant) {\n\t\t\tcause = { area: \"internal\", code: \"unclassified\" };\n\t\t\tmessage = \"Agent run ended without an assistant result.\";\n\t\t} else if (assistant.stopReason === \"aborted\") {\n\t\t\tcause = this._userAbortRequested ? { area: \"user\", code: \"abort\" } : { area: \"provider\", code: \"abort\" };\n\t\t\tmessage = this._terminationMessage(\n\t\t\t\tassistant.errorMessage,\n\t\t\t\tthis._userAbortRequested ? \"The user aborted the run.\" : \"The provider aborted the run.\",\n\t\t\t);\n\t\t} else if (assistant.stopReason === \"error\") {\n\t\t\tcause = this._providerFailureCause(assistant);\n\t\t\tmessage = this._terminationMessage(assistant.errorMessage, \"The provider request failed.\");\n\t\t} else {\n\t\t\tcause = { area: \"completed\" };\n\t\t\tmessage = \"Run completed.\";\n\t\t}\n\n\t\tlet provider = this.model?.provider;\n\t\tif (assistant?.provider) provider = assistant.provider;\n\t\tlet model = this.model?.id;\n\t\tif (assistant?.model) model = assistant.model;\n\n\t\treturn classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage,\n\t\t\tcause,\n\t\t\tsideEffects,\n\t\t\t...(provider ? { provider } : {}),\n\t\t\t...(model ? { model } : {}),\n\t\t\t...(toolCallId ? { toolCallId } : {}),\n\t\t\t...(toolName ? { toolName } : {}),\n\t\t});\n\t}\n\n\tprivate _publishTermination(termination: SessionTermination): void {\n\t\tthis._lastTermination = termination;\n\t\tthis._emit({ type: \"session_termination\", termination });\n\t}\n\n\tprivate _runtimeFailureCause(error: unknown): SessionTerminationCause {\n\t\tif (this._pendingRuntimeTerminationCause) return this._pendingRuntimeTerminationCause;\n\t\tconst code =\n\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error ? Reflect.get(error, \"code\") : undefined;\n\t\tif (\n\t\t\ttypeof code === \"string\" &&\n\t\t\tnew Set([\"EACCES\", \"EDQUOT\", \"EFBIG\", \"EIO\", \"EISDIR\", \"EMFILE\", \"ENFILE\", \"ENOSPC\", \"EPERM\", \"EROFS\"]).has(\n\t\t\t\tcode,\n\t\t\t)\n\t\t) {\n\t\t\treturn { area: \"persistence\", code: \"append_failed\" };\n\t\t}\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tif (/compaction.+stale|session changed during compaction/i.test(message)) {\n\t\t\treturn { area: \"compaction\", code: \"stale\" };\n\t\t}\n\t\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\t\treturn { area: \"internal\", code: \"unclassified\" };\n\t}\n\n\tprivate _publishRuntimeFailure(error: unknown): void {\n\t\tconst cause = this._runtimeFailureCause(error);\n\t\tconst runId = this._activeRunId ?? `runtime-${randomUUID()}`;\n\t\tconst timestamp = new Date().toISOString();\n\t\tlet message = \"The AgentSession runtime failed before completing the run.\";\n\t\tif (cause.area === \"persistence\") {\n\t\t\tmessage = \"A required runtime persistence operation failed.\";\n\t\t} else if (cause.area === \"compaction\") {\n\t\t\tmessage = \"Runtime compaction failed.\";\n\t\t}\n\t\tlet termination = classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage,\n\t\t\tcause,\n\t\t\tsideEffects: this._activeRunId === null ? \"none\" : \"possible\",\n\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t});\n\t\tif (this._activeRunId !== null && this._runJournalStore.openRunId === this._activeRunId) {\n\t\t\ttry {\n\t\t\t\tthis._runJournalStore.finish({\n\t\t\t\t\ttermination,\n\t\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\t\ttimestamp,\n\t\t\t\t});\n\t\t\t} catch {\n\t\t\t\ttermination = classifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: \"The run journal could not persist the runtime termination.\",\n\t\t\t\t\tcause: { area: \"persistence\", code: \"append_failed\" },\n\t\t\t\t\tsideEffects: \"possible\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tthis._activeRunId = null;\n\t\tthis._activeRunToolTermination = undefined;\n\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\tthis._userAbortRequested = false;\n\t\tthis._publishTermination(termination);\n\t}\n\n\tprivate _handleRunLifecycleEvent(event: AgentEvent): void {\n\t\tif (event.type === \"agent_start\") {\n\t\t\tif (this._activeRunId !== null) throw new Error(\"run journal already has an active AgentSession run\");\n\t\t\tconst runId = randomUUID();\n\t\t\tthis._activeRunId = runId;\n\t\t\tthis._activeRunToolTermination = undefined;\n\t\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\t\tthis._userAbortRequested = false;\n\t\t\ttry {\n\t\t\t\tthis._runJournalStore.start({\n\t\t\t\t\trunId,\n\t\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tthis._pendingRuntimeTerminationCause = { area: \"persistence\", code: \"append_failed\" };\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (event.type !== \"agent_end\") return;\n\t\tif (this._activeRunId === null) throw new Error(\"run journal received agent_end without run_started\");\n\t\tconst termination = this._classifyRunTermination(this._activeRunId, event);\n\t\ttry {\n\t\t\tthis._runJournalStore.finish({\n\t\t\t\ttermination,\n\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\ttimestamp: termination.timestamp,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis._pendingRuntimeTerminationCause = { area: \"persistence\", code: \"append_failed\" };\n\t\t\tthrow error;\n\t\t}\n\t\tthis._activeRunId = null;\n\t\tthis._activeRunToolTermination = undefined;\n\t\tthis._pendingRuntimeTerminationCause = undefined;\n\t\tthis._userAbortRequested = false;\n\t\tthis._publishTermination(termination);\n\t}\n\n\t/**\n\t * Handle tool timeout / late-settlement audit signals (ALG004-A/B). A late\n\t * settlement of a potentially-writing tool raises session risk and emits a\n\t * workspace mutation/invalidation signal for evidence freshness consumers.\n\t */\n\tprivate _handleToolAuditEvent(event: AgentEvent): void {\n\t\tif (event.type === \"tool_execution_end\") {\n\t\t\tthis._invalidateContextBudgetCache({ type: \"toolResultDisposition\" });\n\t\t\tconst envelope = (event.result as { details?: { omk?: Record<string, unknown> } } | undefined)?.details?.omk;\n\t\t\tif (envelope && envelope.schema === \"tool-result/v2\" && envelope.disposition === \"timeout\") {\n\t\t\t\tconst timeout = {\n\t\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t...(typeof envelope.timeoutMs === \"number\" ? { timeoutMs: envelope.timeoutMs } : {}),\n\t\t\t\t\texecutionStarted: envelope.executionStarted === true,\n\t\t\t\t};\n\t\t\t\tthis._activeRunToolTermination = timeout;\n\t\t\t\tthis._appendRunJournalAudit(\"tool_timeout\", timeout);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (event.type !== \"tool_execution_late_settlement\") {\n\t\t\treturn;\n\t\t}\n\t\t// Fail closed: anything not classified as a read-category tool may write.\n\t\tconst potentiallyWriting = resolveToolTimeoutCategory(event.toolName) !== \"read\";\n\t\tthis._appendRunJournalAudit(\"tool_late_settlement\", {\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\tdisposition: event.disposition,\n\t\t\toutcome: event.outcome,\n\t\t\t...(potentiallyWriting ? { sessionRisk: \"elevated\" as const } : {}),\n\t\t});\n\t\tif (potentiallyWriting) {\n\t\t\tthis._sessionRiskLevel = \"elevated\";\n\t\t\tthis._workspaceMutationCount += 1;\n\t\t\tthis._invalidateContextBudgetCache({\n\t\t\t\ttype: \"worktreeFingerprint\",\n\t\t\t\tvalue: this._contextCacheWorktreeFingerprint(),\n\t\t\t});\n\t\t\tconst payload = { root: this._cwd, paths: [] as readonly string[] };\n\t\t\tthis._appendReplayEvent(\"workspace_mutation\", payload);\n\t\t\tthis._emit({\n\t\t\t\ttype: \"workspace_mutation\",\n\t\t\t\tsource: \"tool_late_settlement\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\tpayload,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _getBashSandboxPreflight(override?: BashSandboxPreflight): BashSandboxPreflight | undefined {\n\t\treturn this._bashRuntime.sandboxPreflight(override);\n\t}\n\n\tprivate async _getRequiredRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tconst result = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\tif (!result.ok) {\n\t\t\tif (result.error.startsWith(\"No API key found\")) {\n\t\t\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t\t\t}\n\t\t\tthrow new Error(result.error);\n\t\t}\n\t\tif (result.apiKey) {\n\t\t\treturn { apiKey: result.apiKey, headers: result.headers };\n\t\t}\n\n\t\tconst isOAuth = this._modelRegistry.isUsingOAuth(model);\n\t\tif (isOAuth) {\n\t\t\tthrow new Error(\n\t\t\t\t`Authentication failed for \"${model.provider}\". ` +\n\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t`Run '/login ${model.provider}' to re-authenticate.`,\n\t\t\t);\n\t\t}\n\t\tthrow new Error(formatNoApiKeyFoundMessage(model.provider));\n\t}\n\n\tprivate async _getCompactionRequestAuth(model: Model<any>): Promise<{\n\t\tapiKey?: string;\n\t\theaders?: Record<string, string>;\n\t}> {\n\t\tif (this.agent.streamFn === streamSimple) {\n\t\t\treturn this._getRequiredRequestAuth(model);\n\t\t}\n\n\t\tconst result = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\treturn result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};\n\t}\n\n\t/**\n\t * Install tool hooks once on the Agent instance.\n\t *\n\t * The callbacks read `this._extensionRunner` at execution time, so extension reload swaps in the\n\t * new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt\n\t * registered tool execution to the extension context. Tool call and tool result interception now\n\t * happens here instead of in wrappers.\n\t */\n\tprivate _installAgentToolHooks(): void {\n\t\tthis.agent.beforeToolCall = async ({ toolCall, args }) => {\n\t\t\tif (this._loadoutAccessPolicy && !this._loadoutAccessPolicy.activeTools.includes(toolCall.name)) {\n\t\t\t\tthrow new Error(`loadout: inactive tool: ${toolCall.name}`);\n\t\t\t}\n\t\t\tconst runner = this._extensionRunner;\n\t\t\tif (!runner.hasHandlers(\"tool_call\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn await runner.emitToolCall({\n\t\t\t\t\ttype: \"tool_call\",\n\t\t\t\t\ttoolName: toolCall.name,\n\t\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (err instanceof Error) {\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t\tthrow new Error(`Extension failed, blocking execution: ${String(err)}`);\n\t\t\t}\n\t\t};\n\n\t\tthis.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {\n\t\t\tconst runner = this._extensionRunner;\n\t\t\tif (!runner.hasHandlers(\"tool_result\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tconst hookResult = await runner.emitToolResult({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\ttoolName: toolCall.name,\n\t\t\t\ttoolCallId: toolCall.id,\n\t\t\t\tinput: args as Record<string, unknown>,\n\t\t\t\tcontent: result.content,\n\t\t\t\tdetails: result.details,\n\t\t\t\tisError,\n\t\t\t});\n\n\t\t\tif (!hookResult) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: hookResult.content,\n\t\t\t\tdetails: hookResult.details,\n\t\t\t\tisError: hookResult.isError ?? isError,\n\t\t\t};\n\t\t};\n\t}\n\n\t// =========================================================================\n\t// Event Subscription\n\t// =========================================================================\n\n\t/** Emit an event to all listeners */\n\tprivate _emit(event: AgentSessionEvent): void {\n\t\tfor (const l of this._eventListeners) {\n\t\t\tl(event);\n\t\t}\n\t}\n\n\tprivate _emitQueueUpdate(): void {\n\t\tthis._emit({\n\t\t\ttype: \"queue_update\",\n\t\t\tsteering: [...this._steeringMessages],\n\t\t\tfollowUp: [...this._followUpMessages],\n\t\t});\n\t}\n\n\t// Track last assistant message for auto-compaction check\n\tprivate _lastAssistantMessage: AssistantMessage | undefined = undefined;\n\n\t/** Internal handler for agent events - shared by subscribe and reconnect */\n\tprivate _handleAgentEvent = async (event: AgentEvent): Promise<void> => {\n\t\t// Required run lifecycle persistence executes before extension/user listeners.\n\t\tthis._handleRunLifecycleEvent(event);\n\n\t\t// When a user message starts, check if it's from either queue and remove it BEFORE emitting\n\t\t// This ensures the UI sees the updated queue state\n\t\tif (event.type === \"message_start\" && event.message.role === \"user\") {\n\t\t\tthis._overflowRecoveryAttempts = 0;\n\t\t\tconst messageText = this._getUserMessageText(event.message);\n\t\t\tif (messageText) {\n\t\t\t\t// Check steering queue first\n\t\t\t\tconst steeringIndex = this._steeringMessages.indexOf(messageText);\n\t\t\t\tif (steeringIndex !== -1) {\n\t\t\t\t\tthis._steeringMessages.splice(steeringIndex, 1);\n\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t} else {\n\t\t\t\t\t// Check follow-up queue\n\t\t\t\t\tconst followUpIndex = this._followUpMessages.indexOf(messageText);\n\t\t\t\t\tif (followUpIndex !== -1) {\n\t\t\t\t\t\tthis._followUpMessages.splice(followUpIndex, 1);\n\t\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Durable tool timeout / late-settlement audits (ALG004-A/B).\n\t\tthis._handleToolAuditEvent(event);\n\n\t\t// Emit to extensions first\n\t\tawait this._emitExtensionEvent(event);\n\t\tconst replacement = event.type === \"message_end\" ? this._messageEndReplacements.get(event) : undefined;\n\t\tif (event.type === \"message_end\") {\n\t\t\tthis._messageEndReplacements.delete(event);\n\t\t}\n\t\tconst finalizedEvent: AgentEvent =\n\t\t\treplacement === undefined ? event : (Object.freeze({ ...event, message: replacement }) as AgentEvent);\n\n\t\t// Notify all listeners\n\t\tthis._emit(\n\t\t\tfinalizedEvent.type === \"agent_end\"\n\t\t\t\t? { ...finalizedEvent, willRetry: this._willRetryAfterAgentEnd(finalizedEvent) }\n\t\t\t\t: finalizedEvent,\n\t\t);\n\n\t\t// Handle session persistence\n\t\tif (finalizedEvent.type === \"message_end\") {\n\t\t\t// Check if this is a custom message from extensions\n\t\t\tif (finalizedEvent.message.role === \"custom\") {\n\t\t\t\t// Persist as CustomMessageEntry\n\t\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\t\tfinalizedEvent.message.customType,\n\t\t\t\t\tfinalizedEvent.message.content,\n\t\t\t\t\tfinalizedEvent.message.display,\n\t\t\t\t\tfinalizedEvent.message.details,\n\t\t\t\t);\n\t\t\t} else if (\n\t\t\t\tfinalizedEvent.message.role === \"user\" ||\n\t\t\t\tfinalizedEvent.message.role === \"assistant\" ||\n\t\t\t\tfinalizedEvent.message.role === \"toolResult\"\n\t\t\t) {\n\t\t\t\t// Regular LLM message - persist as SessionMessageEntry\n\t\t\t\tthis.sessionManager.appendMessage(finalizedEvent.message);\n\t\t\t}\n\t\t\t// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere\n\n\t\t\t// Track assistant message for auto-compaction (checked on agent_end)\n\t\t\tif (finalizedEvent.message.role === \"assistant\") {\n\t\t\t\tthis._lastAssistantMessage = finalizedEvent.message;\n\n\t\t\t\tconst assistantMsg = finalizedEvent.message as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason !== \"error\") {\n\t\t\t\t\tthis._overflowRecoveryAttempts = 0;\n\t\t\t\t}\n\n\t\t\t\t// Reset retry counter immediately on successful assistant response\n\t\t\t\t// This prevents accumulation across multiple LLM calls within a turn\n\t\t\t\tif (assistantMsg.stopReason !== \"error\" && this._retryAttempt > 0) {\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\t\t});\n\t\t\t\t\tthis._retryAttempt = 0;\n\t\t\t\t\tthis._refusedModels.clear();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t};\n\n\tprivate _willRetryAfterAgentEnd(event: Extract<AgentEvent, { type: \"agent_end\" }>): boolean {\n\t\tconst settings = this.settingsManager.getRetrySettings();\n\t\tif (!settings.enabled) return false;\n\n\t\tfor (let i = event.messages.length - 1; i >= 0; i--) {\n\t\t\tconst message = event.messages[i];\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t\tconst maxRetries = retryBudgetForAssistantError(assistant, settings.maxRetries);\n\t\t\t\treturn this._retryAttempt < maxRetries && this._isRetryableError(assistant);\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** Extract text content from a message */\n\tprivate _getUserMessageText(message: Message): string {\n\t\tif (message.role !== \"user\") return \"\";\n\t\tconst content = message.content;\n\t\tif (typeof content === \"string\") return content;\n\t\tconst textBlocks = content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c) => (c as TextContent).text).join(\"\");\n\t}\n\n\t/** Find the last assistant message in agent state (including aborted ones) */\n\tprivate _findLastAssistantMessage(): AssistantMessage | undefined {\n\t\tconst messages = this.agent.state.messages;\n\t\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\t\tconst msg = messages[i];\n\t\t\tif (msg.role === \"assistant\") {\n\t\t\t\treturn msg as AssistantMessage;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate _replaceFinalizedMessage(target: AgentMessage, replacement: AgentMessage): AgentMessage {\n\t\tconst messages = this.agent.state.messages;\n\t\tconst matchingIndexes = messages.flatMap((message, index) => (isDeepStrictEqual(message, target) ? [index] : []));\n\t\tif (matchingIndexes.length !== 1) {\n\t\t\tthrow new Error(\n\t\t\t\tmatchingIndexes.length === 0\n\t\t\t\t\t? \"Finalized message was not found in agent state\"\n\t\t\t\t\t: \"Finalized message is ambiguous in agent state\",\n\t\t\t);\n\t\t}\n\n\t\tconst matchingIndex = matchingIndexes[0];\n\t\tif (matchingIndex === undefined) {\n\t\t\tthrow new Error(\"Finalized message was not found in agent state\");\n\t\t}\n\t\tconst finalizedReplacement = createImmutableMessageSnapshot(replacement);\n\t\tconst nextMessages = [...messages];\n\t\tnextMessages[matchingIndex] = finalizedReplacement;\n\t\tthis.agent.state.messages = nextMessages;\n\t\treturn finalizedReplacement;\n\t}\n\n\t/** Emit extension events based on agent events */\n\tprivate async _emitExtensionEvent(event: AgentEvent): Promise<void> {\n\t\tif (event.type === \"agent_start\") {\n\t\t\tthis._turnIndex = 0;\n\t\t\tawait this._extensionRunner.emit({ type: \"agent_start\" });\n\t\t} else if (event.type === \"agent_end\") {\n\t\t\tawait this._extensionRunner.emit({ type: \"agent_end\", messages: event.messages });\n\t\t\t// `agent_end` fires once per attempt. Extensions that release per-run\n\t\t\t// state need the boundary no retry follows, so mark it explicitly.\n\t\t\tif (!this._willRetryAfterAgentEnd(event)) {\n\t\t\t\tawait this._extensionRunner.emit({ type: \"agent_settled\", messages: event.messages });\n\t\t\t}\n\t\t} else if (event.type === \"turn_start\") {\n\t\t\tthis._turnMetricsState = {\n\t\t\t\tstartedAtEpochMs: Date.now(),\n\t\t\t\ttoolCalls: [],\n\t\t\t\ttoolStarts: new Map(),\n\t\t\t\trequestedModel: this.model ? `${this.model.provider}/${this.model.id}` : null,\n\t\t\t\trequestedThinking: this.thinkingMode === \"auto\" ? \"auto\" : this.thinkingLevel,\n\t\t\t};\n\t\t\tconst extensionEvent: TurnStartEvent = {\n\t\t\t\ttype: \"turn_start\",\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"turn_end\") {\n\t\t\tthis._recordTurnMetrics(event.message);\n\t\t\tconst extensionEvent: TurnEndEvent = {\n\t\t\t\ttype: \"turn_end\",\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\tmessage: event.message,\n\t\t\t\ttoolResults: event.toolResults,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t\tthis._turnIndex++;\n\t\t} else if (event.type === \"message_start\") {\n\t\t\tconst extensionEvent: MessageStartEvent = {\n\t\t\t\ttype: \"message_start\",\n\t\t\t\tmessage: event.message,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"message_update\") {\n\t\t\tconst extensionEvent: MessageUpdateEvent = {\n\t\t\t\ttype: \"message_update\",\n\t\t\t\tmessage: event.message,\n\t\t\t\tassistantMessageEvent: event.assistantMessageEvent,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"message_end\") {\n\t\t\tconst extensionEvent: MessageEndEvent = {\n\t\t\t\ttype: \"message_end\",\n\t\t\t\tmessage: event.message,\n\t\t\t};\n\t\t\tconst replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);\n\t\t\tif (replacement) {\n\t\t\t\tconst finalizedReplacement = this._replaceFinalizedMessage(event.message, replacement);\n\t\t\t\tthis._messageEndReplacements.set(event, finalizedReplacement);\n\t\t\t}\n\t\t} else if (event.type === \"tool_execution_start\") {\n\t\t\tthis._turnMetricsState?.toolStarts.set(event.toolCallId, Date.now());\n\t\t\tconst extensionEvent: ToolExecutionStartEvent = {\n\t\t\t\ttype: \"tool_execution_start\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\targs: event.args,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"tool_execution_update\") {\n\t\t\tconst extensionEvent: ToolExecutionUpdateEvent = {\n\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\targs: event.args,\n\t\t\t\tpartialResult: event.partialResult,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t} else if (event.type === \"tool_execution_end\") {\n\t\t\tthis._recordToolMetric(event.toolCallId, event.toolName, event.isError === true, event.result);\n\t\t\tconst extensionEvent: ToolExecutionEndEvent = {\n\t\t\t\ttype: \"tool_execution_end\",\n\t\t\t\ttoolCallId: event.toolCallId,\n\t\t\t\ttoolName: event.toolName,\n\t\t\t\tresult: event.result,\n\t\t\t\tisError: event.isError,\n\t\t\t};\n\t\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\t}\n\t}\n\n\t/**\n\t * Turn metrics sink for this session, created on first use.\n\t *\n\t * Writes to `<cwd>/.omk/metrics/`. Set `OMK_TURN_METRICS=0` to disable, or\n\t * `OMK_TURN_METRICS_DIR` to relocate. Returns `undefined` when disabled, so\n\t * every call site is a no-op rather than a branch.\n\t */\n\tprivate _metricsSink(): TurnMetricsSink | undefined {\n\t\tif (isDisabledEnvValue(process.env.OMK_TURN_METRICS)) return undefined;\n\t\tif (!this._turnMetricsSink) {\n\t\t\tconst dir = process.env.OMK_TURN_METRICS_DIR ?? joinPath(this._cwd, \".omk\", \"metrics\");\n\t\t\tthis._turnMetricsSink = new TurnMetricsSink({ dir });\n\t\t}\n\t\treturn this._turnMetricsSink;\n\t}\n\n\tprivate _recordToolMetric(toolCallId: string, toolName: string, isError: boolean, result: unknown): void {\n\t\tconst state = this._turnMetricsState;\n\t\tif (!state) return;\n\t\tconst startedAt = state.toolStarts.get(toolCallId);\n\t\tstate.toolStarts.delete(toolCallId);\n\t\tstate.toolCalls.push({\n\t\t\tname: toolName,\n\t\t\tdurationMs: startedAt === undefined ? 0 : Date.now() - startedAt,\n\t\t\tok: !isError,\n\t\t\t...(isError ? { error: firstTextContent(result) } : {}),\n\t\t});\n\t}\n\n\t/** Persist one turn. Metrics are advisory: a failure here never affects the turn. */\n\tprivate _recordTurnMetrics(message: AgentMessage): void {\n\t\tconst state = this._turnMetricsState;\n\t\tthis._turnMetricsState = undefined;\n\t\tif (!state) return;\n\t\tconst sink = this._metricsSink();\n\t\tif (!sink) return;\n\t\tconst assistant = message.role === \"assistant\" ? message : undefined;\n\t\tconst usage = assistant?.usage;\n\t\tconst selectedModel = this.model ? `${this.model.provider}/${this.model.id}` : null;\n\t\tconst runtimeProvenance = buildRuntimeProvenance({\n\t\t\trequestedModel: state.requestedModel,\n\t\t\tselectedModel,\n\t\t\tresponseModel: assistant?.responseModel ?? (assistant ? `${assistant.provider}/${assistant.model}` : null),\n\t\t\trequestedThinking: state.requestedThinking,\n\t\t\teffectiveThinking: this.thinkingLevel,\n\t\t\tsource: \"session\",\n\t\t});\n\t\ttry {\n\t\t\tsink.record({\n\t\t\t\tsessionId: this.sessionId,\n\t\t\t\tturnIndex: this._turnIndex,\n\t\t\t\tprovider: this.model?.provider,\n\t\t\t\tmodel: this.model?.id,\n\t\t\t\tstartedAtEpochMs: state.startedAtEpochMs,\n\t\t\t\tendedAtEpochMs: Date.now(),\n\t\t\t\tstopReason: assistant?.stopReason,\n\t\t\t\truntimeProvenance,\n\t\t\t\tusage: usage\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tinput: usage.input,\n\t\t\t\t\t\t\toutput: usage.output,\n\t\t\t\t\t\t\tcacheRead: usage.cacheRead,\n\t\t\t\t\t\t\tcacheWrite: usage.cacheWrite,\n\t\t\t\t\t\t\tcostUsd: usage.cost.total,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\ttoolCalls: state.toolCalls,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Never let observability break a turn.\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to agent events.\n\t * Session persistence is handled internally (saves messages on message_end).\n\t * Multiple listeners can be added. Returns unsubscribe function for this listener.\n\t */\n\tsubscribe(listener: AgentSessionEventListener): () => void {\n\t\tthis._eventListeners.push(listener);\n\n\t\t// Return unsubscribe function for this specific listener\n\t\treturn () => {\n\t\t\tconst index = this._eventListeners.indexOf(listener);\n\t\t\tif (index !== -1) {\n\t\t\t\tthis._eventListeners.splice(index, 1);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Temporarily disconnect from agent events.\n\t * User listeners are preserved and will receive events again after resubscribe().\n\t * Used internally during operations that need to pause event processing.\n\t */\n\tprivate _disconnectFromAgent(): void {\n\t\tif (this._unsubscribeAgent) {\n\t\t\tthis._unsubscribeAgent();\n\t\t\tthis._unsubscribeAgent = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Reconnect to agent events after _disconnectFromAgent().\n\t * Preserves all existing listeners.\n\t */\n\tprivate _reconnectToAgent(): void {\n\t\tif (this._unsubscribeAgent) return; // Already connected\n\t\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n\t}\n\n\t/**\n\t * Remove all listeners and disconnect from agent.\n\t * Call this when completely done with the session.\n\t */\n\tdispose(): void {\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tthis.abortRetry();\n\t\t\t\tthis.abortCompaction();\n\t\t\t\tthis.abortBranchSummary();\n\t\t\t\tthis.abortBash();\n\t\t\t\tthis.agent.abort();\n\t\t\t} catch {\n\t\t\t\t// Dispose must succeed even if an abort hook throws.\n\t\t\t}\n\n\t\t\tthis._extensionRunner.invalidate(\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured extension API 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().\",\n\t\t\t);\n\t\t\tthis._disconnectFromAgent();\n\t\t\tthis._eventListeners = [];\n\t\t\tif (this._contextBudgetCacheProvider instanceof DiskContextBudgetCacheProviderV2) {\n\t\t\t\t// Persist whatever the session rendered; a failed flush is never fatal.\n\t\t\t\tthis._contextBudgetCacheProvider.close();\n\t\t\t}\n\t\t\tthis._mcpManager?.close();\n\t\t\tthis._mcpManager = undefined;\n\t\t\tcleanupSessionResources(this.sessionId);\n\t\t} finally {\n\t\t\tconst ownerLease = this._ownedSessionOwnerLease;\n\t\t\tif (ownerLease) {\n\t\t\t\townerLease.release();\n\t\t\t\tif (this.sessionManager.getOwnerLease() === ownerLease) this.sessionManager.setOwnerLease(undefined);\n\t\t\t\tthis._ownedSessionOwnerLease = undefined;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Build the context-budget cache provider for this session.\n\t *\n\t * Defaults to a workspace-layer provider persisted under `<cwd>/.omk/cache`,\n\t * so a second session reuses representations the first one already rendered\n\t * (representation keys are content-addressed; see\n\t * `context-budget-v2-cache-keys.ts`). Set\n\t * `OMK_CONTEXT_GOVERNOR_CACHE=memory` to opt out, or\n\t * `OMK_CONTEXT_GOVERNOR_CACHE_DIR` to relocate the snapshot.\n\t */\n\tprivate _createContextBudgetCacheProvider(): ContextBudgetCacheProviderV2 {\n\t\tif (process.env.OMK_CONTEXT_GOVERNOR_CACHE === \"memory\") {\n\t\t\treturn createMemoryContextBudgetCacheProviderV2(\"session\");\n\t\t}\n\t\tconst dir =\n\t\t\tprocess.env.OMK_CONTEXT_GOVERNOR_CACHE_DIR ?? joinPath(this._cwd, \".omk\", \"cache\", \"context-budget-v2\");\n\t\ttry {\n\t\t\treturn createDiskContextBudgetCacheProviderV2({ dir, layer: \"workspace\" });\n\t\t} catch {\n\t\t\t// An unusable cache directory must not take the session down.\n\t\t\treturn createMemoryContextBudgetCacheProviderV2(\"session\");\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Read-only State Access\n\t// =========================================================================\n\n\t/** Full agent state */\n\tget state(): AgentState {\n\t\treturn this.agent.state;\n\t}\n\n\t/** Current model (may be undefined if not yet selected) */\n\tget model(): Model<any> | undefined {\n\t\treturn this.agent.state.model;\n\t}\n\n\t/** Current thinking level */\n\tget thinkingLevel(): ThinkingLevel {\n\t\treturn this.agent.state.thinkingLevel;\n\t}\n\n\t/** Current thinking mode (\"manual\" = user-selected level, \"auto\" = per-turn router) */\n\tget thinkingMode(): ThinkingMode {\n\t\treturn this._thinkingMode;\n\t}\n\n\t/** Whether agent is currently streaming a response */\n\tget isStreaming(): boolean {\n\t\treturn this.agent.state.isStreaming;\n\t}\n\n\t/** Current effective system prompt (includes any per-turn extension modifications) */\n\tget systemPrompt(): string {\n\t\treturn this.agent.state.systemPrompt;\n\t}\n\n\t/** Current retry attempt (0 if not retrying) */\n\tget retryAttempt(): number {\n\t\treturn this._retryAttempt;\n\t}\n\n\t/**\n\t * Get the names of currently active tools.\n\t * Returns the names of tools currently set on the agent.\n\t */\n\tgetActiveToolNames(): string[] {\n\t\treturn this.agent.state.tools.map((t) => t.name);\n\t}\n\n\t/**\n\t * Get all configured tools with name, description, parameter schema, prompt guidelines, and source metadata.\n\t */\n\tgetAllTools(): ToolInfo[] {\n\t\treturn Array.from(this._toolDefinitions.values()).map(({ definition, sourceInfo }) => ({\n\t\t\tname: definition.name,\n\t\t\tdescription: definition.description,\n\t\t\tparameters: definition.parameters,\n\t\t\tpromptGuidelines: definition.promptGuidelines,\n\t\t\tsourceInfo,\n\t\t}));\n\t}\n\n\tgetToolDefinition(name: string): ToolDefinition | undefined {\n\t\treturn this._toolDefinitions.get(name)?.definition;\n\t}\n\n\t/**\n\t * Set active tools by name.\n\t * Only tools in the registry can be enabled. Unknown tool names are ignored.\n\t * Also rebuilds the system prompt to reflect the new tool set.\n\t * Changes take effect on the next agent turn.\n\t */\n\tsetActiveToolsByName(toolNames: string[]): void {\n\t\tconst requestedToolNames = normalizeToolNames(toolNames);\n\t\tconst lockedToolNames = this._loadoutAccessPolicy?.activeTools;\n\t\tif (lockedToolNames && !toolNameSetsEqual(requestedToolNames, lockedToolNames)) {\n\t\t\tthrow new Error(\n\t\t\t\t`loadout active tools are locked: expected ${lockedToolNames.join(\", \") || \"(none)\"}, received ${requestedToolNames.join(\", \") || \"(none)\"}`,\n\t\t\t);\n\t\t}\n\n\t\tconst desiredToolNames = lockedToolNames ? [...lockedToolNames] : toolNames;\n\t\tconst tools: AgentTool[] = [];\n\t\tconst validToolNames: string[] = [];\n\t\tfor (const name of desiredToolNames) {\n\t\t\tconst tool = this._toolRegistry.get(name);\n\t\t\tif (tool) {\n\t\t\t\ttools.push(tool);\n\t\t\t\tvalidToolNames.push(name);\n\t\t\t} else if (lockedToolNames?.includes(name)) {\n\t\t\t\tthrow new Error(`loadout locked tool unavailable: ${name}`);\n\t\t\t}\n\t\t}\n\t\tthis.agent.state.tools = tools;\n\t\tthis._applyToolTimeoutCategoryDefaults(validToolNames);\n\n\t\t// Rebuild base system prompt with new tool set\n\t\tthis._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis._recordPromptCachePlan(\"tool-set\");\n\t}\n\n\t/**\n\t * Recompute the Agent's per-name timeout map for the active tool set:\n\t * explicit user/settings entries always win; active tools without an entry\n\t * receive their §6.3 category default (ALG004-C); uncategorized tools fall\n\t * through to the global `toolTimeoutMs`.\n\t */\n\tprivate _applyToolTimeoutCategoryDefaults(activeToolNames: readonly string[]): void {\n\t\ttry {\n\t\t\tconst resolved = resolveAgentToolSettings(this.settingsManager);\n\t\t\tthis.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, resolved.toolTimeouts);\n\t\t} catch {\n\t\t\t// Invalid settings fail closed at session creation; keep current\n\t\t\t// explicit entries and still fill category defaults for active tools.\n\t\t\tthis.agent.toolTimeouts = applyCategoryTimeoutDefaults(activeToolNames, this.agent.toolTimeouts ?? {});\n\t\t}\n\t}\n\n\t/** Whether compaction or branch summarization is currently running */\n\tget isCompacting(): boolean {\n\t\treturn (\n\t\t\tthis._autoCompactionAbortController !== undefined ||\n\t\t\tthis._compactionAbortController !== undefined ||\n\t\t\tthis._branchSummaryAbortController !== undefined\n\t\t);\n\t}\n\n\t/** All messages including custom types like BashExecutionMessage */\n\tget messages(): AgentMessage[] {\n\t\treturn this.agent.state.messages;\n\t}\n\n\t/** Current steering mode */\n\tget steeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.agent.steeringMode;\n\t}\n\n\t/** Current follow-up mode */\n\tget followUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.agent.followUpMode;\n\t}\n\n\t/** Current session file path, or undefined if sessions are disabled */\n\tget sessionFile(): string | undefined {\n\t\treturn this.sessionManager.getSessionFile();\n\t}\n\n\t/** Current session ID */\n\tget sessionId(): string {\n\t\treturn this.sessionManager.getSessionId();\n\t}\n\n\t/** Current session display name, if set */\n\tget sessionName(): string | undefined {\n\t\treturn this.sessionManager.getSessionName();\n\t}\n\n\t/** Scoped models for cycling (from --models flag) */\n\tget scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel?: ThinkingLevel }> {\n\t\treturn this._scopedModels;\n\t}\n\n\t/** Update scoped models for cycling */\n\tsetScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>): void {\n\t\tthis._scopedModels = scopedModels;\n\t}\n\n\t/** File-based prompt templates */\n\tget promptTemplates(): ReadonlyArray<PromptTemplate> {\n\t\treturn this._resourceLoader.getPrompts().prompts;\n\t}\n\n\tprivate _normalizePromptSnippet(text: string | undefined): string | undefined {\n\t\tif (!text) return undefined;\n\t\tconst oneLine = text\n\t\t\t.replace(/[\\r\\n]+/g, \" \")\n\t\t\t.replace(/\\s+/g, \" \")\n\t\t\t.trim();\n\t\treturn oneLine.length > 0 ? oneLine : undefined;\n\t}\n\n\tprivate _normalizePromptGuidelines(guidelines: string[] | undefined): string[] {\n\t\tif (!guidelines || guidelines.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst unique = new Set<string>();\n\t\tfor (const guideline of guidelines) {\n\t\t\tconst normalized = guideline.trim();\n\t\t\tif (normalized.length > 0) {\n\t\t\t\tunique.add(normalized);\n\t\t\t}\n\t\t}\n\t\treturn Array.from(unique);\n\t}\n\n\t/**\n\t * Extract the most recent user query text from conversation messages.\n\t * Returns undefined when no user message exists or content is empty.\n\t * Handles both string content and multimodal (array) content.\n\t */\n\tprivate _extractCurrentQuery(): string | undefined {\n\t\tconst messages = this.messages;\n\t\tif (messages.length === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\t// Scan recent messages (last 3) for the most recent user message\n\t\tconst recent = messages.slice(-3);\n\t\tfor (let i = recent.length - 1; i >= 0; i--) {\n\t\t\tconst msg = recent[i];\n\t\t\tif (!msg || !(\"role\" in msg) || msg.role !== \"user\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst content = (msg as { content: string | (TextContent | ImageContent)[] }).content;\n\t\t\tif (typeof content === \"string\") {\n\t\t\t\tconst trimmed = content.trim();\n\t\t\t\treturn trimmed.length > 0 ? trimmed : undefined;\n\t\t\t}\n\t\t\tif (Array.isArray(content)) {\n\t\t\t\tconst textParts = content.flatMap((item) => (item.type === \"text\" ? [item.text] : []));\n\t\t\t\tconst joined = textParts.join(\"\\n\").trim();\n\t\t\t\treturn joined.length > 0 ? joined : undefined;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate _getContextBudgetOptions(\n\t\tqueryContext = this._extractCurrentQuery(),\n\t\tmodel: Model<any> | undefined = this.model,\n\t): BuildSystemPromptOptions[\"contextBudget\"] | undefined {\n\t\tconst contextGovernorOverride = process.env.OMK_CONTEXT_GOVERNOR;\n\t\tif (contextGovernorOverride === \"0\") {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (contextGovernorOverride !== \"1\" && !this.settingsManager.getContextBudgetEnabled()) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst contextWindow = model?.contextWindow ?? 0;\n\t\tconst budget = computePromptTokenBudget({\n\t\t\tcontextWindow,\n\t\t\tmodelMaxTokens: model?.maxTokens,\n\t\t\tenvMaxPromptTokens: parsePositiveIntegerEnv(\"OMK_CONTEXT_GOVERNOR_MAX_PROMPT_TOKENS\"),\n\t\t\tenvResponseReserveTokens: parsePositiveIntegerEnv(\"OMK_CONTEXT_GOVERNOR_RESPONSE_RESERVE_TOKENS\"),\n\t\t\tenvPromptRatio: parsePositiveFloatEnv(\"OMK_CONTEXT_GOVERNOR_PROMPT_RATIO\"),\n\t\t\tenvResponseRatio: parsePositiveFloatEnv(\"OMK_CONTEXT_GOVERNOR_RESPONSE_RATIO\"),\n\t\t});\n\t\tconst maxPromptTokens = budget.maxPromptTokens;\n\t\tconst responseReserveTokens = budget.responseReserveTokens;\n\n\t\tconst cacheProvider = this._contextBudgetCacheProvider ?? this._createContextBudgetCacheProvider();\n\t\tcacheProvider.setInvalidationSnapshot?.(this._contextCacheInvalidationSnapshot);\n\t\tthis._contextBudgetCacheProvider = cacheProvider;\n\n\t\treturn {\n\t\t\tmaxPromptTokens,\n\t\t\tresponseReserveTokens,\n\t\t\tmodelId: model?.id ?? \"unknown\",\n\t\t\ttokenizerMode: parseTokenizerModeEnv(process.env.OMK_CONTEXT_GOVERNOR_TOKENIZER),\n\t\t\tactiveSkillNames: parseCommaSeparatedEnv(process.env.OMK_CONTEXT_GOVERNOR_ACTIVE_SKILLS),\n\t\t\tqueryContext,\n\t\t\tcacheProvider,\n\t\t};\n\t}\n\n\t/**\n\t * Compute responseReserveTokens from contextWindow.\n\t * Prefers the model's own maxTokens when available, otherwise uses a ratio.\n\t */\n\tprivate _recordPromptCachePlan(reason: string): void {\n\t\tconst boundary = this.agent.state.systemPromptCacheBoundary;\n\t\tconst scope = this.model ? `${this.model.provider}/${this.model.id}` : \"unknown\";\n\t\tconst nextKey = deriveContextPromptCacheKey(\n\t\t\t{\n\t\t\t\tsystemPrompt: this.agent.state.systemPrompt,\n\t\t\t\tsystemPromptCacheBoundary: boundary,\n\t\t\t\tsystemPromptCacheBoundaryBypass: this.agent.state.systemPromptCacheBoundaryBypass,\n\t\t\t\tmessages: [],\n\t\t\t\ttools: this.agent.state.tools,\n\t\t\t},\n\t\t\tscope,\n\t\t);\n\t\tconst transition = classifyPromptCacheTransition(this._promptCacheKey, nextKey);\n\t\tif (transition.kind === \"bypass\") {\n\t\t\tthis._promptCacheBoundaryBypasses += 1;\n\t\t\tthis._promptCacheStablePrefixCharacters = 0;\n\t\t\tif (transition.recordBreak) {\n\t\t\t\tthis._promptCacheLastBreakReason = reason;\n\t\t\t}\n\t\t\tthis._promptCacheKey = undefined;\n\t\t\treturn;\n\t\t}\n\t\tif (transition.kind === \"changed\") {\n\t\t\tthis._promptCacheKeyChanges += 1;\n\t\t\tthis._promptCacheLastBreakReason = reason;\n\t\t}\n\t\tthis._promptCacheKey = nextKey;\n\t\tthis._promptCacheStablePrefixCharacters = boundary ?? 0;\n\t}\n\n\tprivate _getDefaultActiveSkills(): string[] {\n\t\tconst trustedSkillNames = new Set(\n\t\t\tthis._resourceLoader\n\t\t\t\t.getSkills()\n\t\t\t\t.skills.filter((skill) => skill.sourceInfo.scope === \"user\")\n\t\t\t\t.map((skill) => skill.name),\n\t\t);\n\t\treturn this.settingsManager.getDefaultActiveSkills().filter((name) => trustedSkillNames.has(name));\n\t}\n\n\tprivate _rebuildSystemPrompt(toolNames: string[], model: Model<any> | undefined = this.model): string {\n\t\tconst defaultActiveSkills = this._getDefaultActiveSkills();\n\t\tconst assembled = assembleSessionSystemPrompt({\n\t\t\tcwd: this._cwd,\n\t\t\ttoolNames,\n\t\t\thasTool: (name) => this._toolRegistry.has(name),\n\t\t\ttoolPromptSnippets: this._toolPromptSnippets,\n\t\t\ttoolPromptGuidelines: this._toolPromptGuidelines,\n\t\t\tcustomPrompt: this._resourceLoader.getSystemPrompt(),\n\t\t\tappendSystemPrompt: this._resourceLoader.getAppendSystemPrompt(),\n\t\t\tproviderAppend: grokPlaybookAppendForProvider(model?.provider),\n\t\t\tskills: this._resourceLoader.getSkills().skills,\n\t\t\tactiveSkillNames: defaultActiveSkills,\n\t\t\tactiveSkillSource: defaultActiveSkills.length > 0 ? \"settings\" : undefined,\n\t\t\tcontextFiles: selectContextFilesForModel(this._resourceLoader.getAgentsFiles().agentsFiles, model),\n\t\t\tcontextBudget: this._getContextBudgetOptions(undefined, model),\n\t\t});\n\t\tthis._baseSystemPromptOptions = assembled.options;\n\t\tthis._baseSystemPromptCacheBoundary = assembled.cacheBoundary;\n\t\treturn assembled.prompt;\n\t}\n\n\t// =========================================================================\n\t// Prompting\n\t// =========================================================================\n\n\tprivate async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> {\n\t\t// One identity per top-level run (§16.2): internal retries and\n\t\t// continuations inside this call share it, as does the resource lease.\n\t\tconst promptRunId = `prompt-run-${randomUUID()}`;\n\t\tconst startedAtEpochMs = Date.now();\n\t\t// Roadmap M2: the lease spans the whole run including internal retries\n\t\t// and continuations, so they share one admission decision (§8.3).\n\t\tconst resourceLease = await this._beginResourceGovernedRun(promptRunId);\n\t\tconst resourceObservations = this._resourceObservationJournals.get(promptRunId) ?? null;\n\t\tlet outcome: PromptSettlementOutcome = \"completed\";\n\t\ttry {\n\t\t\tawait this.agent.prompt(messages);\n\t\t\twhile (await this._handlePostAgentRun()) {\n\t\t\t\tawait this.agent.continue();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\toutcome =\n\t\t\t\tthis._userAbortRequested || (error instanceof Error && /abort/i.test(error.message)) ? \"aborted\" : \"failed\";\n\t\t\tthis._publishRuntimeFailure(error);\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tif (resourceLease !== null) {\n\t\t\t\t// Generation-safe: a stale release after another run acquired the\n\t\t\t\t// lease is a no-op instead of clobbering the newer cap (§8.1).\n\t\t\t\tthis._resourceLeaseController?.release(resourceLease);\n\t\t\t\tresourceObservations?.record(\"resource_lease_released_v1\", { promptRunId });\n\t\t\t}\n\t\t\tthis._flushPendingBashMessages();\n\t\t\t// §16.4 (M4): settle only after every in-run continuation drained.\n\t\t\tthis._emitPromptSettledIfReady({ promptRunId, startedAtEpochMs, outcome, resourceObservations });\n\t\t}\n\t}\n\n\t/**\n\t * Emit `prompt_settled` when the §16.4 conditions hold. Called exactly\n\t * once per promptRunId (from `_runAgentPrompt`'s finally), so the same\n\t * run can never emit twice; late-queued messages block emission\n\t * conservatively — the continuation run settles under its own id.\n\t * Settlement is a UX signal only and must never affect the run (§16.1).\n\t */\n\tprivate _emitPromptSettledIfReady(input: {\n\t\treadonly promptRunId: string;\n\t\treadonly startedAtEpochMs: number;\n\t\treadonly outcome: PromptSettlementOutcome;\n\t\treadonly resourceObservations: ResourceObservationJournal | null;\n\t}): void {\n\t\ttry {\n\t\t\tlet state = createPromptSettlementState(input.promptRunId, input.startedAtEpochMs);\n\t\t\tstate = reducePromptSettlement(state, { kind: \"terminal\", outcome: input.outcome });\n\t\t\tif (this.isStreaming) {\n\t\t\t\tstate = reducePromptSettlement(state, { kind: \"tool\", delta: 1 });\n\t\t\t}\n\t\t\tif (this.agent.hasQueuedMessages()) {\n\t\t\t\tstate = reducePromptSettlement(state, { kind: \"continuation\", delta: 1 });\n\t\t\t}\n\t\t\tconst settled = settlePromptIfReady(state, Date.now());\n\t\t\tif (settled.event !== null) {\n\t\t\t\tinput.resourceObservations?.record(\"prompt_settled_v1\", settledObservationFacts(settled.event));\n\t\t\t\tthis._emit(settled.event);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Never let settlement bookkeeping break a run.\n\t\t}\n\t}\n\n\t/**\n\t * Resource governor preflight for one top-level run (roadmap §8, §25.3 M2).\n\t *\n\t * - `off`: nothing; v0.96.1 observable behavior is preserved.\n\t * - `observe` (default): fire-and-forget probe records a decision for the\n\t * §19.4 read-only API; the cap never changes and the prompt is never\n\t * delayed. The pending probe is bounded (~cpuSampleMs) and never rejects.\n\t * - `adaptive`/`strict`: bounded blocking probe (default 300 ms deadline),\n\t * then a generation-safe lease applies the effective tool cap for this\n\t * run. The caller's `finally` releases it exactly once.\n\t *\n\t * Never throws: any probe or policy failure leaves this run ungoverned\n\t * (§2.1: probe failure must not crash or block the prompt).\n\t */\n\tprivate async _beginResourceGovernedRun(promptRunId: string): Promise<RunResourceLease | null> {\n\t\ttry {\n\t\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\t\tif (resolved.mode === \"off\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst resourceObservations = this._openResourceObservations(promptRunId);\n\t\t\tconst probeOptions = {\n\t\t\t\tcwd: this._cwd,\n\t\t\t\tmaxProbeMs: resolved.maxProbeMs,\n\t\t\t\tcpuSampleMs: resolved.cpuSampleMs,\n\t\t\t};\n\t\t\t// §7.3: Agent.maxToolConcurrency uses undefined for \"unlimited\"; the\n\t\t\t// admission contract uses 0 for the same meaning.\n\t\t\tconst configuredCaps = { maxToolConcurrency: this.agent.maxToolConcurrency ?? 0 };\n\t\t\tif (resolved.mode === \"observe\") {\n\t\t\t\tvoid captureHostResourceSnapshot(probeOptions)\n\t\t\t\t\t.then((snapshot) => {\n\t\t\t\t\t\tconst decision = decideResourceAdmission({\n\t\t\t\t\t\t\tsnapshot,\n\t\t\t\t\t\t\tconfig: resolved.admission,\n\t\t\t\t\t\t\tconfiguredCaps,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\t\t\t\t\tthis._lastResourceAdmission = decision;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresourceObservations?.record(\"resource_snapshot_v1\", snapshotObservationFacts(snapshot));\n\t\t\t\t\t\tresourceObservations?.record(\"resource_admission_v1\", admissionObservationFacts(decision));\n\t\t\t\t\t})\n\t\t\t\t\t.catch(() => {});\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst snapshot = await captureHostResourceSnapshot(probeOptions);\n\t\t\tconst decision = decideResourceAdmission({ snapshot, config: resolved.admission, configuredCaps });\n\t\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\t\tthis._lastResourceAdmission = decision;\n\t\t\t}\n\t\t\tresourceObservations?.record(\"resource_snapshot_v1\", snapshotObservationFacts(snapshot));\n\t\t\tresourceObservations?.record(\"resource_admission_v1\", admissionObservationFacts(decision));\n\t\t\tthis._resourceLeaseController ??= new RunResourceLeaseController({\n\t\t\t\tgetCap: () => this.agent.maxToolConcurrency,\n\t\t\t\tsetCap: (cap) => {\n\t\t\t\t\tthis.agent.maxToolConcurrency = cap;\n\t\t\t\t},\n\t\t\t});\n\t\t\tconst lease = this._resourceLeaseController.acquire({ promptRunId, decision });\n\t\t\tresourceObservations?.record(\"resource_lease_acquired_v1\", {\n\t\t\t\tdecisionId: decision.decisionId,\n\t\t\t\tappliedToolCap: decision.maxToolConcurrency,\n\t\t\t});\n\t\t\treturn lease;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate _openResourceObservations(promptRunId: string): ResourceObservationJournal | null {\n\t\tthis._latestResourcePromptRunId = promptRunId;\n\t\ttry {\n\t\t\tconst journal = ResourceObservationJournal.open(this._cwd, promptRunId);\n\t\t\tthis._resourceObservations = journal;\n\t\t\tthis._resourceObservationJournals.set(promptRunId, journal);\n\t\t\twhile (this._resourceObservationJournals.size > MAX_RESOURCE_OBSERVATION_JOURNALS) {\n\t\t\t\tconst oldestPromptRunId = this._resourceObservationJournals.keys().next().value;\n\t\t\t\tif (oldestPromptRunId === undefined) break;\n\t\t\t\tthis._resourceObservationJournals.delete(oldestPromptRunId);\n\t\t\t}\n\t\t\treturn journal;\n\t\t} catch {\n\t\t\tthis._resourceObservations = null;\n\t\t\tthis._resourceObservationJournals.delete(promptRunId);\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/** §20.2: record the TUI-side completion sound result in its originating run journal. */\n\trecordCompletionSoundResult(\n\t\tpromptRunId: string,\n\t\tresult: import(\"./completion-sound.ts\").CompletionSoundResult,\n\t): void {\n\t\tconst journal = this._resourceObservationJournals.get(promptRunId);\n\t\tjournal?.record(\"completion_sound_result_v1\", soundObservationFacts(result));\n\t\tthis._resourceObservationJournals.delete(promptRunId);\n\t\tif (this._latestResourcePromptRunId === promptRunId) {\n\t\t\tthis._latestResourcePromptRunId = null;\n\t\t\tif (this._resourceObservations === journal) this._resourceObservations = null;\n\t\t}\n\t}\n\n\t/** §19.4 read-only SDK surface: the most recent resource admission decision, if any. */\n\tgetCurrentResourceAdmission(): ResourceAdmissionDecision | null {\n\t\treturn this._lastResourceAdmission;\n\t}\n\n\t/** §19.4 read-only SDK surface: capture a fresh host resource snapshot. */\n\tasync getHostResourceSnapshot(): Promise<HostResourceSnapshot> {\n\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\treturn captureHostResourceSnapshot({\n\t\t\tcwd: this._cwd,\n\t\t\tmaxProbeMs: resolved.maxProbeMs,\n\t\t\tcpuSampleMs: resolved.cpuSampleMs,\n\t\t});\n\t}\n\n\t/** §19.4 read-only SDK surface: shared workload permit pool state, if any. */\n\tgetWorkloadPermitSnapshot(): WorkloadPermitPoolSnapshot | null {\n\t\treturn this._workloadPermitPool?.snapshot() ?? null;\n\t}\n\n\t/**\n\t * §14.1 shared-budget seam: child subagents must reuse this instance\n\t * instead of creating a private pool (host oversubscription). The live\n\t * child launcher wiring lands in M6.\n\t */\n\tget workloadPermitPool(): WorkloadPermitPool {\n\t\tthis._workloadPermitPool ??= new WorkloadPermitPool();\n\t\treturn this._workloadPermitPool;\n\t}\n\n\t/**\n\t * Resource safety gate for the bash boundary (roadmap §9.4/§11, M3).\n\t * Runs only in adaptive/strict mode with a recorded admission decision;\n\t * observe/off keep v0.96.1 behavior. Never throws: gate machinery\n\t * failures leave the command ungated (§2.1), while computed block\n\t * verdicts and permit rejections return a bounded §11.3 payload.\n\t */\n\tprivate async _acquireBashResourcePermit(command: string, signal: AbortSignal): Promise<BashResourcePermitGrant> {\n\t\ttry {\n\t\t\tconst resolved = resolveResourceGovernorSettings(this.settingsManager.getResourceGovernorSettings());\n\t\t\tif (resolved.mode !== \"adaptive\" && resolved.mode !== \"strict\") {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst decision = this._lastResourceAdmission;\n\t\t\tif (decision === null) {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tconst classification = classifyWorkloadCommand(command);\n\t\t\tthis._resourceObservations?.record(\n\t\t\t\t\"workload_classification_v1\",\n\t\t\t\tclassificationObservationFacts(classification),\n\t\t\t);\n\t\t\tconst verdict = decideResourceSafetyGate({ commandSafety: \"allowed\", classification, decision });\n\t\t\tif (verdict.kind === \"allow\") {\n\t\t\t\treturn {};\n\t\t\t}\n\t\t\tif (verdict.kind === \"block\") {\n\t\t\t\treturn { blocked: JSON.stringify(verdict.block) };\n\t\t\t}\n\t\t\tthis._workloadPermitPool ??= new WorkloadPermitPool({ capacity: decision.maxHeavyProcesses });\n\t\t\tconst pool = this._workloadPermitPool;\n\t\t\tpool.setCapacity(decision.maxHeavyProcesses);\n\t\t\tconst waitStartedMs = Date.now();\n\t\t\tthis._resourceObservations?.record(\"workload_permit_wait_v1\", {\n\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\tweight: verdict.weight,\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst permit = await pool.acquire({\n\t\t\t\t\trequestId: `bash-${randomUUID()}`,\n\t\t\t\t\tpromptRunId: this._resourceLeaseController?.activeLease?.promptRunId ?? \"unleased\",\n\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\tsignal,\n\t\t\t\t\t// Bounded §10.3 timeout-aware wait; a settings knob can follow later.\n\t\t\t\t\ttimeoutMs: 60_000,\n\t\t\t\t});\n\t\t\t\tthis._resourceObservations?.record(\"workload_permit_acquired_v1\", {\n\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\twaitMs: Date.now() - waitStartedMs,\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\trelease: () => {\n\t\t\t\t\t\tpermit.release();\n\t\t\t\t\t\tthis._resourceObservations?.record(\"workload_permit_released_v1\", {\n\t\t\t\t\t\t\tworkloadClass: classification.workloadClass,\n\t\t\t\t\t\t\tweight: verdict.weight,\n\t\t\t\t\t\t});\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\tconst code = error instanceof WorkloadPermitError ? error.code : \"queue_overflow\";\n\t\t\t\treturn {\n\t\t\t\t\tblocked: JSON.stringify({\n\t\t\t\t\t\tkind: \"resource_pressure\",\n\t\t\t\t\t\tpressure: decision.pressure,\n\t\t\t\t\t\taction: \"defer-heavy\",\n\t\t\t\t\t\treasonCodes: [...decision.reasons, `resource.permit.${code}`],\n\t\t\t\t\t\trequiredAction: RESOURCE_PRESSURE_REQUIRED_ACTION,\n\t\t\t\t\t}),\n\t\t\t\t};\n\t\t\t}\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async _handlePostAgentRun(): Promise<boolean> {\n\t\tconst msg = this._lastAssistantMessage;\n\t\tthis._lastAssistantMessage = undefined;\n\t\tif (!msg) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (msg.stopReason === \"error\" && this._retryAttempt > 0) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\tfinalError: msg.errorMessage,\n\t\t\t});\n\t\t\tthis._retryAttempt = 0;\n\t\t\tthis._refusedModels.clear();\n\t\t}\n\n\t\tif (await this._checkCompaction(msg)) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// The agent loop drains both queues before emitting agent_end. Any messages\n\t\t// here were queued by agent_end extension handlers and need a continuation.\n\t\treturn this.agent.hasQueuedMessages();\n\t}\n\n\t/**\n\t * Send a prompt to the agent.\n\t * - Handles extension commands (registered via omk.registerCommand) immediately, even during streaming\n\t * - Expands file-based prompt templates by default\n\t * - During streaming, queues via steer() or followUp() based on streamingBehavior option\n\t * - Validates model and API key before sending (when not streaming)\n\t * @throws Error if streaming and no streamingBehavior specified\n\t * @throws Error if no model selected or no API key available (when not streaming)\n\t */\n\tasync prompt(text: string, options?: PromptOptions): Promise<void> {\n\t\tconst expandPromptTemplates = options?.expandPromptTemplates ?? true;\n\t\tconst preflightResult = options?.preflightResult;\n\t\tlet currentText = redactSensitiveText(text);\n\t\tconst defaultActiveSkills = this._getDefaultActiveSkills();\n\t\tlet promptActiveSkillNames = mergePromptActiveSkillNames(defaultActiveSkills, options?.activeSkillNames ?? []);\n\t\tlet promptActiveSkillSource =\n\t\t\tdefaultActiveSkills.length > 0\n\t\t\t\t? options?.activeSkillSource\n\t\t\t\t\t? `settings+${options.activeSkillSource}`\n\t\t\t\t\t: \"settings\"\n\t\t\t\t: options?.activeSkillSource;\n\t\tlet isBangSkillInvocation = false;\n\t\tif (expandPromptTemplates) {\n\t\t\tconst bangInvocation = parseBangInvocation(text, {\n\t\t\t\thasSkill: (name) => this._resourceLoader.getSkills().skills.some((skill) => skill.name === name),\n\t\t\t});\n\t\t\tif (bangInvocation.kind === \"skill\") {\n\t\t\t\tisBangSkillInvocation = true;\n\t\t\t\tcurrentText = bangInvocation.prompt\n\t\t\t\t\t? `/skill:${bangInvocation.skillName} ${bangInvocation.prompt}`\n\t\t\t\t\t: `/skill:${bangInvocation.skillName}`;\n\t\t\t\tpromptActiveSkillNames = mergePromptActiveSkillNames(\n\t\t\t\t\tpromptActiveSkillNames,\n\t\t\t\t\tbangInvocation.activeSkillNames,\n\t\t\t\t);\n\t\t\t\tpromptActiveSkillSource =\n\t\t\t\t\tdefaultActiveSkills.length > 0 ? `settings+${bangInvocation.source}` : bangInvocation.source;\n\t\t\t}\n\t\t}\n\t\tlet messages: AgentMessage[] | undefined;\n\n\t\ttry {\n\t\t\t// Handle extension commands first (execute immediately, even during streaming)\n\t\t\t// Extension commands manage their own LLM interaction via omk.sendMessage()\n\t\t\tif (expandPromptTemplates && !isBangSkillInvocation && currentText.startsWith(\"/\")) {\n\t\t\t\tconst handled = await this._tryExecuteExtensionCommand(currentText);\n\t\t\t\tif (handled) {\n\t\t\t\t\t// Extension command executed, no prompt to send\n\t\t\t\t\tpreflightResult?.(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Emit input event for extension interception (before skill/template expansion)\n\t\t\tlet currentImages = options?.images;\n\t\t\tif (this._extensionRunner.hasHandlers(\"input\")) {\n\t\t\t\tconst inputResult = await this._extensionRunner.emitInput(\n\t\t\t\t\tcurrentText,\n\t\t\t\t\tcurrentImages,\n\t\t\t\t\toptions?.source ?? \"interactive\",\n\t\t\t\t\tthis.isStreaming ? options?.streamingBehavior : undefined,\n\t\t\t\t);\n\t\t\t\tif (inputResult.action === \"handled\") {\n\t\t\t\t\tpreflightResult?.(true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (inputResult.action === \"transform\") {\n\t\t\t\t\tcurrentText = inputResult.text;\n\t\t\t\t\tcurrentImages = inputResult.images ?? currentImages;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Expand skill commands (/skill:name args) and prompt templates (/template args)\n\t\t\tlet expandedText = currentText;\n\t\t\tif (expandPromptTemplates) {\n\t\t\t\texpandedText = this._expandSkillCommand(expandedText);\n\t\t\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\t\t\t}\n\t\t\texpandedText = redactSensitiveText(expandedText);\n\n\t\t\t// If streaming, queue via steer() or followUp() based on option\n\t\t\tif (this.isStreaming) {\n\t\t\t\tif (!options?.streamingBehavior) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (options.streamingBehavior === \"followUp\") {\n\t\t\t\t\tawait this._queueFollowUp(expandedText, currentImages);\n\t\t\t\t} else {\n\t\t\t\t\tawait this._queueSteer(expandedText, currentImages);\n\t\t\t\t}\n\t\t\t\tpreflightResult?.(true);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Flush any pending bash messages before the new prompt\n\t\t\tthis._flushPendingBashMessages();\n\n\t\t\t// Validate model\n\t\t\tif (!this.model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\t// Root-level: eject sticky safety models (Fable) before the turn leaves the machine.\n\t\t\tawait this._ejectStickySafetyModelIfNeeded();\n\n\t\t\t// Grok OAuth: refuse Imagine ids on the chat/completions path (tool-only).\n\t\t\tassertTextChatModelForCompletion(this.model.id, this.model.provider);\n\n\t\t\tif (!this._modelRegistry.hasConfiguredAuth(this.model)) {\n\t\t\t\tconst isOAuth = this._modelRegistry.isUsingOAuth(this.model);\n\t\t\t\tif (isOAuth) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Authentication failed for \"${this.model.provider}\". ` +\n\t\t\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t\t\t`Run '/login ${this.model.provider}' to re-authenticate.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow new Error(formatNoApiKeyFoundMessage(this.model.provider));\n\t\t\t}\n\n\t\t\t// Check if we need to compact before sending (catches aborted responses)\n\t\t\tconst lastAssistant = this._findLastAssistantMessage();\n\t\t\tif (lastAssistant && (await this._checkCompaction(lastAssistant, false))) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.agent.continue();\n\t\t\t\t\twhile (await this._handlePostAgentRun()) {\n\t\t\t\t\t\tawait this.agent.continue();\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tthis._flushPendingBashMessages();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Auto thinking mode: resolve this turn's level from the prompt content.\n\t\t\t// Manual mode never enters the router, so /think <level> always wins.\n\t\t\tthis._applyAutoThinkingLevelForTurn(expandedText);\n\n\t\t\t// Build messages array (custom message if any, then user message)\n\t\t\tmessages = [];\n\n\t\t\t// Add user message\n\t\t\tconst userContent: (TextContent | ImageContent)[] = [{ type: \"text\", text: expandedText }];\n\t\t\tif (currentImages) {\n\t\t\t\tuserContent.push(...currentImages);\n\t\t\t}\n\t\t\tmessages.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: userContent,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\n\t\t\t// Inject any pending \"nextTurn\" messages as context alongside the user message\n\t\t\tfor (const msg of this._pendingNextTurnMessages) {\n\t\t\t\tmessages.push(msg);\n\t\t\t}\n\t\t\tthis._pendingNextTurnMessages = [];\n\n\t\t\tconst turnSystemPromptOptions = {\n\t\t\t\t...this._baseSystemPromptOptions,\n\t\t\t\tcontextBudget: this._getContextBudgetOptions(expandedText),\n\t\t\t\tactiveSkillNames: promptActiveSkillNames,\n\t\t\t\tactiveSkillSource: promptActiveSkillSource,\n\t\t\t};\n\t\t\tconst turnSystemPrompt = buildSystemPromptPlan(turnSystemPromptOptions);\n\n\t\t\t// Emit before_agent_start extension event\n\t\t\tconst result = await this._extensionRunner.emitBeforeAgentStart(\n\t\t\t\texpandedText,\n\t\t\t\tcurrentImages,\n\t\t\t\tturnSystemPrompt.prompt,\n\t\t\t\tturnSystemPromptOptions,\n\t\t\t);\n\t\t\t// Add all custom messages from extensions\n\t\t\tif (result?.messages) {\n\t\t\t\tfor (const msg of result.messages) {\n\t\t\t\t\tmessages.push({\n\t\t\t\t\t\trole: \"custom\",\n\t\t\t\t\t\tcustomType: msg.customType,\n\t\t\t\t\t\tcontent: msg.content,\n\t\t\t\t\t\tdisplay: msg.display,\n\t\t\t\t\t\tdetails: msg.details,\n\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Apply extension-modified system prompt, or reset to the turn plan.\n\t\t\tif (result?.systemPrompt) {\n\t\t\t\tconst preservesCacheBoundary = result.systemPrompt === turnSystemPrompt.prompt;\n\t\t\t\tthis.agent.state.systemPrompt = result.systemPrompt;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundary = preservesCacheBoundary\n\t\t\t\t\t? turnSystemPrompt.cacheBoundary\n\t\t\t\t\t: undefined;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundaryBypass = !preservesCacheBoundary;\n\t\t\t} else {\n\t\t\t\t// Ensure we're using the turn prompt (in case the previous turn had modifications).\n\t\t\t\tthis.agent.state.systemPrompt = turnSystemPrompt.prompt;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundary = turnSystemPrompt.cacheBoundary;\n\t\t\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\t\t}\n\t\t\tthis._recordPromptCachePlan(result?.systemPrompt ? \"extension-override\" : \"turn-plan\");\n\n\t\t\tawait this._checkProjectedCompaction(messages);\n\t\t} catch (error) {\n\t\t\tpreflightResult?.(false);\n\t\t\tconst rawMessage = error instanceof Error ? error.message : String(error);\n\t\t\tconst cause = this._classifyPreflightCause(rawMessage);\n\t\t\tconst timestamp = new Date().toISOString();\n\t\t\tthis._publishTermination(\n\t\t\t\tclassifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId: `preflight-${randomUUID()}`,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: this._terminationMessage(rawMessage, \"Prompt preflight failed.\"),\n\t\t\t\t\tcause,\n\t\t\t\t\tsideEffects: \"none\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tthrow error;\n\t\t}\n\n\t\tif (!messages) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreflightResult?.(true);\n\t\tawait this._runAgentPrompt(messages);\n\t}\n\n\t/**\n\t * Try to execute an extension command. Returns true if command was found and executed.\n\t */\n\tprivate async _tryExecuteExtensionCommand(text: string): Promise<boolean> {\n\t\t// Parse command name and args\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1);\n\n\t\tconst command = this._extensionRunner.getCommand(commandName);\n\t\tif (!command) return false;\n\n\t\t// Get command context from extension runner (includes session control methods)\n\t\tconst ctx = this._extensionRunner.createCommandContext();\n\n\t\ttry {\n\t\t\tawait command.handler(args, ctx);\n\t\t\treturn true;\n\t\t} catch (err) {\n\t\t\t// Emit error via extension runner\n\t\t\tthis._extensionRunner.emitError({\n\t\t\t\textensionPath: `command:${commandName}`,\n\t\t\t\tevent: \"command\",\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t/**\n\t * Expand skill commands (/skill:name args) to their full content.\n\t * Returns the expanded text, or the original text if not a skill command or skill not found.\n\t * Emits errors via extension runner if file read fails.\n\t */\n\tprivate _expandSkillCommand(text: string): string {\n\t\tif (!text.startsWith(\"/skill:\")) return text;\n\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);\n\t\tconst args = spaceIndex === -1 ? \"\" : text.slice(spaceIndex + 1).trim();\n\n\t\tconst skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName);\n\t\tif (!skill) return text; // Unknown skill, pass through\n\n\t\ttry {\n\t\t\tconst content = readFileSync(skill.filePath, \"utf-8\");\n\t\t\tconst body = stripFrontmatter(content).trim();\n\t\t\tconst skillBlock = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n\t\t\treturn args ? `${skillBlock}\\n\\n${args}` : skillBlock;\n\t\t} catch (err) {\n\t\t\t// Emit error like extension commands do\n\t\t\tthis._extensionRunner.emitError({\n\t\t\t\textensionPath: skill.filePath,\n\t\t\t\tevent: \"skill_expansion\",\n\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t});\n\t\t\treturn text; // Return original on error\n\t\t}\n\t}\n\n\t/**\n\t * Queue a steering message while the agent is running.\n\t * Delivered after the current assistant turn finishes executing its tool calls,\n\t * before the next LLM call.\n\t * Expands skill commands and prompt templates. Errors on extension commands.\n\t * @param images Optional image attachments to include with the message\n\t * @throws Error if text is an extension command\n\t */\n\tasync steer(text: string, images?: ImageContent[]): Promise<void> {\n\t\tconst sanitizedText = redactSensitiveText(text);\n\n\t\t// Check for extension commands (cannot be queued)\n\t\tif (sanitizedText.startsWith(\"/\")) {\n\t\t\tthis._throwIfExtensionCommand(sanitizedText);\n\t\t}\n\n\t\t// Expand skill commands and prompt templates\n\t\tlet expandedText = this._expandSkillCommand(sanitizedText);\n\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\n\t\tawait this._queueSteer(redactSensitiveText(expandedText), images);\n\t}\n\n\t/**\n\t * Queue a follow-up message to be processed after the agent finishes.\n\t * Delivered only when agent has no more tool calls or steering messages.\n\t * Expands skill commands and prompt templates. Errors on extension commands.\n\t * @param images Optional image attachments to include with the message\n\t * @throws Error if text is an extension command\n\t */\n\tasync followUp(text: string, images?: ImageContent[]): Promise<void> {\n\t\tconst sanitizedText = redactSensitiveText(text);\n\n\t\t// Check for extension commands (cannot be queued)\n\t\tif (sanitizedText.startsWith(\"/\")) {\n\t\t\tthis._throwIfExtensionCommand(sanitizedText);\n\t\t}\n\n\t\t// Expand skill commands and prompt templates\n\t\tlet expandedText = this._expandSkillCommand(sanitizedText);\n\t\texpandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]);\n\n\t\tawait this._queueFollowUp(redactSensitiveText(expandedText), images);\n\t}\n\n\t/**\n\t * Internal: Queue a steering message (already expanded, no extension command check).\n\t */\n\tprivate async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {\n\t\tthis._invalidateContextBudgetCache({ type: \"userSteering\" });\n\t\tthis._steeringMessages.push(text);\n\t\tthis._emitQueueUpdate();\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (images) {\n\t\t\tcontent.push(...images);\n\t\t}\n\t\tthis.agent.steer({\n\t\t\trole: \"user\",\n\t\t\tcontent,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t/**\n\t * Internal: Queue a follow-up message (already expanded, no extension command check).\n\t */\n\tprivate async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {\n\t\tthis._followUpMessages.push(text);\n\t\tthis._emitQueueUpdate();\n\t\tconst content: (TextContent | ImageContent)[] = [{ type: \"text\", text }];\n\t\tif (images) {\n\t\t\tcontent.push(...images);\n\t\t}\n\t\tthis.agent.followUp({\n\t\t\trole: \"user\",\n\t\t\tcontent,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t/**\n\t * Throw an error if the text is an extension command.\n\t */\n\tprivate _throwIfExtensionCommand(text: string): void {\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\tconst command = this._extensionRunner.getCommand(commandName);\n\n\t\tif (command) {\n\t\t\tthrow new Error(\n\t\t\t\t`Extension command \"/${commandName}\" cannot be queued. Use prompt() or execute the command when not streaming.`,\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Send a custom message to the session. Creates a CustomMessageEntry.\n\t *\n\t * Handles three cases:\n\t * - Streaming: queues message, processed when loop pulls from queue\n\t * - Not streaming + triggerTurn: appends to state/session, starts new turn\n\t * - Not streaming + no trigger: appends to state/session, no turn\n\t *\n\t * @param message Custom message with customType, content, display, details\n\t * @param options.triggerTurn If true and not streaming, triggers a new LLM turn\n\t * @param options.deliverAs Delivery mode: \"steer\", \"followUp\", or \"nextTurn\"\n\t */\n\tasync sendCustomMessage<T = unknown>(\n\t\tmessage: Pick<CustomMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: { triggerTurn?: boolean; deliverAs?: \"steer\" | \"followUp\" | \"nextTurn\" },\n\t): Promise<void> {\n\t\tconst appMessage = {\n\t\t\trole: \"custom\" as const,\n\t\t\tcustomType: message.customType,\n\t\t\tcontent: message.content,\n\t\t\tdisplay: message.display,\n\t\t\tdetails: message.details,\n\t\t\ttimestamp: Date.now(),\n\t\t} satisfies CustomMessage<T>;\n\t\tthis._recordEvidenceReceiptInvalidation(message.customType);\n\t\tif (options?.deliverAs === \"nextTurn\") {\n\t\t\tthis._pendingNextTurnMessages.push(appMessage);\n\t\t} else if (this.isStreaming) {\n\t\t\tif (options?.deliverAs === \"followUp\") {\n\t\t\t\tthis.agent.followUp(appMessage);\n\t\t\t} else {\n\t\t\t\tthis.agent.steer(appMessage);\n\t\t\t}\n\t\t} else if (options?.triggerTurn) {\n\t\t\tawait this._runAgentPrompt(appMessage);\n\t\t} else {\n\t\t\tthis.agent.state.messages.push(appMessage);\n\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\tmessage.customType,\n\t\t\t\tmessage.content,\n\t\t\t\tmessage.display,\n\t\t\t\tmessage.details,\n\t\t\t);\n\t\t\tthis._emit({ type: \"message_start\", message: appMessage });\n\t\t\tthis._emit({ type: \"message_end\", message: appMessage });\n\t\t}\n\t}\n\n\t/**\n\t * Send a user message to the agent. Always triggers a turn.\n\t * When the agent is streaming, use deliverAs to specify how to queue the message.\n\t *\n\t * @param content User message content (string or content array)\n\t * @param options.deliverAs Delivery mode when streaming: \"steer\" or \"followUp\"\n\t */\n\tasync sendUserMessage(\n\t\tcontent: string | (TextContent | ImageContent)[],\n\t\toptions?: { deliverAs?: \"steer\" | \"followUp\" },\n\t): Promise<void> {\n\t\t// Normalize content to text string + optional images\n\t\tlet text: string;\n\t\tlet images: ImageContent[] | undefined;\n\n\t\tif (typeof content === \"string\") {\n\t\t\ttext = content;\n\t\t} else {\n\t\t\tconst textParts: string[] = [];\n\t\t\timages = [];\n\t\t\tfor (const part of content) {\n\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t} else {\n\t\t\t\t\timages.push(part);\n\t\t\t\t}\n\t\t\t}\n\t\t\ttext = textParts.join(\"\\n\");\n\t\t\tif (images.length === 0) images = undefined;\n\t\t}\n\n\t\t// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion\n\t\tawait this.prompt(text, {\n\t\t\texpandPromptTemplates: false,\n\t\t\tstreamingBehavior: options?.deliverAs,\n\t\t\timages,\n\t\t\tsource: \"extension\",\n\t\t});\n\t}\n\n\t/**\n\t * Clear all queued messages and return them.\n\t * Useful for restoring to editor when user aborts.\n\t * @returns Object with steering and followUp arrays\n\t */\n\tclearQueue(): { steering: string[]; followUp: string[] } {\n\t\tconst steering = [...this._steeringMessages];\n\t\tconst followUp = [...this._followUpMessages];\n\t\tthis._steeringMessages = [];\n\t\tthis._followUpMessages = [];\n\t\tthis.agent.clearAllQueues();\n\t\tthis._emitQueueUpdate();\n\t\treturn { steering, followUp };\n\t}\n\n\t/** Number of pending messages (includes both steering and follow-up) */\n\tget pendingMessageCount(): number {\n\t\treturn this._steeringMessages.length + this._followUpMessages.length;\n\t}\n\n\t/** Get pending steering messages (read-only) */\n\tgetSteeringMessages(): readonly string[] {\n\t\treturn this._steeringMessages;\n\t}\n\n\t/** Get pending follow-up messages (read-only) */\n\tgetFollowUpMessages(): readonly string[] {\n\t\treturn this._followUpMessages;\n\t}\n\n\tget resourceLoader(): ResourceLoader {\n\t\treturn this._resourceLoader;\n\t}\n\n\t/**\n\t * Abort current operation and wait for agent to become idle.\n\t */\n\tasync abort(): Promise<void> {\n\t\tthis._userAbortRequested = this.isStreaming || this.isRetrying;\n\t\tthis.abortRetry();\n\t\tthis.agent.abort();\n\t\tawait this.agent.waitForIdle();\n\t}\n\n\t/** Record an observed process signal before a mode begins shutdown. */\n\trecordProcessSignal(signal: SessionProcessSignal): SessionTermination {\n\t\tconst timestamp = new Date().toISOString();\n\t\tconst runId = this._activeRunId ?? `signal-${randomUUID()}`;\n\t\tconst termination = classifySessionTermination({\n\t\t\tsessionId: this.sessionId,\n\t\t\trunId,\n\t\t\ttimestamp,\n\t\t\tsource: \"observed\",\n\t\t\tmessage: `Process received ${signal}.`,\n\t\t\tcause: { area: \"process\", code: \"signal\", signal },\n\t\t\tsideEffects: this._activeRunId === null ? \"none\" : \"possible\",\n\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t});\n\t\tif (this._activeRunId !== null) {\n\t\t\tthis._runJournalStore.finish({\n\t\t\t\ttermination,\n\t\t\t\tsessionRevision: this._sessionRevision(),\n\t\t\t\ttimestamp,\n\t\t\t});\n\t\t\tthis._activeRunId = null;\n\t\t}\n\t\tthis._publishTermination(termination);\n\t\treturn termination;\n\t}\n\n\t// =========================================================================\n\t// Model Management\n\t// =========================================================================\n\n\tprivate async _emitModelSelect(\n\t\tnextModel: Model<any>,\n\t\tpreviousModel: Model<any> | undefined,\n\t\tsource: \"set\" | \"cycle\" | \"restore\",\n\t): Promise<void> {\n\t\tif (modelsAreEqual(previousModel, nextModel)) return;\n\t\tthis._invalidateContextBudgetCache({ type: \"activeModelId\", value: this._contextCacheModelId(nextModel) });\n\t\tawait this._extensionRunner.emit({\n\t\t\ttype: \"model_select\",\n\t\t\tmodel: nextModel,\n\t\t\tpreviousModel,\n\t\t\tsource,\n\t\t});\n\t}\n\n\t/**\n\t * Set model directly.\n\t * Validates that auth is configured, saves to session and settings.\n\t * @throws Error if no auth is configured for the model\n\t */\n\tasync setModel(model: Model<any>): Promise<void> {\n\t\tif (!this._modelRegistry.hasConfiguredAuth(model)) {\n\t\t\tthrow new Error(`No API key for ${model.provider}/${model.id}`);\n\t\t}\n\n\t\t// Root-level: block sticky safety models (Fable) unless the user pinned `--model`.\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\tshouldEjectStickySafetyModel({\n\t\t\t\tblockStickySafetyModels: resilience.blockStickySafetyModels,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t}) &&\n\t\t\tisStickySafetyModel(model.id, model.provider)\n\t\t) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(model.id, model.provider));\n\t\t}\n\n\t\t// Grok OAuth: block selecting Imagine models as the session chat model.\n\t\tassertTextChatModelForCompletion(model.id, model.provider);\n\n\t\tconst previousModel = this.model;\n\t\tconst thinkingLevel = this._getThinkingLevelForModelSwitch();\n\t\tconst nextSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames(), model);\n\t\tthis.agent.state.model = model;\n\t\tthis._baseSystemPrompt = nextSystemPrompt;\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis.sessionManager.appendModelChange(model.provider, model.id);\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\n\t\t// Re-clamp thinking level for new model's capabilities\n\t\tthis.setThinkingLevel(thinkingLevel);\n\n\t\tawait this._emitModelSelect(model, previousModel, \"set\");\n\t}\n\n\t/**\n\t * Cycle to next/previous model.\n\t * Uses scoped models (from --models flag) if available, otherwise all available models.\n\t * @param direction - \"forward\" (default) or \"backward\"\n\t * @returns The new model info, or undefined if only one model available\n\t */\n\tasync cycleModel(direction: \"forward\" | \"backward\" = \"forward\"): Promise<ModelCycleResult | undefined> {\n\t\tif (this._scopedModels.length > 0) {\n\t\t\treturn this._cycleScopedModel(direction);\n\t\t}\n\t\treturn this._cycleAvailableModel(direction);\n\t}\n\n\tprivate async _cycleScopedModel(direction: \"forward\" | \"backward\"): Promise<ModelCycleResult | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tconst scopedModels = this._scopedModels.filter((scoped) => {\n\t\t\tif (!this._modelRegistry.hasConfiguredAuth(scoped.model)) return false;\n\t\t\tif (resilience.blockStickySafetyModels && isStickySafetyModel(scoped.model.id, scoped.model.provider)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn true;\n\t\t});\n\t\tif (scopedModels.length <= 1) return undefined;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel));\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst len = scopedModels.length;\n\t\tconst nextIndex = direction === \"forward\" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;\n\t\tconst next = scopedModels[nextIndex];\n\t\tconst thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);\n\n\t\t// Single path through setModel (sticky block + auth + Imagine guards).\n\t\tawait this.setModel(next.model);\n\t\t// Explicit scoped thinking level still applies after switch.\n\t\tthis.setThinkingLevel(thinkingLevel);\n\n\t\treturn { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };\n\t}\n\n\tprivate async _cycleAvailableModel(direction: \"forward\" | \"backward\"): Promise<ModelCycleResult | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tconst raw = await this._modelRegistry.getAvailable();\n\t\t// Hard-kill sticky safety models from the cycle list (Fable must not re-enter via hotkey).\n\t\tconst availableModels = resilience.blockStickySafetyModels\n\t\t\t? raw.filter((m) => !isStickySafetyModel(m.id, m.provider))\n\t\t\t: raw;\n\t\tif (availableModels.length <= 1) return undefined;\n\n\t\tconst currentModel = this.model;\n\t\tlet currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel));\n\n\t\tif (currentIndex === -1) currentIndex = 0;\n\t\tconst len = availableModels.length;\n\t\tconst nextIndex = direction === \"forward\" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;\n\t\tconst nextModel = availableModels[nextIndex];\n\n\t\t// Always go through setModel so sticky blocks / auth / Imagine guards stay single-path.\n\t\tawait this.setModel(nextModel);\n\n\t\treturn { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };\n\t}\n\n\t// =========================================================================\n\t// Thinking Level Management\n\t// =========================================================================\n\n\t/**\n\t * Set thinking level.\n\t * Clamps to model capabilities based on available thinking levels.\n\t * Saves to session and settings only if the level actually changes.\n\t */\n\tsetThinkingLevel(level: ThinkingLevel): void {\n\t\tconst availableLevels = this.getAvailableThinkingLevels();\n\t\tconst effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);\n\n\t\t// Only persist if actually changing\n\t\tconst previousLevel = this.agent.state.thinkingLevel;\n\t\tconst isChanging = effectiveLevel !== previousLevel;\n\n\t\tthis.agent.state.thinkingLevel = effectiveLevel;\n\n\t\tif (isChanging) {\n\t\t\tthis.sessionManager.appendThinkingLevelChange(effectiveLevel);\n\t\t\tif (this.supportsThinking() || effectiveLevel !== \"off\") {\n\t\t\t\tthis.settingsManager.setDefaultThinkingLevel(effectiveLevel);\n\t\t\t}\n\t\t\tthis._emit({ type: \"thinking_level_changed\", level: effectiveLevel });\n\t\t\tvoid this._extensionRunner.emit({\n\t\t\t\ttype: \"thinking_level_select\",\n\t\t\t\tlevel: effectiveLevel,\n\t\t\t\tpreviousLevel,\n\t\t\t});\n\t\t}\n\t}\n\n\t/** Apply an explicit user choice and retain it as bounded router feedback. */\n\tsetUserThinkingLevel(level: ThinkingLevel): void {\n\t\tconst overridesAuto = this._thinkingMode === \"auto\";\n\t\tthis.setThinkingMode(\"manual\");\n\t\tthis.setThinkingLevel(level);\n\t\tif (overridesAuto) {\n\t\t\tthis._recordUserThinkingOverride(this.thinkingLevel);\n\t\t} else {\n\t\t\tthis._lastAutoRouterDecision = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Set the thinking mode. \"auto\" resolves a level per turn via the reasoning\n\t * router; \"manual\" keeps the explicitly selected level. The mode persists for\n\t * the session lifetime and is never written to user settings.\n\t */\n\tsetThinkingMode(mode: ThinkingMode): void {\n\t\tthis._thinkingMode = mode;\n\t}\n\n\t/**\n\t * In auto thinking mode, resolve and apply this turn's thinking level from the\n\t * prompt content. Updates agent state, records the change in the session, and\n\t * notifies observers - but never overwrites the user's persisted default\n\t * thinking level in settings. Models without reasoning support bypass the\n\t * router entirely (level stays \"off\").\n\t */\n\tprivate _applyAutoThinkingLevelForTurn(promptText: string): void {\n\t\tif (this._thinkingMode !== \"auto\") return;\n\t\tif (!this.supportsThinking()) return;\n\n\t\tthis._applyAutoThinkingLevelV4(promptText);\n\t}\n\n\t/**\n\t * Auto-mode resolver. Reuses the N=8 recent-class history and\n\t * context-pressure bucket, routed through the confidence-bearing v4 classifier\n\t * and its uncertainty-aware resolver. No\n\t * `laneType` applies to the main session (always \"none\"/`undefined`);\n\t * the default-off Adaptorch bridge is wired but its current transport returns\n\t * no advisory hint. `bias` stays `0` unless BOTH hold: (a) the global,\n\t * owner-only `reasoningRouterLearning.enabled` setting is `true` (default\n\t * off; a project-scope `.omk/settings.json` value for this key is never\n\t * consulted -- see settings-manager.ts), and (b) a compiled\n\t * `RouterBiasSnapshot` was found and passed strict validation at the\n\t * configured path or opaque repository/worktree-scoped default, loaded and\n\t * cached (\"pinned\") at most once per session (see\n\t * `_getReasoningRouterBiasSnapshot`). When learning is enabled, one bounded\n\t * \"accepted\" record (no raw prompt/path/diff/session/model/provider/tool/hook\n\t * content; see router-feedback-collector.ts's exact ten-key schema) is\n\t * appended after every v4 auto-turn. A later explicit user level records one\n\t * bounded `s1-override` direction for the same cell. Both remain inert until\n\t * a future, separate offline compile step. The\n\t * resolver's own confidence-band/fallback-reason escalation (see\n\t * reasoning-router-v4.ts) still applies on top of the base+lane+bias\n\t * target, so a low-confidence or fallback-decided verdict can still only\n\t * match or exceed the confident-path level.\n\t */\n\tprivate _applyAutoThinkingLevelV4(promptText: string): void {\n\t\tconst availableLevels = this.getAvailableThinkingLevels();\n\t\tconst features = deriveRouterFeedbackFeaturesV4(promptText);\n\t\tconst verdict = classifyTaskV4(\n\t\t\t{\n\t\t\t\tprompt: promptText,\n\t\t\t\thistory: this._taskClassHistory,\n\t\t\t\tpressureBucket: this._computePressureBucket(),\n\t\t\t},\n\t\t\tundefined,\n\t\t\tfeatures,\n\t\t);\n\n\t\tthis._taskClassHistory.unshift(verdict.taskClass);\n\t\tif (this._taskClassHistory.length > 8) this._taskClassHistory.length = 8;\n\n\t\tconst learningEnabled = this.settingsManager.getReasoningRouterLearningEnabled();\n\t\tconst snapshot = learningEnabled ? this._getReasoningRouterBiasSnapshot() : null;\n\t\tconst bias =\n\t\t\tsnapshot === null\n\t\t\t\t? 0\n\t\t\t\t: getBiasStepsForCell(snapshot, {\n\t\t\t\t\t\tpredictedClass: verdict.taskClass,\n\t\t\t\t\t\tlaneType: \"none\",\n\t\t\t\t\t\tlenBucket: features.lenBucket,\n\t\t\t\t\t\thadFence: features.hadFence,\n\t\t\t\t\t\thadDiff: features.hadDiff,\n\t\t\t\t\t});\n\n\t\tconst resolved = resolveThinkingLevelV4WithUncertainty(\n\t\t\tverdict,\n\t\t\tavailableLevels,\n\t\t\tundefined,\n\t\t\tbias,\n\t\t\tthis._getAdaptorchHint(verdict, features),\n\t\t);\n\n\t\tthis._lastAutoRouterDecision = undefined;\n\t\tif (learningEnabled && resolved !== \"off\") {\n\t\t\tconst decision: RouterAutoDecision = {\n\t\t\t\trouterVersion: \"v4\",\n\t\t\t\tlaneType: \"none\",\n\t\t\t\tpredictedClass: verdict.taskClass,\n\t\t\t\tresolvedLevel: resolved,\n\t\t\t\tlenBucket: features.lenBucket,\n\t\t\t\thadFence: features.hadFence,\n\t\t\t\thadDiff: features.hadDiff,\n\t\t\t};\n\t\t\tthis._lastAutoRouterDecision = decision;\n\t\t\tthis._appendRouterFeedback({\n\t\t\t\t...decision,\n\t\t\t\tacceptedLevel: resolved,\n\t\t\t\tsignal: \"s2-accept\",\n\t\t\t\toutcome: \"accepted\",\n\t\t\t});\n\t\t}\n\n\t\tconst previousLevel = this.agent.state.thinkingLevel;\n\t\tif (resolved === previousLevel) return;\n\n\t\tthis.agent.state.thinkingLevel = resolved;\n\t\tthis.sessionManager.appendThinkingLevelChange(resolved);\n\t\tthis._emit({ type: \"thinking_level_changed\", level: resolved });\n\t}\n\n\tprivate _getRepositoryRouterLearningPaths(): RepositoryRouterLearningPaths {\n\t\tif (this._repositoryRouterLearningPaths === undefined) {\n\t\t\tthis._repositoryRouterLearningPaths = getRepositoryRouterLearningPaths(this._cwd);\n\t\t}\n\t\treturn this._repositoryRouterLearningPaths;\n\t}\n\n\tprivate _appendRouterFeedback(record: RouterFeedbackRecord): void {\n\t\tappendRouterFeedbackRecord(record, {\n\t\t\tenabled: this.settingsManager.getReasoningRouterLearningEnabled(),\n\t\t\tledgerPath:\n\t\t\t\tthis.settingsManager.getReasoningRouterLearningFeedbackLedgerPath() ??\n\t\t\t\tthis._getRepositoryRouterLearningPaths().ledgerPath,\n\t\t});\n\t}\n\n\tprivate _recordUserThinkingOverride(acceptedLevel: ThinkingLevel): void {\n\t\tconst decision = this._lastAutoRouterDecision;\n\t\tthis._lastAutoRouterDecision = undefined;\n\t\tif (!this.settingsManager.getReasoningRouterLearningEnabled() || !decision || acceptedLevel === \"off\") return;\n\n\t\tconst resolvedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(decision.resolvedLevel);\n\t\tconst acceptedIndex = ROUTER_FEEDBACK_LEVELS.indexOf(acceptedLevel);\n\t\tconst outcome = acceptedIndex > resolvedIndex ? \"up\" : acceptedIndex < resolvedIndex ? \"down\" : \"same\";\n\t\tthis._appendRouterFeedback({\n\t\t\t...decision,\n\t\t\tacceptedLevel,\n\t\t\tsignal: \"s1-override\",\n\t\t\toutcome,\n\t\t});\n\t}\n\n\t/**\n\t * Strictly load the compiled bias snapshot once per session. The first call\n\t * pins either a valid snapshot or a null miss; later calls never touch disk.\n\t */\n\tprivate _getReasoningRouterBiasSnapshot(): RouterBiasSnapshot | null {\n\t\tif (this._reasoningRouterBiasSnapshotLoaded) return this._reasoningRouterBiasSnapshot;\n\t\tthis._reasoningRouterBiasSnapshotLoaded = true;\n\n\t\tconst path =\n\t\t\tthis.settingsManager.getReasoningRouterLearningBiasSnapshotPath() ??\n\t\t\tthis._getRepositoryRouterLearningPaths().biasSnapshotPath;\n\t\ttry {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tthis._reasoningRouterBiasSnapshot = parseRouterBiasSnapshot(readFileSync(path, \"utf-8\"));\n\t\t\t}\n\t\t} catch {\n\t\t\tthis._reasoningRouterBiasSnapshot = null;\n\t\t}\n\t\treturn this._reasoningRouterBiasSnapshot;\n\t}\n\n\t/**\n\t * Context-pressure band 0..3 from the projected token estimate over the\n\t * model context window (reuses estimateProjectedContextTokens). 0..<0.5,\n\t * 1..<0.75, 2..<0.9, 3..>=0.9. Inert under DEFAULT_WEIGHTS (no pressure\n\t * coefficient); computed for future calibrated weight presets.\n\t */\n\tprivate _computePressureBucket(): number {\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return 0;\n\t\tconst estimate = estimateProjectedContextTokens(this.agent.state.messages, []);\n\t\tconst pressure = estimate.tokens / contextWindow;\n\t\tif (pressure >= 0.9) return 3;\n\t\tif (pressure >= 0.75) return 2;\n\t\tif (pressure >= 0.5) return 1;\n\t\treturn 0;\n\t}\n\n\t/**\n\t * AdaptOrch advisory bridge hint accessor (default-off, global-only).\n\t * Lazily constructs the bridge on first call when enabled; returns `null`\n\t * (no hint) otherwise. The bridge's `getFreshHint` is synchronous and\n\t * cache-read-only, so this never blocks the turn-start path. A fire-and-\n\t * forget `requestRefresh` is issued for the NEXT turn's benefit.\n\t *\n\t * The returned hint is shaped as `{ level, confidence }` for the resolver's\n\t * hint-fusion slot: confidence is mapped from the bridge's closed\n\t * `confidenceBand` enum (low=0.5, medium=0.75, high=0.95) and the level is\n\t * derived from the bridge's `taskClass` via the same static rule table the\n\t * resolver uses. The resolver's own HINT_CONFIDENCE_THRESHOLD (0.7) gates\n\t * whether the hint actually fuses, so a \"low\" band hint is structurally\n\t * inert.\n\t */\n\tprivate _getAdaptorchHint(\n\t\tverdict: { taskClass: TaskClassV4 },\n\t\tfeatures: RouterFeedbackFeaturesV4,\n\t): { level: import(\"omk-agent-core\").ThinkingLevel; confidence: number } | null {\n\t\tif (!this.settingsManager.getAdaptorchBridgeEnabled()) return null;\n\n\t\t// Lazy init (at most once per session)\n\t\tif (!this._adaptorchBridgeInitAttempted) {\n\t\t\tthis._adaptorchBridgeInitAttempted = true;\n\t\t\tconst bridgeSettings = this.settingsManager.getAdaptorchBridgeSettings();\n\t\t\tthis._adaptorchBridge = createAdaptorchBridge({\n\t\t\t\t// The advisory function is a no-op stub until a real MCP transport\n\t\t\t\t// is wired by a future lane. The bridge's circuit breaker and budget\n\t\t\t\t// counter ensure this stub can never cause harm.\n\t\t\t\tadvisoryFn: async () => null,\n\t\t\t\tttlMs: bridgeSettings?.ttlMs,\n\t\t\t\ttimeoutMs: bridgeSettings?.timeoutMs,\n\t\t\t\tmaxConsultsPerSession: bridgeSettings?.maxConsultsPerSession,\n\t\t\t\tfailureThreshold: bridgeSettings?.failureThreshold,\n\t\t\t});\n\t\t}\n\n\t\tconst bridge = this._adaptorchBridge;\n\t\tif (bridge === null) return null;\n\n\t\tconst payload: AdaptorchConsultPayload = {\n\t\t\tschemaVersion: 1,\n\t\t\ttaskClass: verdict.taskClass as AdaptorchConsultPayload[\"taskClass\"],\n\t\t\trunnerUp: verdict.taskClass as AdaptorchConsultPayload[\"runnerUp\"],\n\t\t\tmarginBucket: \"high\",\n\t\t\tlenBucket: features.lenBucket as AdaptorchConsultPayload[\"lenBucket\"],\n\t\t\thadFence: features.hadFence,\n\t\t\thadDiff: features.hadDiff,\n\t\t\tpressureBucket: this._computePressureBucket() as AdaptorchConsultPayload[\"pressureBucket\"],\n\t\t};\n\n\t\t// Fire-and-forget refresh for next turn\n\t\tbridge.requestRefresh(payload);\n\n\t\t// Synchronous cache read for this turn\n\t\tconst hint = bridge.getFreshHint(payload);\n\t\tif (hint === null) return null;\n\n\t\t// Map confidenceBand -> numeric confidence for the resolver's threshold gate\n\t\tconst confidenceMap: Record<string, number> = { low: 0.5, medium: 0.75, high: 0.95 };\n\t\tconst confidence = confidenceMap[hint.confidenceBand] ?? 0.5;\n\n\t\treturn { level: TASK_CLASS_THINKING_LEVELS_V4[hint.taskClass], confidence };\n\t}\n\n\t/**\n\t * Cycle to next thinking level.\n\t * @returns New level, or undefined if model doesn't support thinking\n\t */\n\tcycleThinkingLevel(): ThinkingLevel | undefined {\n\t\tif (!this.supportsThinking()) return undefined;\n\n\t\tconst levels = this.getAvailableThinkingLevels();\n\t\tconst currentIndex = levels.indexOf(this.thinkingLevel);\n\t\tconst nextIndex = (currentIndex + 1) % levels.length;\n\t\tconst nextLevel = levels[nextIndex];\n\t\tconst overridesAuto = this._thinkingMode === \"auto\";\n\n\t\tthis.setThinkingLevel(nextLevel);\n\t\tif (overridesAuto) {\n\t\t\tthis.setThinkingMode(\"manual\");\n\t\t\tthis._recordUserThinkingOverride(nextLevel);\n\t\t}\n\t\treturn nextLevel;\n\t}\n\n\t/**\n\t * Get available thinking levels for current model.\n\t * The provider will clamp to what the specific model supports internally.\n\t */\n\tgetAvailableThinkingLevels(): ThinkingLevel[] {\n\t\tif (!this.model) return THINKING_LEVELS;\n\t\treturn getSupportedThinkingLevels(this.model) as ThinkingLevel[];\n\t}\n\n\t/**\n\t * Check if current model supports thinking/reasoning.\n\t */\n\tsupportsThinking(): boolean {\n\t\treturn Boolean(this.model?.reasoning);\n\t}\n\n\tprivate _getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel {\n\t\tif (explicitLevel !== undefined) {\n\t\t\treturn explicitLevel;\n\t\t}\n\t\tif (!this.supportsThinking()) {\n\t\t\treturn this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t\t}\n\t\treturn this.thinkingLevel;\n\t}\n\n\tprivate _clampThinkingLevel(level: ThinkingLevel, _availableLevels: ThinkingLevel[]): ThinkingLevel {\n\t\treturn this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : \"off\";\n\t}\n\n\t// =========================================================================\n\t// Queue Mode Management\n\t// =========================================================================\n\n\t/**\n\t * Set steering message mode.\n\t * Saves to settings.\n\t */\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.agent.steeringMode = mode;\n\t\tthis.settingsManager.setSteeringMode(mode);\n\t}\n\n\t/**\n\t * Set follow-up message mode.\n\t * Saves to settings.\n\t */\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.agent.followUpMode = mode;\n\t\tthis.settingsManager.setFollowUpMode(mode);\n\t}\n\n\t// =========================================================================\n\t// Compaction\n\t// =========================================================================\n\n\tprivate _beginCompactionTransaction(compactionModel: Model<Api>, emergency: boolean): BegunCompaction {\n\t\treturn this._compactionService.beginTransaction(compactionModel, emergency);\n\t}\n\n\tprivate _commitCompaction(\n\t\tbegun: BegunCompaction,\n\t\tresult: CompactionResult,\n\t\tfromExtension: boolean,\n\t): CommittedCompaction {\n\t\treturn this._compactionService.commit(begun, result, fromExtension);\n\t}\n\n\tprivate _pendingToolResultReserve(settings: CompactionSettings): number {\n\t\tconst pendingIds = this.agent.state.pendingToolCalls;\n\t\tconst calls = new Map<string, { readonly name: string; readonly args: unknown }>();\n\t\tconst streaming = this.agent.state.streamingMessage;\n\t\tconst messages = streaming ? [...this.agent.state.messages, streaming] : this.agent.state.messages;\n\t\tfor (const message of messages) {\n\t\t\tif (message.role !== \"assistant\" || !Array.isArray(message.content)) continue;\n\t\t\tfor (const part of message.content) {\n\t\t\t\tif (part.type === \"toolCall\" && pendingIds.has(part.id)) {\n\t\t\t\t\tcalls.set(part.id, { name: part.name, args: part.arguments });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst counts: Record<ToolResultClass, number> = { text: 0, image: 0, \"large-output\": 0 };\n\t\tfor (const id of pendingIds) {\n\t\t\tconst call = calls.get(id);\n\t\t\tconst resultClass = call ? pendingToolResultClass(call.name, call.args) : \"large-output\";\n\t\t\tcounts[resultClass] += 1;\n\t\t}\n\t\tconst requests: ToolResultReserveRequest[] = [];\n\t\tfor (const resultClass of [\"text\", \"image\", \"large-output\"] as const) {\n\t\t\tif (counts[resultClass] > 0) {\n\t\t\t\trequests.push({\n\t\t\t\t\tclass: resultClass,\n\t\t\t\t\tcount: counts[resultClass],\n\t\t\t\t\ttokensPerResult: PENDING_TOOL_RESULT_TOKENS[resultClass],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tconst configured = settings.reservedToolResultTokens ?? 0;\n\t\trequests.push({ class: \"large-output\", count: 1, tokensPerResult: configured });\n\t\treturn estimateToolResultReserve(requests);\n\t}\n\n\tprivate _compactionHysteresisConfig(contextWindow: number, settings: CompactionSettings) {\n\t\tconst threshold = getCompactionHeadroomThreshold(contextWindow, {\n\t\t\t...settings,\n\t\t\treservedToolResultTokens: this._pendingToolResultReserve(settings),\n\t\t});\n\t\tif (!threshold) return undefined;\n\t\tconst triggerRatio = Math.min(\n\t\t\t1,\n\t\t\tMath.max(1 / Math.floor(contextWindow), threshold.triggerTokens / contextWindow),\n\t\t);\n\t\tconst configuredRearm = settings.rearmRatio ?? triggerRatio * 0.75;\n\t\tconst rearmRatio = Math.min(configuredRearm, triggerRatio * 0.999);\n\t\tconst emergencyRatio = Math.max(triggerRatio, settings.emergencyRatio ?? 0.98);\n\t\treturn createCompactionHysteresisConfig({ rearmRatio, triggerRatio, emergencyRatio });\n\t}\n\n\tprivate _runtimeCompactionDecision(\n\t\tcontextTokens: number,\n\t\tcontextWindow: number,\n\t\tsettings: CompactionSettings,\n\t): { readonly compact: boolean; readonly emergency: boolean } {\n\t\tif (!Number.isFinite(contextTokens) || contextTokens < 0) return { compact: false, emergency: false };\n\t\tconst config = this._compactionHysteresisConfig(contextWindow, settings);\n\t\tif (!config) return { compact: false, emergency: false };\n\t\tconst result = stepCompactionHysteresis({\n\t\t\tconfig,\n\t\t\tstate: this._compactionHysteresisState,\n\t\t\tratio: Math.min(1, contextTokens / contextWindow),\n\t\t});\n\t\tthis._compactionHysteresisState = result.nextState;\n\t\treturn { compact: result.action === \"compact\", emergency: result.reason === \"emergency_threshold_reached\" };\n\t}\n\n\tprivate async _runThresholdCompaction(emergency: boolean): Promise<boolean> {\n\t\tthis._thresholdCompactionEmergency = emergency;\n\t\ttry {\n\t\t\treturn await this._runAutoCompaction(\"threshold\", false);\n\t\t} finally {\n\t\t\tthis._thresholdCompactionEmergency = false;\n\t\t}\n\t}\n\n\tprivate _recordCompactionCommitForHysteresis(): void {\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\t\tconst config = this._compactionHysteresisConfig(contextWindow, this.settingsManager.getCompactionSettings());\n\t\tif (!config) return;\n\t\tthis._compactionHysteresisState = stepCompactionHysteresis({\n\t\t\tconfig,\n\t\t\tstate: this._compactionHysteresisState,\n\t\t\tratio: 0,\n\t\t\toutcome: \"commit\",\n\t\t}).nextState;\n\t}\n\n\tprivate _resolveCompactionModel(sessionModel: Model<Api>): Model<Api> {\n\t\tconst configuredModel = this.settingsManager.getCompactionModel();\n\t\tconst availableModels = this._modelRegistry.getAvailable();\n\t\tlet model: Model<Api>;\n\t\tif (configuredModel) {\n\t\t\tconst configured = findExactModelReferenceMatch(configuredModel, availableModels);\n\t\t\tif (!configured) {\n\t\t\t\tthrow new Error(`Configured compaction model \"${configuredModel}\" is unavailable or unauthenticated.`);\n\t\t\t}\n\t\t\tmodel = configured;\n\t\t} else {\n\t\t\tmodel = resolveCompactionModel(sessionModel, availableModels);\n\t\t}\n\t\tthis._invalidateContextBudgetCache({\n\t\t\ttype: \"compactionModelId\",\n\t\t\tvalue: this._contextCacheModelId(model),\n\t\t});\n\t\treturn model;\n\t}\n\n\t/**\n\t * Authenticated failover candidates for compaction summarization, in\n\t * resilience-chain order, excluding the primary compaction model itself.\n\t * Used when the primary model's quota is exhausted (same-model retry is\n\t * useless until reset; another provider can still save the compaction).\n\t */\n\tprivate _compactionFailoverModels(primary: Model<Api>): Model<Api>[] {\n\t\tconst candidates = resolveFailoverCandidates(this.settingsManager.getProviderResilienceSettings());\n\t\tconst available = this._modelRegistry.getAvailable();\n\t\tconst models: Model<Api>[] = [];\n\t\tfor (const candidate of candidates) {\n\t\t\tif (candidate.provider === primary.provider && candidate.id === primary.id) continue;\n\t\t\tconst resolved = available.find((m) => m.provider === candidate.provider && m.id === candidate.id);\n\t\t\tif (resolved) models.push(resolved);\n\t\t}\n\t\treturn models;\n\t}\n\n\t/**\n\t * Manually compact the session context.\n\t * Aborts current agent operation first.\n\t * @param customInstructions Optional instructions for the compaction summary\n\t */\n\tasync compact(customInstructions?: string): Promise<CompactionResult> {\n\t\tawait this.abort();\n\t\tthis._disconnectFromAgent();\n\t\tthis._compactionAbortController = new AbortController();\n\t\tthis._emit({ type: \"compaction_start\", reason: \"manual\" });\n\t\tlet committedCompaction = false;\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthrow new Error(formatNoModelSelectedMessage());\n\t\t\t}\n\n\t\t\tconst compactionModel = this._resolveCompactionModel(this.model);\n\t\t\tconst { apiKey, headers } = await this._getCompactionRequestAuth(compactionModel);\n\n\t\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\t\tconst begun = this._beginCompactionTransaction(compactionModel, false);\n\t\t\tconst pathEntries = [...begun.capture.branchEntries];\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\t// Check why we can't compact\n\t\t\t\tconst lastEntry = pathEntries[pathEntries.length - 1];\n\t\t\t\tif (lastEntry?.type === \"compaction\") {\n\t\t\t\t\tthrow new Error(\"Already compacted\");\n\t\t\t\t}\n\t\t\t\tthrow new Error(\"Nothing to compact (session too small)\");\n\t\t\t}\n\n\t\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\t\tconst result = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\tsignal: this._compactionAbortController.signal,\n\t\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t\t}\n\n\t\t\t\tif (result?.compaction) {\n\t\t\t\t\textensionCompaction = result.compaction;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet summary: string;\n\t\t\tlet firstKeptEntryId: string;\n\t\t\tlet tokensBefore: number;\n\t\t\tlet details: unknown;\n\n\t\t\tif (extensionCompaction) {\n\t\t\t\t// Extension provided compaction content\n\t\t\t\tsummary = extensionCompaction.summary;\n\t\t\t\tfirstKeptEntryId = extensionCompaction.firstKeptEntryId;\n\t\t\t\ttokensBefore = extensionCompaction.tokensBefore;\n\t\t\t\tdetails = extensionCompaction.details;\n\t\t\t} else {\n\t\t\t\t// Generate compaction result\n\t\t\t\tconst result = await compact(\n\t\t\t\t\tpreparation,\n\t\t\t\t\tcompactionModel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\tthis._compactionAbortController.signal,\n\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\tthis.agent.streamFn,\n\t\t\t\t\tthis.settingsManager.getRetrySettings(),\n\t\t\t\t\tthis._summarizationRetryCallbacks({ source: \"compaction\", reason: \"manual\" }),\n\t\t\t\t\tthis._compactionFailoverModels(compactionModel),\n\t\t\t\t);\n\t\t\t\tsummary = result.summary;\n\t\t\t\tfirstKeptEntryId = result.firstKeptEntryId;\n\t\t\t\ttokensBefore = result.tokensBefore;\n\t\t\t\tdetails = result.details;\n\t\t\t}\n\n\t\t\tif (this._compactionAbortController.signal.aborted) {\n\t\t\t\tthrow new Error(\"Compaction cancelled\");\n\t\t\t}\n\n\t\t\tconst compactionResult: CompactionResult = {\n\t\t\t\tsummary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tconst committed = this._commitCompaction(begun, compactionResult, fromExtension);\n\t\t\tcommittedCompaction = true;\n\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\n\t\t\tif (this._extensionRunner) {\n\t\t\t\tawait this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_compact\",\n\t\t\t\t\tcompactionEntry: committed.entry,\n\t\t\t\t\tfromExtension,\n\t\t\t\t});\n\t\t\t}\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: compactionResult,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t});\n\t\t\treturn compactionResult;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tconst aborted = message === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\t\tconst stale = /stale|session changed during compaction|already compacted/i.test(message);\n\t\t\tlet compactionCode: \"aborted\" | \"stale\" | \"failed\" | \"quota_exhausted\" = \"failed\";\n\t\t\tif (aborted) compactionCode = \"aborted\";\n\t\t\telse if (stale) compactionCode = \"stale\";\n\t\t\telse if (isQuotaExhaustionMessage(message)) compactionCode = \"quota_exhausted\";\n\t\t\tconst timestamp = new Date().toISOString();\n\t\t\tthis._publishTermination(\n\t\t\t\tclassifySessionTermination({\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\trunId: `compaction-${randomUUID()}`,\n\t\t\t\t\ttimestamp,\n\t\t\t\t\tsource: \"observed\",\n\t\t\t\t\tmessage: this._terminationMessage(message, \"Manual compaction failed.\"),\n\t\t\t\t\tcause: { area: \"compaction\", code: compactionCode },\n\t\t\t\t\tsideEffects: committedCompaction ? \"confirmed\" : \"none\",\n\t\t\t\t\t...(this.model ? { provider: this.model.provider, model: this.model.id } : {}),\n\t\t\t\t}),\n\t\t\t);\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"manual\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage: aborted ? undefined : `Compaction failed: ${message}`,\n\t\t\t});\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tthis._compactionAbortController = undefined;\n\t\t\tthis._reconnectToAgent();\n\t\t}\n\t}\n\n\t/**\n\t * Cancel in-progress compaction (manual or auto).\n\t */\n\tabortCompaction(): void {\n\t\tthis._compactionAbortController?.abort();\n\t\tthis._autoCompactionAbortController?.abort();\n\t}\n\n\t/**\n\t * Cancel in-progress branch summarization.\n\t */\n\tabortBranchSummary(): void {\n\t\tthis._branchSummaryAbortController?.abort();\n\t}\n\n\t/** True when the transcript carries image content that forces a vision-route turn. */\n\tprivate _transcriptHasImages(messages: AgentMessage[]): boolean {\n\t\treturn messages.some((m) => {\n\t\t\tconst content = (m as { content?: unknown }).content;\n\t\t\tif (!Array.isArray(content)) return false;\n\t\t\treturn content.some((part: unknown) => {\n\t\t\t\treturn typeof part === \"object\" && part !== null && (part as { type?: string }).type === \"image\";\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Effective context window for the upcoming turn.\n\t * Image-bearing turns with a text-only session model are auto-routed to the\n\t * vision model (gpt-5.6-luna, 1M) — that window is the real limit. Text-only\n\t * turns keep the session model's (typically much larger) window.\n\t */\n\tprivate _effectiveTurnContextWindow(pendingMessages: AgentMessage[], sessionWindow: number): number {\n\t\t// Vision routing keys off the full transcript, not just the pending turn:\n\t\t// images retained in history keep every subsequent request on the vision model.\n\t\tconst hasImages =\n\t\t\tthis._transcriptHasImages(this.agent.state.messages) || this._transcriptHasImages(pendingMessages);\n\t\tconst model = this.model;\n\t\tif (!hasImages || !model || (model.input ?? []).includes(\"image\")) {\n\t\t\treturn sessionWindow;\n\t\t}\n\t\treturn Math.min(sessionWindow, getVisionRouteModel(model).contextWindow);\n\t}\n\n\tprivate async _checkProjectedCompaction(pendingMessages: AgentMessage[]): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\tif (!settings.enabled || pendingMessages.length === 0) return false;\n\n\t\tconst sessionWindow = this.model?.contextWindow ?? 0;\n\t\tif (sessionWindow <= 0) return false;\n\t\t// Image-bearing turns are served by the auto-routed vision model, not the\n\t\t// session model. Its window (not the session model's) is the real limit the\n\t\t// provider enforces, so threshold compaction must use it or the vision\n\t\t// request overflows (context_length_exceeded) before deepseek-based\n\t\t// compaction would fire.\n\t\tconst contextWindow = this._effectiveTurnContextWindow(pendingMessages, sessionWindow);\n\t\tif (contextWindow <= 0) return false;\n\n\t\tconst messages = [...this.agent.state.messages, ...pendingMessages];\n\t\tconst estimate = estimateProjectedContextTokens(this.agent.state.messages, pendingMessages);\n\t\tconst compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());\n\t\tif (estimate.lastUsageIndex !== null && compactionEntry) {\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\tconst decision = this._runtimeCompactionDecision(estimate.tokens, contextWindow, settings);\n\t\tif (decision.compact) {\n\t\t\treturn this._runThresholdCompaction(decision.emergency);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Check if compaction is needed and run it.\n\t * Called after agent_end and before prompt submission.\n\t *\n\t * Two cases:\n\t * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry\n\t * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually)\n\t *\n\t * @param assistantMessage The assistant message to check\n\t * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true\n\t */\n\tprivate async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getCompactionSettings();\n\t\t// Stale pre-compaction usage/errors must not retrigger compaction on the\n\t\t// first prompt after one finished.\n\t\tconst compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());\n\t\tif (\n\t\t\tshouldSkipCompactionCheck({\n\t\t\t\tenabled: settings.enabled,\n\t\t\t\tskipAbortedCheck,\n\t\t\t\tstopReason: assistantMessage.stopReason,\n\t\t\t\tmessageTimestamp: assistantMessage.timestamp,\n\t\t\t\tlatestCompactionTimestamp: compactionEntry ? new Date(compactionEntry.timestamp).getTime() : undefined,\n\t\t\t})\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst contextWindow = this.model?.contextWindow ?? 0;\n\n\t\t// Case 1: Overflow - LLM returned context overflow error. Only counts when\n\t\t// the failing message belongs to the session model — or to the auto-routed\n\t\t// vision model while the session model is text-only.\n\t\tif (\n\t\t\tisSessionModelOverflow({\n\t\t\t\tmessage: assistantMessage,\n\t\t\t\tcontextWindow,\n\t\t\t\tsessionProvider: this.model?.provider,\n\t\t\t\tsessionModelId: this.model?.id,\n\t\t\t\tsessionInputs: this.model?.input,\n\t\t\t})\n\t\t) {\n\t\t\tif (this._overflowRecoveryAttempts >= MAX_OVERFLOW_RECOVERY_ATTEMPTS) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"overflow\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Context overflow recovery failed after two staged compact-and-retry attempts. Reduce the latest input or switch to a model with a larger effective context window.\",\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tthis._overflowRecoveryAttempts++;\n\t\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t\t// but we don't want it in context for the retry)\n\t\t\tconst messages = this.agent.state.messages;\n\t\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t\t}\n\t\t\treturn this._runAutoCompaction(\"overflow\", true);\n\t\t}\n\n\t\t// Case 2: Threshold - context is getting large\n\t\t// For error messages (no usage data), estimate from last successful response.\n\t\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\t\tlet contextTokens: number;\n\t\tif (assistantMessage.stopReason === \"error\") {\n\t\t\tconst messages = this.agent.state.messages;\n\t\t\tconst estimate = estimateContextTokens(messages);\n\t\t\tif (estimate.lastUsageIndex === null) return false; // No usage data at all\n\t\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t\t// trigger compaction right after one just finished.\n\t\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\t\tif (\n\t\t\t\tcompactionEntry &&\n\t\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionEntry.timestamp).getTime()\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tcontextTokens = estimate.tokens;\n\t\t} else {\n\t\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t\t}\n\t\tconst decision = this._runtimeCompactionDecision(contextTokens, contextWindow, settings);\n\t\tif (decision.compact) {\n\t\t\treturn this._runThresholdCompaction(decision.emergency);\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate _overflowCompactionSettings(settings: CompactionSettings, attempt: number): CompactionSettings {\n\t\tif (attempt < MAX_OVERFLOW_RECOVERY_ATTEMPTS) return settings;\n\n\t\treturn {\n\t\t\t...settings,\n\t\t\treserveTokens: Math.min(settings.reserveTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),\n\t\t\treservedOutputTokens: Math.min(\n\t\t\t\tsettings.reservedOutputTokens ?? settings.reserveTokens,\n\t\t\t\tOVERFLOW_RECOVERY_EMERGENCY_TOKENS,\n\t\t\t),\n\t\t\tkeepRecentTokens: Math.min(settings.keepRecentTokens, OVERFLOW_RECOVERY_EMERGENCY_TOKENS),\n\t\t};\n\t}\n\n\t/**\n\t * Internal: Run auto-compaction with events.\n\t */\n\tprivate async _runAutoCompaction(\n\t\treason: \"overflow\" | \"threshold\",\n\t\twillRetry: boolean,\n\t\temergency = reason === \"overflow\" || this._thresholdCompactionEmergency,\n\t): Promise<boolean> {\n\t\tconst configuredSettings = this.settingsManager.getCompactionSettings();\n\t\tconst settings =\n\t\t\treason === \"overflow\"\n\t\t\t\t? this._overflowCompactionSettings(configuredSettings, this._overflowRecoveryAttempts)\n\t\t\t\t: configuredSettings;\n\n\t\tthis._emit({ type: \"compaction_start\", reason });\n\t\tthis._autoCompactionAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tif (!this.model) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst compactionModel = this._resolveCompactionModel(this.model);\n\t\t\tlet apiKey: string | undefined;\n\t\t\tlet headers: Record<string, string> | undefined;\n\t\t\tif (this.agent.streamFn === streamSimple) {\n\t\t\t\tconst authResult = await this._modelRegistry.getApiKeyAndHeaders(compactionModel);\n\t\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\t\tconst providerLabel = compactionModel.provider;\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\t\treason,\n\t\t\t\t\t\tresult: undefined,\n\t\t\t\t\t\taborted: false,\n\t\t\t\t\t\twillRetry: false,\n\t\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\t`Auto-compaction could not authenticate for \"${providerLabel}\" (${compactionModel.id}). ` +\n\t\t\t\t\t\t\t`Check proxy/OAuth health and run '/login ${providerLabel}' if needed.`,\n\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tapiKey = authResult.apiKey;\n\t\t\t\theaders = authResult.headers;\n\t\t\t} else {\n\t\t\t\t({ apiKey, headers } = await this._getCompactionRequestAuth(compactionModel));\n\t\t\t}\n\n\t\t\tconst begun = this._beginCompactionTransaction(compactionModel, emergency);\n\t\t\tconst pathEntries = [...begun.capture.branchEntries];\n\n\t\t\tconst preparation = prepareCompaction(pathEntries, settings);\n\t\t\tif (!preparation) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tlet extensionCompaction: CompactionResult | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_compact\")) {\n\t\t\t\tconst extensionResult = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_compact\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tbranchEntries: pathEntries,\n\t\t\t\t\tcustomInstructions: undefined,\n\t\t\t\t\tsignal: this._autoCompactionAbortController.signal,\n\t\t\t\t})) as SessionBeforeCompactResult | undefined;\n\n\t\t\t\tif (extensionResult?.cancel) {\n\t\t\t\t\tthis._emit({\n\t\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\t\treason,\n\t\t\t\t\t\tresult: undefined,\n\t\t\t\t\t\taborted: true,\n\t\t\t\t\t\twillRetry: false,\n\t\t\t\t\t});\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (extensionResult?.compaction) {\n\t\t\t\t\textensionCompaction = extensionResult.compaction;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlet summary: string;\n\t\t\tlet firstKeptEntryId: string;\n\t\t\tlet tokensBefore: number;\n\t\t\tlet details: unknown;\n\n\t\t\tif (extensionCompaction) {\n\t\t\t\t// Extension provided compaction content\n\t\t\t\tsummary = extensionCompaction.summary;\n\t\t\t\tfirstKeptEntryId = extensionCompaction.firstKeptEntryId;\n\t\t\t\ttokensBefore = extensionCompaction.tokensBefore;\n\t\t\t\tdetails = extensionCompaction.details;\n\t\t\t} else {\n\t\t\t\t// Generate compaction result\n\t\t\t\tconst compactResult = await compact(\n\t\t\t\t\tpreparation,\n\t\t\t\t\tcompactionModel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tundefined,\n\t\t\t\t\tthis._autoCompactionAbortController.signal,\n\t\t\t\t\tthis.thinkingLevel,\n\t\t\t\t\tthis.agent.streamFn,\n\t\t\t\t\tthis.settingsManager.getRetrySettings(),\n\t\t\t\t\tthis._summarizationRetryCallbacks({ source: \"compaction\", reason }),\n\t\t\t\t\tthis._compactionFailoverModels(compactionModel),\n\t\t\t\t);\n\t\t\t\tsummary = compactResult.summary;\n\t\t\t\tfirstKeptEntryId = compactResult.firstKeptEntryId;\n\t\t\t\ttokensBefore = compactResult.tokensBefore;\n\t\t\t\tdetails = compactResult.details;\n\t\t\t}\n\n\t\t\tif (this._autoCompactionAbortController.signal.aborted) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason,\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: true,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t});\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst result: CompactionResult = {\n\t\t\t\tsummary: redactCredentialShapedContent(redactSensitiveTextForced(summary)),\n\t\t\t\tfirstKeptEntryId,\n\t\t\t\ttokensBefore,\n\t\t\t\tdetails,\n\t\t\t};\n\t\t\tconst committed = this._commitCompaction(begun, result, fromExtension);\n\t\t\tthis.agent.state.messages = this.sessionManager.buildSessionContext().messages;\n\n\t\t\tif (this._extensionRunner) {\n\t\t\t\tawait this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_compact\",\n\t\t\t\t\tcompactionEntry: committed.entry,\n\t\t\t\t\tfromExtension,\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Whether the agent loop will resume after this compaction. Shared by\n\t\t\t// the compaction_end event (so the TUI flushes its queued messages\n\t\t\t// correctly) and the return value below.\n\t\t\tconst resumeMessages = this.agent.state.messages;\n\t\t\tlet lastAssistantMsg: AssistantMessage | undefined;\n\t\t\tfor (let i = resumeMessages.length - 1; i >= 0; i--) {\n\t\t\t\tconst candidate = resumeMessages[i];\n\t\t\t\tif (candidate.role === \"assistant\") {\n\t\t\t\t\tlastAssistantMsg = candidate as AssistantMessage;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst endedCleanly = lastAssistantMsg?.stopReason === \"stop\";\n\t\t\tconst willResume = this.agent.hasQueuedMessages() || !endedCleanly;\n\t\t\tconst emitWillRetry = compactionEmitWillRetry(willRetry, willResume);\n\t\t\tthis._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry: emitWillRetry });\n\n\t\t\tif (willRetry) {\n\t\t\t\tconst messages = this.agent.state.messages;\n\t\t\t\tconst lastMsg = messages[messages.length - 1];\n\t\t\t\tif (lastMsg?.role === \"assistant\" && (lastMsg as AssistantMessage).stopReason === \"error\") {\n\t\t\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Resume the agent loop after a successful auto-compaction unless the\n\t\t\t// agent had already finished cleanly. Previously this returned\n\t\t\t// `hasQueuedMessages()`, which made the agent stall right after the\n\t\t\t// purple \"Auto-compacting...\" indicator whenever no steer/follow-up\n\t\t\t// messages were queued — even when the last turn was cut off mid-task\n\t\t\t// (stopReason toolUse/length/error/aborted). Continue in that case so\n\t\t\t// the task runs to completion on the compacted context. Hysteresis\n\t\t\t// (disarmed until context drops below rearmRatio) prevents an immediate\n\t\t\t// re-trigger; a clean \"stop\" with nothing queued still ends the loop.\n\t\t\treturn willResume;\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\terrorMessage:\n\t\t\t\t\treason === \"overflow\"\n\t\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t\t});\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._autoCompactionAbortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Toggle auto-compaction setting.\n\t */\n\tsetAutoCompactionEnabled(enabled: boolean): void {\n\t\tthis.settingsManager.setCompactionEnabled(enabled);\n\t}\n\n\t/** Whether auto-compaction is enabled */\n\tget autoCompactionEnabled(): boolean {\n\t\treturn this.settingsManager.getCompactionEnabled();\n\t}\n\n\tasync bindExtensions(bindings: ExtensionBindings): Promise<void> {\n\t\tif (bindings.uiContext !== undefined) {\n\t\t\tthis._extensionUIContext = bindings.uiContext;\n\t\t}\n\t\tif (bindings.mode !== undefined) {\n\t\t\tthis._extensionMode = bindings.mode;\n\t\t}\n\t\tif (bindings.commandContextActions !== undefined) {\n\t\t\tthis._extensionCommandContextActions = bindings.commandContextActions;\n\t\t}\n\t\tif (bindings.abortHandler !== undefined) {\n\t\t\tthis._extensionAbortHandler = bindings.abortHandler;\n\t\t}\n\t\tif (bindings.shutdownHandler !== undefined) {\n\t\t\tthis._extensionShutdownHandler = bindings.shutdownHandler;\n\t\t}\n\t\tif (bindings.onError !== undefined) {\n\t\t\tthis._extensionErrorListener = bindings.onError;\n\t\t}\n\n\t\tthis._applyExtensionBindings(this._extensionRunner);\n\t\tawait this._extensionRunner.emit(this._sessionStartEvent);\n\t\tawait this.extendResourcesFromExtensions(this._sessionStartEvent.reason === \"reload\" ? \"reload\" : \"startup\");\n\t}\n\n\tprivate async extendResourcesFromExtensions(reason: \"startup\" | \"reload\"): Promise<void> {\n\t\tif (!this._extensionRunner.hasHandlers(\"resources_discover\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { skillPaths, promptPaths, themePaths } = await this._extensionRunner.emitResourcesDiscover(\n\t\t\tthis._cwd,\n\t\t\treason,\n\t\t);\n\n\t\tif (skillPaths.length === 0 && promptPaths.length === 0 && themePaths.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst extensionPaths: ResourceExtensionPaths = {\n\t\t\tskillPaths: this.buildExtensionResourcePaths(skillPaths),\n\t\t\tpromptPaths: this.buildExtensionResourcePaths(promptPaths),\n\t\t\tthemePaths: this.buildExtensionResourcePaths(themePaths),\n\t\t};\n\n\t\tthis._resourceLoader.extendResources(extensionPaths);\n\t\tthis._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());\n\t\tthis.agent.state.systemPrompt = this._baseSystemPrompt;\n\t\tthis.agent.state.systemPromptCacheBoundary = this._baseSystemPromptCacheBoundary;\n\t\tthis.agent.state.systemPromptCacheBoundaryBypass = false;\n\t\tthis._recordPromptCachePlan(\"resource-reload\");\n\t}\n\n\tprivate buildExtensionResourcePaths(entries: Array<{ path: string; extensionPath: string }>): Array<{\n\t\tpath: string;\n\t\tmetadata: { source: string; scope: \"temporary\"; origin: \"top-level\"; baseDir?: string };\n\t}> {\n\t\treturn entries.map((entry) => {\n\t\t\tconst source = this.getExtensionSourceLabel(entry.extensionPath);\n\t\t\tconst baseDir = entry.extensionPath.startsWith(\"<\") ? undefined : dirname(entry.extensionPath);\n\t\t\treturn {\n\t\t\t\tpath: entry.path,\n\t\t\t\tmetadata: {\n\t\t\t\t\tsource,\n\t\t\t\t\tscope: \"temporary\",\n\t\t\t\t\torigin: \"top-level\",\n\t\t\t\t\tbaseDir,\n\t\t\t\t},\n\t\t\t};\n\t\t});\n\t}\n\n\tprivate getExtensionSourceLabel(extensionPath: string): string {\n\t\tif (extensionPath.startsWith(\"<\")) {\n\t\t\treturn `extension:${extensionPath.replace(/[<>]/g, \"\")}`;\n\t\t}\n\t\tconst base = basename(extensionPath);\n\t\tconst name = base.replace(/\\.(ts|js)$/, \"\");\n\t\treturn `extension:${name}`;\n\t}\n\n\tprivate _applyExtensionBindings(runner: ExtensionRunner): void {\n\t\trunner.setUIContext(this._extensionUIContext, this._extensionMode);\n\t\trunner.bindCommandContext(this._extensionCommandContextActions);\n\n\t\tthis._extensionErrorUnsubscriber?.();\n\t\tthis._extensionErrorUnsubscriber = this._extensionErrorListener\n\t\t\t? runner.onError(this._extensionErrorListener)\n\t\t\t: undefined;\n\t}\n\n\tprivate _refreshCurrentModelFromRegistry(): void {\n\t\tconst currentModel = this.model;\n\t\tif (!currentModel) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst refreshedModel = this._modelRegistry.find(currentModel.provider, currentModel.id);\n\t\tif (!refreshedModel || refreshedModel === currentModel) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.agent.state.model = refreshedModel;\n\t}\n\n\tprivate _bindExtensionCore(runner: ExtensionRunner): void {\n\t\tconst getCommands = (): SlashCommandInfo[] => {\n\t\t\tconst extensionCommands: SlashCommandInfo[] = runner.getRegisteredCommands().map((command) => ({\n\t\t\t\tname: command.invocationName,\n\t\t\t\tdescription: command.description,\n\t\t\t\tsource: \"extension\",\n\t\t\t\tsourceInfo: command.sourceInfo,\n\t\t\t}));\n\n\t\t\tconst templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({\n\t\t\t\tname: template.name,\n\t\t\t\tdescription: template.description,\n\t\t\t\tsource: \"prompt\",\n\t\t\t\tsourceInfo: template.sourceInfo,\n\t\t\t}));\n\n\t\t\tconst skills: SlashCommandInfo[] = this._resourceLoader.getSkills().skills.map((skill) => ({\n\t\t\t\tname: `skill:${skill.name}`,\n\t\t\t\tdescription: skill.description,\n\t\t\t\tsource: \"skill\",\n\t\t\t\tsourceInfo: skill.sourceInfo,\n\t\t\t}));\n\n\t\t\treturn [...extensionCommands, ...templates, ...skills];\n\t\t};\n\n\t\trunner.bindCore(\n\t\t\t{\n\t\t\t\tsendMessage: (message, options) => {\n\t\t\t\t\tthis.sendCustomMessage(message, options).catch((err) => {\n\t\t\t\t\t\trunner.emitError({\n\t\t\t\t\t\t\textensionPath: \"<runtime>\",\n\t\t\t\t\t\t\tevent: \"send_message\",\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tsendUserMessage: (content, options) => {\n\t\t\t\t\tthis.sendUserMessage(content, options).catch((err) => {\n\t\t\t\t\t\trunner.emitError({\n\t\t\t\t\t\t\textensionPath: \"<runtime>\",\n\t\t\t\t\t\t\tevent: \"send_user_message\",\n\t\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\tappendEntry: (customType, data) => {\n\t\t\t\t\tthis.sessionManager.appendCustomEntry(customType, data);\n\t\t\t\t\tthis._recordEvidenceReceiptInvalidation(customType);\n\t\t\t\t},\n\t\t\t\tsetSessionName: (name) => {\n\t\t\t\t\tthis.setSessionName(name);\n\t\t\t\t},\n\t\t\t\tgetSessionName: () => {\n\t\t\t\t\treturn this.sessionManager.getSessionName();\n\t\t\t\t},\n\t\t\t\tsetLabel: (entryId, label) => {\n\t\t\t\t\tthis.sessionManager.appendLabelChange(entryId, label);\n\t\t\t\t},\n\t\t\t\tgetActiveTools: () => this.getActiveToolNames(),\n\t\t\t\tgetAllTools: () => this.getAllTools(),\n\t\t\t\tsetActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),\n\t\t\t\trefreshTools: () => this._refreshToolRegistry(),\n\t\t\t\tgetCommands,\n\t\t\t\tsetModel: async (model) => {\n\t\t\t\t\tif (!this.modelRegistry.hasConfiguredAuth(model)) return false;\n\t\t\t\t\tawait this.setModel(model);\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\tgetThinkingLevel: () => this.thinkingLevel,\n\t\t\t\tsetThinkingLevel: (level) => this.setThinkingLevel(level),\n\t\t\t\t// Optional: no in-process MCP client manager yet. Leave unbound so\n\t\t\t\t// ExtensionAPI.callMcpTool remains present (load-time capture) but throws\n\t\t\t\t// until a session-level handler is provided. Tests / future MCP hub bind here.\n\t\t\t\tcallMcpTool: undefined,\n\t\t\t},\n\t\t\t{\n\t\t\t\tgetModel: () => this.model,\n\t\t\t\tisIdle: () => !this.isStreaming,\n\t\t\t\tgetSignal: () => this.agent.signal,\n\t\t\t\tabort: () => {\n\t\t\t\t\tif (this._extensionAbortHandler) {\n\t\t\t\t\t\tthis._extensionAbortHandler();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tvoid this.abort();\n\t\t\t\t},\n\t\t\t\thasPendingMessages: () => this.pendingMessageCount > 0,\n\t\t\t\tshutdown: () => {\n\t\t\t\t\tthis._extensionShutdownHandler?.();\n\t\t\t\t},\n\t\t\t\tgetContextUsage: () => this.getContextUsage(),\n\t\t\t\tcompact: (options) => {\n\t\t\t\t\tvoid (async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst result = await this.compact(options?.customInstructions);\n\t\t\t\t\t\t\toptions?.onComplete?.(result);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconst err = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t\t\toptions?.onError?.(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t\tgetSystemPrompt: () => this.systemPrompt,\n\t\t\t\tgetSystemPromptOptions: () => this._baseSystemPromptOptions,\n\t\t\t},\n\t\t\t{\n\t\t\t\tregisterProvider: (name, config) => {\n\t\t\t\t\tthis._modelRegistry.registerProvider(name, config);\n\t\t\t\t\tthis._refreshCurrentModelFromRegistry();\n\t\t\t\t},\n\t\t\t\tunregisterProvider: (name) => {\n\t\t\t\t\tthis._modelRegistry.unregisterProvider(name);\n\t\t\t\t\tthis._refreshCurrentModelFromRegistry();\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\t/**\n\t * Connect configured MCP servers and register their tools for this session.\n\t *\n\t * Connection is lazy by construction: nothing is spawned until this is called,\n\t * and a server that fails to start is reported in the returned status instead\n\t * of taking the session down. Calling it twice replaces the previously\n\t * attached MCP tools rather than duplicating them.\n\t *\n\t * Returns per-server status so a caller can surface failures; an empty\n\t * configuration returns an empty array without spawning anything.\n\t */\n\tasync attachMcpServers(options?: {\n\t\tservers?: readonly McpServerConfig[];\n\t\tcallTimeoutMs?: number;\n\t}): Promise<McpServerStatus[]> {\n\t\tconst servers = options?.servers ?? loadMcpServerConfigs(this._cwd);\n\t\tthis._mcpManager?.close();\n\t\tthis._mcpManager = undefined;\n\t\tthis._customTools = this._customTools.filter((definition) => !this._mcpToolNames.has(definition.name));\n\t\tthis._mcpToolNames = new Set();\n\t\tif (servers.length === 0) {\n\t\t\tthis._refreshToolRegistry();\n\t\t\treturn [];\n\t\t}\n\n\t\tconst manager = new McpManager({\n\t\t\tservers,\n\t\t\tcwd: this._cwd,\n\t\t\tclientInfo: { name: APP_NAME, version: VERSION },\n\t\t\tcallTimeoutMs: options?.callTimeoutMs,\n\t\t});\n\t\tthis._mcpManager = manager;\n\t\tconst definitions = await manager.listToolDefinitions();\n\t\t// A builtin always wins a name collision; MCP must never shadow `bash`.\n\t\tconst usable = definitions.filter((definition) => !this._baseToolDefinitions.has(definition.name));\n\t\tthis._mcpToolNames = new Set(usable.map((definition) => definition.name));\n\t\tthis._customTools = [...this._customTools, ...(usable as ToolDefinition[])];\n\t\tthis._refreshToolRegistry();\n\t\treturn manager.status();\n\t}\n\n\t/** Status of MCP servers attached to this session. Empty when none were attached. */\n\tmcpServerStatus(): McpServerStatus[] {\n\t\treturn this._mcpManager?.status() ?? [];\n\t}\n\n\t/**\n\t * Ping attached MCP servers and return fresh status, so callers (e.g. the\n\t * status rail) can keep displaying real connectivity instead of the last\n\t * handshake result. Resolves to [] when no manager is attached.\n\t */\n\tmcpCheckHealth(options?: { pingTimeoutMs?: number; reconnectFailed?: boolean }): Promise<McpServerStatus[]> {\n\t\treturn this._mcpManager?.checkHealth(options) ?? Promise.resolve([]);\n\t}\n\n\tprivate _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {\n\t\tconst previousRegistryNames = new Set(this._toolRegistry.keys());\n\t\tconst previousActiveToolNames = this.getActiveToolNames();\n\t\tconst allowedToolNames = this._allowedToolNames;\n\t\tconst excludedToolNames = this._excludedToolNames;\n\t\tconst isAllowedTool = (name: string): boolean =>\n\t\t\t(!allowedToolNames || allowedToolNames.has(name)) && !excludedToolNames?.has(name);\n\n\t\tconst registeredTools = this._extensionRunner.getAllRegisteredTools();\n\t\tconst allCustomTools = [\n\t\t\t...registeredTools,\n\t\t\t...this._customTools.map((definition) => ({\n\t\t\t\tdefinition,\n\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: \"sdk\" }),\n\t\t\t})),\n\t\t].filter((tool) => isAllowedTool(tool.definition.name));\n\t\tif (this._loadoutAccessPolicy) {\n\t\t\tconst shadowedBuiltins = normalizeToolNames(\n\t\t\t\tallCustomTools.map((tool) => tool.definition.name).filter((name) => this._baseToolDefinitions.has(name)),\n\t\t\t);\n\t\t\tif (shadowedBuiltins.length > 0) {\n\t\t\t\tthrow new Error(`loadout extension tool shadows builtin: ${shadowedBuiltins.join(\", \")}`);\n\t\t\t}\n\t\t}\n\n\t\tconst definitionRegistry = new Map<string, ToolDefinitionEntry>(\n\t\t\tArray.from(this._baseToolDefinitions.entries()).flatMap(([name, definition]) =>\n\t\t\t\tisAllowedTool(name)\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdefinition,\n\t\t\t\t\t\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<builtin:${name}>`, { source: \"builtin\" }),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t] as [string, ToolDefinitionEntry],\n\t\t\t\t\t\t]\n\t\t\t\t\t: [],\n\t\t\t),\n\t\t);\n\t\tfor (const tool of allCustomTools) {\n\t\t\tdefinitionRegistry.set(tool.definition.name, {\n\t\t\t\tdefinition: tool.definition,\n\t\t\t\tsourceInfo: tool.sourceInfo,\n\t\t\t});\n\t\t}\n\t\tthis._toolDefinitions = definitionRegistry;\n\t\tthis._toolPromptSnippets = new Map(\n\t\t\tArray.from(definitionRegistry.values()).flatMap(({ definition }) => {\n\t\t\t\tconst snippet = this._normalizePromptSnippet(definition.promptSnippet);\n\t\t\t\treturn snippet ? [[definition.name, snippet] as const] : [];\n\t\t\t}),\n\t\t);\n\t\tthis._toolPromptGuidelines = new Map(\n\t\t\tArray.from(definitionRegistry.values()).flatMap(({ definition }) => {\n\t\t\t\tconst guidelines = this._normalizePromptGuidelines(definition.promptGuidelines);\n\t\t\t\treturn guidelines.length > 0 ? [[definition.name, guidelines] as const] : [];\n\t\t\t}),\n\t\t);\n\t\t// Tools contributed by extensions the harness itself loads (`<builtin:*>`),\n\t\t// as opposed to user/project/SDK extensions.\n\t\tconst builtinExtensionToolNames = new Set(\n\t\t\tallCustomTools.flatMap((tool) => (tool.sourceInfo.path.startsWith(\"<builtin:\") ? [tool.definition.name] : [])),\n\t\t);\n\t\tconst runner = this._extensionRunner;\n\t\tconst wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner);\n\t\tconst wrappedBuiltInTools = wrapRegisteredTools(\n\t\t\tArray.from(this._baseToolDefinitions.values()).flatMap((definition) =>\n\t\t\t\tisAllowedTool(definition.name)\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdefinition,\n\t\t\t\t\t\t\t\tsourceInfo: createSyntheticSourceInfo(`<builtin:${definition.name}>`, {\n\t\t\t\t\t\t\t\t\tsource: \"builtin\",\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: [],\n\t\t\t),\n\t\t\trunner,\n\t\t);\n\n\t\tconst toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));\n\t\tfor (const tool of wrappedExtensionTools as AgentTool[]) {\n\t\t\ttoolRegistry.set(tool.name, tool);\n\t\t}\n\t\tthis._toolRegistry = toolRegistry;\n\n\t\tif (this._loadoutAccessPolicy) {\n\t\t\tconst missingLockedTools = this._loadoutAccessPolicy.activeTools.filter(\n\t\t\t\t(toolName) => !isAllowedTool(toolName) || !this._toolRegistry.has(toolName),\n\t\t\t);\n\t\t\tif (missingLockedTools.length > 0) {\n\t\t\t\tthrow new Error(`loadout locked tool unavailable: ${missingLockedTools.join(\", \")}`);\n\t\t\t}\n\t\t\tthis.setActiveToolsByName([...this._loadoutAccessPolicy.activeTools]);\n\t\t\treturn;\n\t\t}\n\n\t\tconst nextActiveToolNames = (\n\t\t\toptions?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]\n\t\t).filter((name) => isAllowedTool(name));\n\n\t\tif (allowedToolNames) {\n\t\t\tfor (const toolName of this._toolRegistry.keys()) {\n\t\t\t\tif (allowedToolNames.has(toolName)) {\n\t\t\t\t\tnextActiveToolNames.push(toolName);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (options?.includeAllExtensionTools) {\n\t\t\t// \"Keep extension tools on\" is about the *user's* extensions. Tools shipped\n\t\t\t// by a built-in extension (e.g. update_todo) are part of the harness's own\n\t\t\t// tool surface, so they follow the built-in policy: when the caller asked\n\t\t\t// for no built-in tools, they stay off too.\n\t\t\tconst builtinToolsDisabled = options.activeToolNames?.length === 0;\n\t\t\tfor (const tool of wrappedExtensionTools) {\n\t\t\t\tif (builtinToolsDisabled && builtinExtensionToolNames.has(tool.name)) continue;\n\t\t\t\tnextActiveToolNames.push(tool.name);\n\t\t\t}\n\t\t} else if (!options?.activeToolNames) {\n\t\t\tfor (const toolName of this._toolRegistry.keys()) {\n\t\t\t\tif (!previousRegistryNames.has(toolName)) {\n\t\t\t\t\tnextActiveToolNames.push(toolName);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.setActiveToolsByName([...new Set(nextActiveToolNames)]);\n\t}\n\n\tprivate _buildRuntime(options: {\n\t\tactiveToolNames?: string[];\n\t\tflagValues?: Map<string, boolean | string>;\n\t\tincludeAllExtensionTools?: boolean;\n\t}): void {\n\t\tconst autoResizeImages = this.settingsManager.getImageAutoResize();\n\t\tconst shellCommandPrefix = this.settingsManager.getShellCommandPrefix();\n\t\tconst shellPath = this.settingsManager.getShellPath();\n\t\tconst loadoutAccessPolicy = this._loadoutAccessPolicy;\n\t\tconst loadoutAccessGuard = loadoutAccessPolicy\n\t\t\t? (request: Parameters<typeof decideLoadoutAccess>[1]) => decideLoadoutAccess(loadoutAccessPolicy, request)\n\t\t\t: undefined;\n\t\tconst loadoutReadOptions = loadoutAccessGuard\n\t\t\t? {\n\t\t\t\t\tcanReadPath: (path: string) => loadoutAccessGuard({ operation: \"read\", toolName: \"read\", path }).allowed,\n\t\t\t\t}\n\t\t\t: {};\n\t\tconst loadoutWriteOptions = loadoutAccessGuard\n\t\t\t? {\n\t\t\t\t\tcanWritePath: (path: string) =>\n\t\t\t\t\t\tloadoutAccessGuard({ operation: \"write\", toolName: \"write\", path }).allowed,\n\t\t\t\t}\n\t\t\t: {};\n\t\tconst baseToolDefinitions = (() => {\n\t\t\tif (this._baseToolsOverride) {\n\t\t\t\treturn Object.fromEntries(\n\t\t\t\t\tObject.entries(this._baseToolsOverride).map(([name, tool]) => [\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tcreateToolDefinitionFromAgentTool(tool),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn createAllToolDefinitions(this._cwd, {\n\t\t\t\tread: { autoResizeImages, ...loadoutReadOptions },\n\t\t\t\tbash: {\n\t\t\t\t\tcommandPrefix: shellCommandPrefix,\n\t\t\t\t\tshellPath,\n\t\t\t\t\t...(loadoutAccessGuard ? { loadoutAccessGuard } : {}),\n\t\t\t\t\t...(() => {\n\t\t\t\t\t\tconst sandboxPolicy = this._getBashSandboxPreflight();\n\t\t\t\t\t\treturn sandboxPolicy ? { sandboxPolicy } : {};\n\t\t\t\t\t})(),\n\t\t\t\t\t...(() => {\n\t\t\t\t\t\tconst evidenceExecutor = this._getVerifiedEvidenceExecutor();\n\t\t\t\t\t\tif (!evidenceExecutor || !this._replayGoalId) return {};\n\t\t\t\t\t\tconst { shell } = getShellConfig(shellPath);\n\t\t\t\t\t\tconst inner = createLocalBashOperations({\n\t\t\t\t\t\t\t...(shellPath !== undefined ? { shellPath } : {}),\n\t\t\t\t\t\t\tsandboxPolicy: this._getBashSandboxPreflight(),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\toperations: createVerifiedBashOperations(inner, {\n\t\t\t\t\t\t\t\tevidenceExecutor,\n\t\t\t\t\t\t\t\tgoalId: this._replayGoalId,\n\t\t\t\t\t\t\t\t...(this._replayLaneId !== undefined ? { laneId: this._replayLaneId } : {}),\n\t\t\t\t\t\t\t\tshell,\n\t\t\t\t\t\t\t\tworkspaceScope: resolveSessionWorkspaceScope(this._cwd),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t};\n\t\t\t\t\t})(),\n\t\t\t\t},\n\t\t\t\tedit: loadoutWriteOptions,\n\t\t\t\twrite: loadoutWriteOptions,\n\t\t\t});\n\t\t})();\n\n\t\tthis._baseToolDefinitions = new Map(\n\t\t\tObject.entries(baseToolDefinitions).map(([name, tool]) => [name, tool as ToolDefinition]),\n\t\t);\n\n\t\tconst extensionsResult = this._resourceLoader.getExtensions();\n\t\tif (options.flagValues) {\n\t\t\tfor (const [name, value] of options.flagValues) {\n\t\t\t\textensionsResult.runtime.flagValues.set(name, value);\n\t\t\t}\n\t\t}\n\n\t\tthis._extensionRunner = new ExtensionRunner(\n\t\t\textensionsResult.extensions,\n\t\t\textensionsResult.runtime,\n\t\t\tthis._cwd,\n\t\t\tthis.sessionManager,\n\t\t\tthis._modelRegistry,\n\t\t);\n\t\tif (this._extensionRunnerRef) {\n\t\t\tthis._extensionRunnerRef.current = this._extensionRunner;\n\t\t}\n\t\tthis._bindExtensionCore(this._extensionRunner);\n\t\tthis._applyExtensionBindings(this._extensionRunner);\n\n\t\tconst defaultActiveToolNames = this._baseToolsOverride\n\t\t\t? Object.keys(this._baseToolsOverride)\n\t\t\t: [\"read\", \"bash\", \"edit\", \"write\", \"diagnostics\"];\n\t\tconst baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;\n\t\tthis._refreshToolRegistry({\n\t\t\tactiveToolNames: baseActiveToolNames,\n\t\t\tincludeAllExtensionTools: options.includeAllExtensionTools,\n\t\t});\n\t}\n\n\tasync reload(): Promise<void> {\n\t\tconst previousFlagValues = this._extensionRunner.getFlagValues();\n\t\tawait emitSessionShutdownEvent(this._extensionRunner, { type: \"session_shutdown\", reason: \"reload\" });\n\t\tawait this.settingsManager.reload();\n\t\tthis._invalidateContextBudgetCache({ type: \"settings\" });\n\t\tresetApiProviders();\n\t\tawait this._resourceLoader.reload();\n\t\tthis._buildRuntime({\n\t\t\tactiveToolNames: this.getActiveToolNames(),\n\t\t\tflagValues: previousFlagValues,\n\t\t\tincludeAllExtensionTools: true,\n\t\t});\n\n\t\tconst hasBindings =\n\t\t\tthis._extensionUIContext ||\n\t\t\tthis._extensionCommandContextActions ||\n\t\t\tthis._extensionShutdownHandler ||\n\t\t\tthis._extensionErrorListener;\n\t\tif (hasBindings) {\n\t\t\tawait this._extensionRunner.emit({ type: \"session_start\", reason: \"reload\" });\n\t\t\tawait this.extendResourcesFromExtensions(\"reload\");\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Auto-Retry\n\t// =========================================================================\n\n\t/**\n\t * Check if an error is retryable (overloaded, rate limit, server errors).\n\t * Context overflow errors are NOT retryable (handled by compaction instead).\n\t */\n\tprivate _isRetryableError(message: AssistantMessage): boolean {\n\t\treturn isRetryableAssistantError(message, this.model?.contextWindow ?? 0);\n\t}\n\n\t/**\n\t * Retry policy + callbacks shared by compaction and branch-summary summarization calls.\n\t * Uses the same `settings.retry` budget/backoff as agent-turn retries so a single transient\n\t * stream drop no longer fails the whole operation. `source` carries the context\n\t * the TUI needs to render the retry and recreate the underlying indicator.\n\t */\n\tprivate _summarizationRetryCallbacks(\n\t\tsource: { source: \"branchSummary\" } | { source: \"compaction\"; reason: \"manual\" | \"threshold\" | \"overflow\" },\n\t): RetryCallbacks {\n\t\treturn {\n\t\t\tonRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"summarization_retry_scheduled\",\n\t\t\t\t\tattempt,\n\t\t\t\t\tmaxAttempts,\n\t\t\t\t\tdelayMs,\n\t\t\t\t\terrorMessage,\n\t\t\t\t});\n\t\t\t},\n\t\t\tonRetryAttemptStart: () => {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"summarization_retry_attempt_start\",\n\t\t\t\t\t...source,\n\t\t\t\t});\n\t\t\t},\n\t\t\tonRetryFinished: () => {\n\t\t\t\tthis._emit({ type: \"summarization_retry_finished\" });\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * Prepare a retryable error for continuation with exponential backoff.\n\t * @returns true if the caller should continue the agent, false otherwise\n\t */\n\t/** Eject sticky safety model at prompt boundary (session resume / leftover default). */\n\tprivate async _ejectStickySafetyModelIfNeeded(): Promise<void> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\t!shouldEjectStickySafetyModel({\n\t\t\t\tblockStickySafetyModels: resilience.blockStickySafetyModels,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t})\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tconst current = this.model;\n\t\tif (!current || !isStickySafetyModel(current.id, current.provider)) return;\n\n\t\tconst pick = pickFailoverCandidate(\n\t\t\tresilience.failoverCandidates,\n\t\t\t{ provider: current.provider, id: current.id },\n\t\t\t(c) => {\n\t\t\t\tconst next = this._modelRegistry.find(c.provider, c.id);\n\t\t\t\tif (!next) return false;\n\t\t\t\treturn this._modelRegistry.hasConfiguredAuth(next);\n\t\t\t},\n\t\t);\n\t\tif (!pick) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(current.id, current.provider));\n\t\t}\n\t\tconst next = this._modelRegistry.find(pick.provider, pick.id);\n\t\tif (!next) {\n\t\t\tthrow new Error(stickySafetyBlockMessage(current.id, current.provider));\n\t\t}\n\t\t// Bypass setModel sticky check by temporarily allowing via direct state switch path:\n\t\t// setModel itself blocks sticky targets, not sources — safe.\n\t\tawait this.setModel(next);\n\t}\n\n\t/**\n\t * Rotate to another authenticated provider route serving the SAME model\n\t * family (e.g. openrouter/stealth/ox-alpha ↔ opencode-go/ox-alpha-free ↔\n\t * opencode/x-preview-f-free). Used when the current route fails with an\n\t * upstream availability error — same-model retry would hammer a dead\n\t * endpoint while the identical model is one route away. Visited and\n\t * unauthenticated routes are marked in {@link _refusedModels} so repeat\n\t * failures advance the rotation within the turn.\n\t */\n\tprivate async _maybeRotateModelRoutes(): Promise<string | undefined> {\n\t\tconst current = this.model;\n\t\tif (!current) return undefined;\n\t\tconst candidates = sameModelRouteCandidates(current, this._modelRegistry.getAvailable());\n\t\tfor (const candidate of candidates) {\n\t\t\tconst key = failoverModelKey(candidate.provider, candidate.id);\n\t\t\tif (this._refusedModels.has(key)) continue;\n\t\t\tconst next = this._modelRegistry.find(candidate.provider, candidate.id);\n\t\t\tif (!next || !this._modelRegistry.hasConfiguredAuth(next)) {\n\t\t\t\tthis._refusedModels.add(key);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.setModel(next);\n\t\t\t\t// The old route just failed upstream — keep it out of this turn's retries.\n\t\t\t\tthis._refusedModels.add(failoverModelKey(current.provider, current.id));\n\t\t\t\treturn `${next.provider}/${next.id}`;\n\t\t\t} catch {\n\t\t\t\tthis._refusedModels.add(key);\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Content/safety stops (stop_reason=refusal) are often false positives on coding turns\n\t * for Fable AND Claude Opus/Sonnet. Retrying the same model usually reprints the refusal —\n\t * switch via provider-resilience chain first. Failover targets still skip sticky models.\n\t */\n\tprivate async _maybeFailoverFromSafetyStop(message: AssistantMessage): Promise<string | undefined> {\n\t\tconst resilience = resolveProviderResilience(this.settingsManager.getProviderResilienceSettings());\n\t\tif (\n\t\t\t!shouldHonorSafetyFailover({\n\t\t\t\tautoFailoverOnSafetyStop: resilience.autoFailoverOnSafetyStop,\n\t\t\t\tmodelPinned: this._modelPinned,\n\t\t\t\tmodelId: this.model?.id,\n\t\t\t\tprovider: this.model?.provider,\n\t\t\t})\n\t\t) {\n\t\t\treturn undefined;\n\t\t}\n\t\t// Fire on safety-stop FPs AND on billing/quota exhaustion: both mean the\n\t\t// current model cannot finish this turn and same-model retry is useless.\n\t\tif (!isFailoverTriggerError(message.errorMessage)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst current = this.model;\n\t\t// v10.0-Ω: do NOT gate on isStickySafetyModel(source).\n\t\t// claude-opus-5 emits the same stop_reason=refusal FP; same-model retry is useless.\n\n\t\t// v10.3-Ω: mark the model that just refused so we never re-pick it this turn.\n\t\tif (current) this._refusedModels.add(failoverModelKey(current.provider, current.id));\n\n\t\tconst pick = pickFailoverCandidate(\n\t\t\tresilience.failoverCandidates,\n\t\t\tcurrent ? { provider: current.provider, id: current.id } : undefined,\n\t\t\t(c) => {\n\t\t\t\t// Skip any model that already refused/failed this turn → advance the chain.\n\t\t\t\tif (this._refusedModels.has(failoverModelKey(c.provider, c.id))) return false;\n\t\t\t\tconst next = this._modelRegistry.find(c.provider, c.id);\n\t\t\t\tif (!next) return false;\n\t\t\t\treturn this._modelRegistry.hasConfiguredAuth(next);\n\t\t\t},\n\t\t);\n\t\tif (!pick) return undefined;\n\n\t\tconst next = this._modelRegistry.find(pick.provider, pick.id);\n\t\tif (!next) return undefined;\n\t\ttry {\n\t\t\tawait this.setModel(next);\n\t\t\treturn `${next.provider}/${next.id}`;\n\t\t} catch {\n\t\t\t// setModel failed (auth/guard) — blacklist so next retry advances further.\n\t\t\tthis._refusedModels.add(failoverModelKey(pick.provider, pick.id));\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate async _prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst settings = this.settingsManager.getRetrySettings();\n\t\tconst maxRetries = retryBudgetForAssistantError(message, settings.maxRetries);\n\t\tconst attempt = nextRetryAttempt({\n\t\t\tenabled: settings.enabled,\n\t\t\tcompletedAttempts: this._retryAttempt,\n\t\t\tmaxRetries,\n\t\t});\n\t\tif (attempt === undefined) return false;\n\t\tthis._retryAttempt = attempt;\n\n\t\t// Upstream-unavailable (gateway 5xx / dropped stream): rotate to another\n\t\t// authenticated route of the SAME model family so the retry lands on a\n\t\t// live endpoint instead of hammering the dead one.\n\t\tconst rotatedTo = isUpstreamUnavailableMessage(message.errorMessage)\n\t\t\t? await this._maybeRotateModelRoutes()\n\t\t\t: undefined;\n\n\t\t// Content/safety stop (Fable/Opus/Sonnet): switch model BEFORE delay so retry is not same-model refusal.\n\t\tconst failoverTo = await this._maybeFailoverFromSafetyStop(message);\n\t\tconst switchedVia = failoverTo ? `failover ${failoverTo}` : rotatedTo ? `route ${rotatedTo}` : undefined;\n\t\t// Safety stops are usually immediate false positives — short delay after failover, full backoff otherwise.\n\t\tconst delayMs = computeRetryDelayMs(settings.baseDelayMs, attempt, switchedVia !== undefined);\n\t\tconst errorMessage = switchedVia\n\t\t\t? `${message.errorMessage || \"content/safety stop\"} → ${switchedVia}`\n\t\t\t: message.errorMessage || \"Unknown error\";\n\n\t\tthis._emit({\n\t\t\ttype: \"auto_retry_start\",\n\t\t\tattempt,\n\t\t\tmaxAttempts: maxRetries,\n\t\t\tdelayMs,\n\t\t\terrorMessage,\n\t\t});\n\n\t\t// Remove error message from agent state (keep in session for history)\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\t// Wait with exponential backoff (abortable)\n\t\tthis._retryAbortController = new AbortController();\n\t\ttry {\n\t\t\tawait sleep(delayMs, this._retryAbortController.signal);\n\t\t} catch {\n\t\t\t// Aborted during sleep - emit end event so UI can clean up\n\t\t\tconst attempt = this._retryAttempt;\n\t\t\tthis._retryAttempt = 0;\n\t\t\tthis._refusedModels.clear();\n\t\t\tthis._emit({\n\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\tsuccess: false,\n\t\t\t\tattempt,\n\t\t\t\tfinalError: \"Retry cancelled\",\n\t\t\t});\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._retryAbortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Cancel in-progress retry.\n\t */\n\tabortRetry(): void {\n\t\tthis._retryAbortController?.abort();\n\t}\n\n\t/** Whether auto-retry is currently in progress */\n\tget isRetrying(): boolean {\n\t\treturn this._retryAbortController !== undefined;\n\t}\n\n\t/** Whether auto-retry is enabled */\n\tget autoRetryEnabled(): boolean {\n\t\treturn this.settingsManager.getRetryEnabled();\n\t}\n\n\t/**\n\t * Toggle auto-retry setting.\n\t */\n\tsetAutoRetryEnabled(enabled: boolean): void {\n\t\tthis.settingsManager.setRetryEnabled(enabled);\n\t}\n\n\t// =========================================================================\n\t// Bash Execution\n\t// =========================================================================\n\n\t/**\n\t * Execute a bash command.\n\t * Adds result to agent context and session.\n\t * @param command The bash command to execute\n\t * @param onChunk Optional streaming callback for output\n\t * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix)\n\t * @param options.operations Custom BashOperations for remote execution\n\t * @param options.safetyGate When \"headless\", pre-classify the command and deny confirm/block-tier verdicts without interactive confirmation\n\t * @param options.sandboxPolicy Trusted sandbox preflight for local bash execution (never sourced from RPC payloads)\n\t */\n\tasync executeBash(\n\t\tcommand: string,\n\t\tonChunk?: (chunk: string) => void,\n\t\toptions?: ExecuteBashOptions,\n\t): Promise<BashResult> {\n\t\treturn this._bashService.executeBash(command, onChunk, options);\n\t}\n\n\t/**\n\t * Record a bash execution result in session history.\n\t * Used by executeBash and by extensions that handle bash execution themselves.\n\t */\n\trecordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {\n\t\tthis._bashService.recordBashResult(command, result, options);\n\t}\n\n\t/**\n\t * Cancel running bash command.\n\t */\n\tabortBash(): void {\n\t\tthis._bashService.abortBash();\n\t}\n\n\t/** Whether a bash command is currently running */\n\tget isBashRunning(): boolean {\n\t\treturn this._bashService.isBashRunning;\n\t}\n\n\t/** Whether there are pending bash messages waiting to be flushed */\n\tget hasPendingBashMessages(): boolean {\n\t\treturn this._bashService.hasPendingBashMessages;\n\t}\n\n\t/**\n\t * Flush pending bash messages to agent state and session.\n\t * Called after agent turn completes to maintain proper message ordering.\n\t */\n\tprivate _flushPendingBashMessages(): void {\n\t\tthis._bashService.flushPending();\n\t}\n\n\t// =========================================================================\n\t// Session Management\n\t// =========================================================================\n\n\t/**\n\t * Set a display name for the current session.\n\t */\n\tsetSessionName(name: string): void {\n\t\tthis.sessionManager.appendSessionInfo(name);\n\t\tthis._emit({ type: \"session_info_changed\", name: this.sessionManager.getSessionName() });\n\t}\n\n\t// =========================================================================\n\t// Tree Navigation\n\t// =========================================================================\n\n\t/**\n\t * Navigate to a different node in the session tree.\n\t * Unlike fork() which creates a new session file, this stays in the same file.\n\t *\n\t * @param targetId The entry ID to navigate to\n\t * @param options.summarize Whether user wants to summarize abandoned branch\n\t * @param options.customInstructions Custom instructions for summarizer\n\t * @param options.replaceInstructions If true, customInstructions replaces the default prompt\n\t * @param options.label Label to attach to the branch summary entry\n\t * @returns Result with editorText (if user message) and cancelled status\n\t */\n\tasync navigateTree(\n\t\ttargetId: string,\n\t\toptions: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string } = {},\n\t): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry }> {\n\t\tconst oldLeafId = this.sessionManager.getLeafId();\n\n\t\t// No-op if already at target\n\t\tif (targetId === oldLeafId) {\n\t\t\treturn { cancelled: false };\n\t\t}\n\n\t\t// Model required for summarization\n\t\tif (options.summarize && !this.model) {\n\t\t\tthrow new Error(\"No model available for summarization\");\n\t\t}\n\n\t\tconst targetEntry = this.sessionManager.getEntry(targetId);\n\t\tif (!targetEntry) {\n\t\t\tthrow new Error(`Entry ${targetId} not found`);\n\t\t}\n\n\t\t// Collect entries to summarize (from old leaf to common ancestor)\n\t\tconst { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary(\n\t\t\tthis.sessionManager,\n\t\t\toldLeafId,\n\t\t\ttargetId,\n\t\t);\n\n\t\t// Prepare event data - mutable so extensions can override\n\t\tlet customInstructions = options.customInstructions;\n\t\tlet replaceInstructions = options.replaceInstructions;\n\t\tlet label = options.label;\n\n\t\tconst preparation: TreePreparation = {\n\t\t\ttargetId,\n\t\t\toldLeafId,\n\t\t\tcommonAncestorId,\n\t\t\tentriesToSummarize,\n\t\t\tuserWantsSummary: options.summarize ?? false,\n\t\t\tcustomInstructions,\n\t\t\treplaceInstructions,\n\t\t\tlabel,\n\t\t};\n\n\t\t// Set up abort controller for summarization\n\t\tthis._branchSummaryAbortController = new AbortController();\n\n\t\ttry {\n\t\t\tlet extensionSummary: { summary: string; details?: unknown } | undefined;\n\t\t\tlet fromExtension = false;\n\n\t\t\t// Emit session_before_tree event\n\t\t\tif (this._extensionRunner.hasHandlers(\"session_before_tree\")) {\n\t\t\t\tconst result = (await this._extensionRunner.emit({\n\t\t\t\t\ttype: \"session_before_tree\",\n\t\t\t\t\tpreparation,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t})) as SessionBeforeTreeResult | undefined;\n\n\t\t\t\tif (result?.cancel) {\n\t\t\t\t\treturn { cancelled: true };\n\t\t\t\t}\n\n\t\t\t\tif (result?.summary && options.summarize) {\n\t\t\t\t\textensionSummary = result.summary;\n\t\t\t\t\tfromExtension = true;\n\t\t\t\t}\n\n\t\t\t\t// Allow extensions to override instructions and label\n\t\t\t\tif (result?.customInstructions !== undefined) {\n\t\t\t\t\tcustomInstructions = result.customInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.replaceInstructions !== undefined) {\n\t\t\t\t\treplaceInstructions = result.replaceInstructions;\n\t\t\t\t}\n\t\t\t\tif (result?.label !== undefined) {\n\t\t\t\t\tlabel = result.label;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Run default summarizer if needed\n\t\t\tlet summaryText: string | undefined;\n\t\t\tlet summaryDetails: unknown;\n\t\t\tif (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {\n\t\t\t\tconst model = this.model;\n\t\t\t\tif (!model) throw new Error(\"No model available for summarization\");\n\t\t\t\tconst { apiKey, headers } = await this._getRequiredRequestAuth(model);\n\t\t\t\tconst branchSummarySettings = this.settingsManager.getBranchSummarySettings();\n\t\t\t\tconst result = await generateBranchSummary(entriesToSummarize, {\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\theaders,\n\t\t\t\t\tsignal: this._branchSummaryAbortController.signal,\n\t\t\t\t\tcustomInstructions,\n\t\t\t\t\treplaceInstructions,\n\t\t\t\t\treserveTokens: branchSummarySettings.reserveTokens,\n\t\t\t\t\tstreamFn: this.agent.streamFn,\n\t\t\t\t\tretry: this.settingsManager.getRetrySettings(),\n\t\t\t\t\tcallbacks: this._summarizationRetryCallbacks({ source: \"branchSummary\" }),\n\t\t\t\t});\n\t\t\t\tif (result.aborted) {\n\t\t\t\t\treturn { cancelled: true, aborted: true };\n\t\t\t\t}\n\t\t\t\tif (result.error) {\n\t\t\t\t\tthrow new Error(result.error);\n\t\t\t\t}\n\t\t\t\tsummaryText = result.summary;\n\t\t\t\tsummaryDetails = {\n\t\t\t\t\treadFiles: result.readFiles || [],\n\t\t\t\t\tmodifiedFiles: result.modifiedFiles || [],\n\t\t\t\t};\n\t\t\t} else if (extensionSummary) {\n\t\t\t\tsummaryText = extensionSummary.summary;\n\t\t\t\tsummaryDetails = extensionSummary.details;\n\t\t\t}\n\n\t\t\t// Determine the new leaf position based on target type\n\t\t\tlet newLeafId: string | null;\n\t\t\tlet editorText: string | undefined;\n\n\t\t\tif (targetEntry.type === \"message\" && targetEntry.message.role === \"user\") {\n\t\t\t\t// User message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\teditorText = this._extractUserMessageText(targetEntry.message.content);\n\t\t\t} else if (targetEntry.type === \"custom_message\") {\n\t\t\t\t// Custom message: leaf = parent (null if root), text goes to editor\n\t\t\t\tnewLeafId = targetEntry.parentId;\n\t\t\t\tif (typeof targetEntry.content === \"string\") {\n\t\t\t\t\teditorText = targetEntry.content;\n\t\t\t\t} else {\n\t\t\t\t\teditorText = targetEntry.content\n\t\t\t\t\t\t.flatMap((item) => {\n\t\t\t\t\t\t\tif (item.type === \"text\") return [item.text];\n\t\t\t\t\t\t\treturn [];\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.join(\"\");\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Non-user message: leaf = selected node\n\t\t\t\tnewLeafId = targetId;\n\t\t\t}\n\n\t\t\t// Switch leaf (with or without summary)\n\t\t\t// Summary is attached at the navigation target position (newLeafId), not the old branch\n\t\t\tlet summaryEntry: BranchSummaryEntry | undefined;\n\t\t\tif (summaryText) {\n\t\t\t\t// Create summary at target position (can be null for root)\n\t\t\t\tconst summaryId = this.sessionManager.branchWithSummary(\n\t\t\t\t\tnewLeafId,\n\t\t\t\t\tsummaryText,\n\t\t\t\t\tsummaryDetails,\n\t\t\t\t\tfromExtension,\n\t\t\t\t);\n\t\t\t\tsummaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry;\n\n\t\t\t\t// Attach label to the summary entry\n\t\t\t\tif (label) {\n\t\t\t\t\tthis.sessionManager.appendLabelChange(summaryId, label);\n\t\t\t\t}\n\t\t\t} else if (newLeafId === null) {\n\t\t\t\t// No summary, navigating to root - reset leaf\n\t\t\t\tthis.sessionManager.resetLeaf();\n\t\t\t} else {\n\t\t\t\t// No summary, navigating to non-root\n\t\t\t\tthis.sessionManager.branch(newLeafId);\n\t\t\t}\n\n\t\t\t// Attach label to target entry when not summarizing (no summary entry to label)\n\t\t\tif (label && !summaryText) {\n\t\t\t\tthis.sessionManager.appendLabelChange(targetId, label);\n\t\t\t}\n\n\t\t\t// Update agent state\n\t\t\tconst sessionContext = this.sessionManager.buildSessionContext();\n\t\t\tthis.agent.state.messages = sessionContext.messages;\n\n\t\t\t// Emit session_tree event\n\t\t\tawait this._extensionRunner.emit({\n\t\t\t\ttype: \"session_tree\",\n\t\t\t\tnewLeafId: this.sessionManager.getLeafId(),\n\t\t\t\toldLeafId,\n\t\t\t\tsummaryEntry,\n\t\t\t\tfromExtension: summaryText ? fromExtension : undefined,\n\t\t\t});\n\n\t\t\t// Emit to custom tools\n\n\t\t\treturn { editorText, cancelled: false, summaryEntry };\n\t\t} finally {\n\t\t\tthis._branchSummaryAbortController = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Get all user messages from session for fork selector.\n\t */\n\tgetUserMessagesForForking(): Array<{ entryId: string; text: string }> {\n\t\tconst entries = this.sessionManager.getEntries();\n\t\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type !== \"message\") continue;\n\t\t\tif (entry.message.role !== \"user\") continue;\n\n\t\t\tconst text = this._extractUserMessageText(entry.message.content);\n\t\t\tif (text) {\n\t\t\t\tresult.push({ entryId: entry.id, text });\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate _extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\t\tif (typeof content === \"string\") return content;\n\t\tif (Array.isArray(content)) {\n\t\t\treturn content\n\t\t\t\t.flatMap((item) => (item.type === \"text\" && typeof item.text === \"string\" ? [item.text] : []))\n\t\t\t\t.join(\"\");\n\t\t}\n\t\treturn \"\";\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tgetSessionStats(): SessionStats {\n\t\tconst state = this.state;\n\t\tconst userMessages = state.messages.filter((m) => m.role === \"user\").length;\n\t\tconst assistantMessages = state.messages.filter((m) => m.role === \"assistant\").length;\n\t\tconst toolResults = state.messages.filter((m) => m.role === \"toolResult\").length;\n\n\t\tlet toolCalls = 0;\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const message of state.messages) {\n\t\t\tif (message.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\tconst providerEligibleInputTokens = totalInput + totalCacheRead + totalCacheWrite;\n\t\treturn {\n\t\t\tsessionFile: this.sessionFile,\n\t\t\tsessionId: this.sessionId,\n\t\t\tuserMessages,\n\t\t\tassistantMessages,\n\t\t\ttoolCalls,\n\t\t\ttoolResults,\n\t\t\ttotalMessages: state.messages.length,\n\t\t\ttokens: {\n\t\t\t\tinput: totalInput,\n\t\t\t\toutput: totalOutput,\n\t\t\t\tcacheRead: totalCacheRead,\n\t\t\t\tcacheWrite: totalCacheWrite,\n\t\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t\t},\n\t\t\tpromptCache: {\n\t\t\t\tproviderEligibleInputTokens,\n\t\t\t\tproviderHitRate: providerEligibleInputTokens > 0 ? totalCacheRead / providerEligibleInputTokens : 0,\n\t\t\t\tkeyChanges: this._promptCacheKeyChanges,\n\t\t\t\tboundaryBypasses: this._promptCacheBoundaryBypasses,\n\t\t\t\tstablePrefixCharacters: this._promptCacheStablePrefixCharacters,\n\t\t\t\t...(this._promptCacheLastBreakReason ? { lastBreakReason: this._promptCacheLastBreakReason } : {}),\n\t\t\t},\n\t\t\tcost: totalCost,\n\t\t\tcontextUsage: this.getContextUsage(),\n\t\t};\n\t}\n\n\tgetContextUsage(): ContextUsage | undefined {\n\t\tconst model = this.model;\n\t\tif (!model) return undefined;\n\n\t\tconst contextWindow = model.contextWindow ?? 0;\n\t\tif (contextWindow <= 0) return undefined;\n\n\t\t// After compaction, the last assistant usage reflects pre-compaction context size.\n\t\t// We can only trust usage from an assistant that responded after the latest compaction.\n\t\t// If no such assistant exists, context token count is unknown until the next LLM response.\n\t\tconst branchEntries = this.sessionManager.getBranch();\n\t\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\t\tif (latestCompaction) {\n\t\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\t\tlet hasPostCompactionUsage = false;\n\t\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\t\tconst entry = branchEntries[i];\n\t\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\t\tconst assistant = entry.message;\n\t\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!hasPostCompactionUsage) {\n\t\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t\t}\n\t\t}\n\n\t\tconst estimate = estimateContextTokens(this.messages);\n\t\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\t\treturn {\n\t\t\ttokens: estimate.tokens,\n\t\t\tcontextWindow,\n\t\t\tpercent,\n\t\t};\n\t}\n\n\t/**\n\t * Export session to HTML.\n\t * @param outputPath Optional output path (defaults to session directory)\n\t * @returns Path to exported file\n\t */\n\tasync exportToHtml(outputPath?: string): Promise<string> {\n\t\tconst themeName = this.settingsManager.getTheme();\n\n\t\t// Create tool renderer if we have an extension runner (for custom tool HTML rendering)\n\t\tconst toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({\n\t\t\tgetToolDefinition: (name) => this.getToolDefinition(name),\n\t\t\ttheme,\n\t\t\tcwd: this.sessionManager.getCwd(),\n\t\t});\n\n\t\treturn exportSessionToHtml(this.sessionManager, this.state, {\n\t\t\toutputPath,\n\t\t\tthemeName,\n\t\t\ttoolRenderer,\n\t\t});\n\t}\n\n\t/**\n\t * Export the current session branch to a JSONL file.\n\t * Writes the session header followed by all entries on the current branch path.\n\t * @param outputPath Target file path. If omitted, generates a timestamped file in cwd.\n\t * @returns The resolved output file path.\n\t */\n\texportToJsonl(outputPath?: string): string {\n\t\tconst filePath = resolvePath(\n\t\t\toutputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`,\n\t\t\tprocess.cwd(),\n\t\t);\n\t\tconst dir = dirname(filePath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tconst header: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tversion: CURRENT_SESSION_VERSION,\n\t\t\tid: this.sessionManager.getSessionId(),\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: this.sessionManager.getCwd(),\n\t\t};\n\n\t\tconst branchEntries = this.sessionManager.getBranch();\n\t\tconst lines = [JSON.stringify(header)];\n\n\t\t// Re-chain parentIds to form a linear sequence\n\t\tlet prevId: string | null = null;\n\t\tfor (const entry of branchEntries) {\n\t\t\tconst linear = { ...entry, parentId: prevId };\n\t\t\tlines.push(JSON.stringify(linear));\n\t\t\tprevId = entry.id;\n\t\t}\n\n\t\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\t\treturn filePath;\n\t}\n\n\t// =========================================================================\n\t// Utilities\n\t// =========================================================================\n\n\t/**\n\t * Get text content of last assistant message.\n\t * Useful for /copy command.\n\t * @returns Text content, or undefined if no assistant message exists\n\t */\n\tgetLastAssistantText(): string | undefined {\n\t\tlet lastAssistant: AssistantMessage | undefined;\n\t\tfor (let index = this.messages.length - 1; index >= 0; index -= 1) {\n\t\t\tconst message = this.messages[index];\n\t\t\tif (message?.role !== \"assistant\") continue;\n\t\t\tconst assistant = message as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (assistant.stopReason === \"aborted\" && assistant.content.length === 0) continue;\n\t\t\tlastAssistant = assistant;\n\t\t\tbreak;\n\t\t}\n\n\t\tif (!lastAssistant) return undefined;\n\n\t\tlet text = \"\";\n\t\tfor (const content of lastAssistant.content) {\n\t\t\tif (content.type === \"text\") {\n\t\t\t\ttext += content.text;\n\t\t\t}\n\t\t}\n\n\t\treturn text.trim() || undefined;\n\t}\n\n\t// =========================================================================\n\t// Extension System\n\t// =========================================================================\n\n\tcreateReplacedSessionContext(): ReplacedSessionContext {\n\t\tconst context = Object.defineProperties(\n\t\t\t{},\n\t\t\tObject.getOwnPropertyDescriptors(this._extensionRunner.createCommandContext()),\n\t\t) as ReplacedSessionContext;\n\t\tcontext.sendMessage = (message, options) => this.sendCustomMessage(message, options);\n\t\tcontext.sendUserMessage = (content, options) => this.sendUserMessage(content, options);\n\t\treturn context;\n\t}\n\n\t/**\n\t * Check if extensions have handlers for a specific event type.\n\t */\n\thasExtensionHandlers(eventType: string): boolean {\n\t\treturn this._extensionRunner.hasHandlers(eventType);\n\t}\n\n\t/**\n\t * Get the extension runner (for setting UI context and error handlers).\n\t */\n\tget extensionRunner(): ExtensionRunner {\n\t\treturn this._extensionRunner;\n\t}\n}\n\nfunction parsePositiveIntegerEnv(name: string): number | undefined {\n\tconst raw = process.env[name];\n\tif (raw === undefined || raw.trim() === \"\") {\n\t\treturn undefined;\n\t}\n\tconst value = Number.parseInt(raw, 10);\n\treturn Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction parsePositiveFloatEnv(name: string): number | undefined {\n\tconst raw = process.env[name];\n\tif (raw === undefined || raw.trim() === \"\") {\n\t\treturn undefined;\n\t}\n\tconst value = Number.parseFloat(raw);\n\treturn Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction parseTokenizerModeEnv(\n\tvalue: string | undefined,\n): NonNullable<BuildSystemPromptOptions[\"contextBudget\"]>[\"tokenizerMode\"] {\n\tswitch (value) {\n\t\tcase \"fallback\":\n\t\tcase \"openai-js\":\n\t\tcase \"openai-wasm\":\n\t\tcase \"auto\":\n\t\t\treturn value;\n\t\tdefault:\n\t\t\treturn \"fallback\";\n\t}\n}\n\nfunction parseCommaSeparatedEnv(value: string | undefined): string[] {\n\tif (value === undefined || value.trim() === \"\") {\n\t\treturn [];\n\t}\n\treturn Array.from(\n\t\tnew Set(\n\t\t\tvalue\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((item) => item.trim())\n\t\t\t\t.filter((item) => item.length > 0),\n\t\t),\n\t);\n}\n"]}
|