From f0413367219d89b0f1354cf5d4168a39059b4d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 18 Apr 2026 23:37:37 -0400 Subject: [PATCH] refactor: nuke dead exports, dead files, dead DB column, dead zero-value wrappers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full-codebase ts-prune + manual audit sweep. Every change below is a pure deletion (or a 2-line re-wire where a dead function was supposed to be alive). No scope additions. Dead wrappers / phantom features - shouldAutoApproveApproval: zero-value wrapper over shouldAutoApprove. Four call sites now import shouldAutoApprove from execution-policy directly. - evaluateRotation(session, connectorId, thresholdsOverride): the connectorId arg was never read (underscored), and thresholdsOverride was only exercised by the test suite — no production caller. Signature collapses to evaluateRotation(session). DEFAULT_THRESHOLDS / RotationThresholds exports and their tests go with it. - installLogRedactor: defensive console wrapper that was never wired from worker-entry. buildRedactor handles the known threat (env-value leaks); the pattern-based redactTokens stays for tests and future composition. Dead files - resume-interrupted-tasks.ts: helper function never imported. - service-graph-orchestration-deps.ts: three factory closures superseded by the inline closures in create-orchestration-service. Dead exports - listConnectorsByAuthKind (shared/connector-catalog) - getLocalePromptInstruction + whole localization.ts file - ImportantDocType - main-side ConnectorAuthKind (duplicated the shared one; not referenced) - CheckpointEngineService, RunHousekeepingService types - WorkspaceResolution type - cmdOrCtrlSymbol - AgentEventPush, CredentialPayload type aliases - parseApprovalPayloadJson, parseEnvValue, ProviderCard - getSOP — buildSOPSection reads the SOPS map directly; the individual getter had no callers. Deduped to a flat array. - renderWakeReason in wake-reasons/en.ts: byte-for-byte duplicate of the one in wake-reasons/index.ts. Forgotten wiring (not cleanup, but adjacent) - registerAgentStandupRoute: the GET /api/agents/me/standup endpoint is advertised in every system prompt and wake-reason doc, but the route handler was never registered in api-server/index.ts. Agents have been hitting 404 since Pi Mono. Registered. Dead DB column - v44 migration drops runs.context_snapshot_json. Column was added by v16-v25, never written or read, and v43's deliberate preservation was an interim measure. Same idempotent table-rebuild pattern as v41/v43. Test tree - Phantom-feature threshold override tests removed (3). The rest of the rotation suite re-expressed against the real maxRuns=20 floor. 495/495 unit tests pass. typecheck + build clean. --- src/main/api-server/index.ts | 2 + src/main/api-server/routes-agents.ts | 5 +- src/main/api-server/routes-approvals.ts | 5 +- src/main/approval-policy.ts | 9 -- src/main/checkpoint-engine.ts | 2 - src/main/connector-definitions.ts | 2 - src/main/database/migrations/v43-v43.ts | 8 +- src/main/database/migrations/v44-v44.ts | 83 +++++++++++++++++ src/main/database/schema.ts | 2 + src/main/ipc-core-agent-graph-handlers.ts | 5 +- src/main/ipc-core-operations-handlers.ts | 5 +- src/main/orchestration/types.ts | 6 -- src/main/prompt-builder/sop-section.ts | 29 ++---- src/main/prompt-builder/wake-reasons/en.ts | 14 --- src/main/resume-interrupted-tasks.ts | 67 -------------- src/main/run-housekeeping.ts | 2 - src/main/service-graph-orchestration-deps.ts | 67 -------------- .../create-orchestration-service.ts | 2 +- src/main/session-management-service.ts | 27 ++---- .../worker/handlers/run-finished-helpers.ts | 2 +- src/main/worker/log-redactor.ts | 62 +------------ .../execution/ModelManagementShared.tsx | 45 --------- .../components/execution/execution-utils.tsx | 9 -- src/renderer/lib/platform.ts | 4 - src/shared/connector-catalog.ts | 6 -- src/shared/contracts/schemas-agent-event.ts | 2 - src/shared/contracts/schemas-credentials.ts | 1 - src/shared/document-categories.ts | 2 - src/shared/localization.ts | 23 ----- tests/integration/process-recovery.spec.ts | 15 +-- tests/unit/connector-matching.test.ts | 1 - tests/unit/edge-case-session.test.ts | 19 ++-- tests/unit/session-management.test.ts | 91 ++----------------- 33 files changed, 143 insertions(+), 481 deletions(-) create mode 100644 src/main/database/migrations/v44-v44.ts delete mode 100644 src/main/resume-interrupted-tasks.ts delete mode 100644 src/main/service-graph-orchestration-deps.ts delete mode 100644 src/shared/localization.ts diff --git a/src/main/api-server/index.ts b/src/main/api-server/index.ts index 16aa2044..fef64baf 100644 --- a/src/main/api-server/index.ts +++ b/src/main/api-server/index.ts @@ -1,6 +1,7 @@ import { AgentApiServer, type ApiServerEventCallbacks } from "./server-core"; import { registerAgentRoutes } from "./routes-agents"; import { registerAgentControlRoutes } from "./routes-agent-control"; +import { registerAgentStandupRoute } from "./routes-agents-standup"; import { registerTaskRoutes } from "./routes-tasks"; import { registerWorkGraphRoutes } from "./routes-work-graph"; import { registerMessageRoutes } from "./routes-messages"; @@ -28,6 +29,7 @@ export function createApiServer( const server = new AgentApiServer(db, onDomainChanged, onWakeAgent, callbacks, allowedOrigins); registerAgentRoutes(server); registerAgentControlRoutes(server); + registerAgentStandupRoute(server); registerTaskRoutes(server); registerWorkGraphRoutes(server); registerMessageRoutes(server); diff --git a/src/main/api-server/routes-agents.ts b/src/main/api-server/routes-agents.ts index a3c2557b..659b2530 100644 --- a/src/main/api-server/routes-agents.ts +++ b/src/main/api-server/routes-agents.ts @@ -8,7 +8,8 @@ import { getHireRoutingValidationError, resolveHireReportsTo, } from "../agent-permissions"; -import { resolveApprovalDecision, shouldAutoApproveApproval } from "../approval-policy"; +import { resolveApprovalDecision } from "../approval-policy"; +import { shouldAutoApprove } from "../execution-policy"; import { requireParam, type AgentApiServer } from "./server-core"; export function registerAgentRoutes(server: AgentApiServer) { @@ -365,7 +366,7 @@ export function registerAgentRoutes(server: AgentApiServer) { }); let autoApproved = false; - if (shouldAutoApproveApproval(db, companyId, "hire_agent")) { + if (shouldAutoApprove(db, companyId, "hire_agent")) { resolveApprovalDecision({ db, handleApprovalResolved: callbacks.onApprovalResolved, diff --git a/src/main/api-server/routes-approvals.ts b/src/main/api-server/routes-approvals.ts index ac2f73fc..46f88f59 100644 --- a/src/main/api-server/routes-approvals.ts +++ b/src/main/api-server/routes-approvals.ts @@ -1,7 +1,8 @@ import { z } from "zod"; import log from "electron-log/main.js"; import { approvalInputSchema } from "@shared/contracts"; -import { resolveApprovalDecision, shouldAutoApproveApproval } from "../approval-policy"; +import { resolveApprovalDecision } from "../approval-policy"; +import { shouldAutoApprove } from "../execution-policy"; import { requireParam, type AgentApiServer } from "./server-core"; const approvalListQuerySchema = z.object({ @@ -69,7 +70,7 @@ export function registerApprovalRoutes(server: AgentApiServer) { // Notify thread integration about the new approval callbacks.onApprovalCreated(id, companyId, input.relatedTaskId ?? null, input.type, input.payloadSummary ?? ""); - if (shouldAutoApproveApproval(db, companyId, input.type)) { + if (shouldAutoApprove(db, companyId, input.type)) { resolveApprovalDecision({ db, handleApprovalResolved: callbacks.onApprovalResolved, diff --git a/src/main/approval-policy.ts b/src/main/approval-policy.ts index 6cf11e3e..e85a9b8b 100644 --- a/src/main/approval-policy.ts +++ b/src/main/approval-policy.ts @@ -1,13 +1,4 @@ import type { AppDatabase } from "./database"; -import { shouldAutoApprove } from "./execution-policy"; - -export function shouldAutoApproveApproval( - db: AppDatabase, - companyId: string, - approvalType: string, -): boolean { - return shouldAutoApprove(db, companyId, approvalType); -} export function resolveApprovalDecision({ db, diff --git a/src/main/checkpoint-engine.ts b/src/main/checkpoint-engine.ts index c1af49dc..06153050 100644 --- a/src/main/checkpoint-engine.ts +++ b/src/main/checkpoint-engine.ts @@ -148,5 +148,3 @@ export function createCheckpointEngine(deps: CheckpointDeps) { }, }; } - -export type CheckpointEngineService = ReturnType; diff --git a/src/main/connector-definitions.ts b/src/main/connector-definitions.ts index 224bceb2..9f7e7980 100644 --- a/src/main/connector-definitions.ts +++ b/src/main/connector-definitions.ts @@ -39,8 +39,6 @@ export type PiOAuthProviderId = | "google-gemini-cli" | "google-antigravity"; -export type ConnectorAuthKind = "oauth" | "api_key" | "aws" | "azure" | "gcp_adc"; - export interface ConnectorAuthOAuth { type: "oauth"; oauthId: PiOAuthProviderId; diff --git a/src/main/database/migrations/v43-v43.ts b/src/main/database/migrations/v43-v43.ts index eb26ea5d..737a7f64 100644 --- a/src/main/database/migrations/v43-v43.ts +++ b/src/main/database/migrations/v43-v43.ts @@ -82,10 +82,10 @@ export function createMigrationsV43V43(deps: MigrationDeps): Migration[] { } if (runsHasDeadCol) { - // v16-v25 added `context_snapshot_json`; it is unreferenced by - // production code today but the column exists on real databases - // and carries no policy against preservation. Keep it so this - // migration's blast radius stays exactly one column. + // `context_snapshot_json` rides through this rebuild untouched + // so the single-concern invariant holds: v43 drops only + // `session_display_id`. v44 is the one that drops the snapshot + // column. db.exec(` CREATE TABLE runs_new ( id TEXT PRIMARY KEY, diff --git a/src/main/database/migrations/v44-v44.ts b/src/main/database/migrations/v44-v44.ts new file mode 100644 index 00000000..e53ca384 --- /dev/null +++ b/src/main/database/migrations/v44-v44.ts @@ -0,0 +1,83 @@ +import log from "electron-log/main.js"; +import type { Migration, MigrationDeps } from "./types"; +import { columnExists, tableExists } from "./types"; + +/** + * Drop `runs.context_snapshot_json` — dead since its introduction. + * + * v16-v25 added the column with the intent to snapshot the turn context + * for post-run forensics. Nothing ever wrote to it, nothing ever read it, + * and the Pi Mono migration removed the last plausible use case (the + * turn snapshot now lives in `task_sessions` / `session_entries`). + * + * Same table-rebuild pattern as v43 — `runs_new` without the column, + * copy the real data, swap, reindex. Idempotent: if the column is + * already absent the migration is a no-op. + */ +export function createMigrationsV44V44(deps: MigrationDeps): Migration[] { + const { db } = deps; + return [ + { + version: 44, + name: "drop_runs_context_snapshot_json", + fn: () => { + if (!tableExists(db, "runs") || !columnExists(db, "runs", "context_snapshot_json")) { + log.info("[migration v44] runs.context_snapshot_json already absent; skipping"); + return; + } + + db.exec("PRAGMA foreign_keys = OFF"); + try { + db.exec(` + CREATE TABLE runs_new ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + task_id TEXT, + agent_id TEXT NOT NULL, + workspace_id TEXT, + connector_id TEXT NOT NULL, + status TEXT NOT NULL, + summary TEXT NOT NULL, + error_message TEXT, + exit_code INTEGER, + signal TEXT, + model TEXT, + cost_usd REAL, + unattributed_cost INTEGER NOT NULL DEFAULT 0, + actor_type TEXT, + actor_id TEXT, + started_at TEXT, + finished_at TEXT, + log_path TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + INSERT INTO runs_new ( + id, company_id, task_id, agent_id, workspace_id, connector_id, + status, summary, error_message, exit_code, signal, model, + cost_usd, unattributed_cost, actor_type, actor_id, + started_at, finished_at, log_path, created_at, updated_at + ) + SELECT + id, company_id, task_id, agent_id, workspace_id, connector_id, + status, summary, error_message, exit_code, signal, model, + cost_usd, unattributed_cost, actor_type, actor_id, + started_at, finished_at, log_path, created_at, updated_at + FROM runs; + DROP TABLE runs; + ALTER TABLE runs_new RENAME TO runs; + CREATE INDEX IF NOT EXISTS idx_runs_company ON runs(company_id); + CREATE INDEX IF NOT EXISTS idx_runs_task ON runs(task_id); + CREATE INDEX IF NOT EXISTS idx_runs_agent ON runs(agent_id); + CREATE INDEX IF NOT EXISTS idx_runs_company_status ON runs(company_id, status); + CREATE INDEX IF NOT EXISTS idx_runs_company_created_at ON runs(company_id, created_at DESC); + CREATE INDEX IF NOT EXISTS idx_runs_agent_created_at ON runs(agent_id, created_at DESC); + `); + log.info("[migration v44] rebuilt runs without context_snapshot_json"); + } finally { + db.exec("PRAGMA foreign_keys = ON"); + } + }, + }, + ]; +} diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 96b9b365..44971d76 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -8,6 +8,7 @@ import { createMigrationsV40V40 } from "./migrations/v40-v40"; import { createMigrationsV41V41 } from "./migrations/v41-v41"; import { createMigrationsV42V42 } from "./migrations/v42-v42"; import { createMigrationsV43V43 } from "./migrations/v43-v43"; +import { createMigrationsV44V44 } from "./migrations/v44-v44"; import { columnExists, nowIso, @@ -182,6 +183,7 @@ export function runMigrations( ...createMigrationsV41V41(deps), ...createMigrationsV42V42(deps), ...createMigrationsV43V43(deps), + ...createMigrationsV44V44(deps), ]; for (const migration of migrations) { diff --git a/src/main/ipc-core-agent-graph-handlers.ts b/src/main/ipc-core-agent-graph-handlers.ts index 245e9813..47dac543 100644 --- a/src/main/ipc-core-agent-graph-handlers.ts +++ b/src/main/ipc-core-agent-graph-handlers.ts @@ -6,7 +6,8 @@ import { workspaceInputSchema, } from "@shared/contracts"; import { getConnectorExecutionReadinessIssue } from "@shared/connector-policy"; -import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-policy"; +import { resolveApprovalDecision } from "./approval-policy"; +import { shouldAutoApprove } from "./execution-policy"; import { getHireNameValidationError, getHireRoutingValidationError, resolveHireReportsTo } from "./agent-permissions"; import { validateConnectorModelSelection } from "./connector-model-policy"; import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common"; @@ -246,7 +247,7 @@ export function registerCoreAgentGraphHandlers({ }); let autoApproved = false; - if (shouldAutoApproveApproval(db, parsed.companyId, "hire_agent")) { + if (shouldAutoApprove(db, parsed.companyId, "hire_agent")) { resolveApprovalDecision({ db, handleApprovalResolved, diff --git a/src/main/ipc-core-operations-handlers.ts b/src/main/ipc-core-operations-handlers.ts index fabc3cf7..9e7b3f47 100644 --- a/src/main/ipc-core-operations-handlers.ts +++ b/src/main/ipc-core-operations-handlers.ts @@ -10,7 +10,8 @@ import { startRunSchema, triggerHeartbeatSchema, } from "@shared/contracts"; -import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-policy"; +import { resolveApprovalDecision } from "./approval-policy"; +import { shouldAutoApprove } from "./execution-policy"; import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common"; export function registerCoreOperationHandlers({ @@ -71,7 +72,7 @@ export function registerCoreOperationHandlers({ // Notify thread integration about the new approval handleApprovalCreated(id, parsed.companyId, parsed.relatedTaskId ?? null, parsed.type, parsed.payloadSummary ?? ""); - if (shouldAutoApproveApproval(db, parsed.companyId, parsed.type)) { + if (shouldAutoApprove(db, parsed.companyId, parsed.type)) { resolveApprovalDecision({ db, handleApprovalResolved, diff --git a/src/main/orchestration/types.ts b/src/main/orchestration/types.ts index e4a21a7d..3e2a63c9 100644 --- a/src/main/orchestration/types.ts +++ b/src/main/orchestration/types.ts @@ -6,12 +6,6 @@ import type { ProjectedSkill } from "../skill-types"; export type WorkspaceLockMode = "exclusive" | "shared" | "none"; -export interface WorkspaceResolution { - workspace: ReturnType; - lockMode: WorkspaceLockMode; - source: "task" | "agent" | "project" | "company_fallback" | "none"; -} - export interface AutomationDispatch { companyId: string; taskId?: string; diff --git a/src/main/prompt-builder/sop-section.ts b/src/main/prompt-builder/sop-section.ts index 109dc22c..cd241a9e 100644 --- a/src/main/prompt-builder/sop-section.ts +++ b/src/main/prompt-builder/sop-section.ts @@ -14,32 +14,15 @@ import sop013En from "./sops/sop-013-incident.en.md?raw"; import sop014En from "./sops/sop-014-hiring-pipeline.en.md?raw"; import sop015En from "./sops/sop-015-knowledge.en.md?raw"; -export type SOPId = - | "sop-001" | "sop-002" | "sop-003" | "sop-004" | "sop-005" - | "sop-006" | "sop-007" | "sop-008" | "sop-009" | "sop-010" - | "sop-011" | "sop-012" | "sop-013" | "sop-014" | "sop-015"; - -const SOPS: Record = { - "sop-001": sop001En, "sop-002": sop002En, "sop-003": sop003En, - "sop-004": sop004En, "sop-005": sop005En, "sop-006": sop006En, - "sop-007": sop007En, "sop-008": sop008En, "sop-009": sop009En, - "sop-010": sop010En, "sop-011": sop011En, "sop-012": sop012En, - "sop-013": sop013En, "sop-014": sop014En, "sop-015": sop015En, -}; +const SOPS_IN_ORDER: string[] = [ + sop001En, sop002En, sop003En, sop004En, sop005En, + sop006En, sop007En, sop008En, sop009En, sop010En, + sop011En, sop012En, sop013En, sop014En, sop015En, +]; const SOP_HEADING = "# Standard Operating Procedures"; -const SOP_IDS: SOPId[] = [ - "sop-001", "sop-002", "sop-003", "sop-004", "sop-005", - "sop-006", "sop-007", "sop-008", "sop-009", "sop-010", - "sop-011", "sop-012", "sop-013", "sop-014", "sop-015", -]; - -export function getSOP(id: SOPId): string { - return SOPS[id].trimEnd(); -} - export function buildSOPSection(): string { - const body = SOP_IDS.map((id) => SOPS[id].trimEnd()).join("\n\n"); + const body = SOPS_IN_ORDER.map((text) => text.trimEnd()).join("\n\n"); return `${SOP_HEADING}\n\n${body}\n`; } diff --git a/src/main/prompt-builder/wake-reasons/en.ts b/src/main/prompt-builder/wake-reasons/en.ts index a6b9b1c4..1e0103fb 100644 --- a/src/main/prompt-builder/wake-reasons/en.ts +++ b/src/main/prompt-builder/wake-reasons/en.ts @@ -132,17 +132,3 @@ export const WAKE_REASONS: Record = { ], }, }; - -export function renderWakeReason( - reason: string, - vars: WakeReasonVars, -): string { - const entry = WAKE_REASONS[reason as WakeReason]; - if (!entry) { - return `Trigger: ${reason}\nAction required: Check your standup briefing and messages for context on why you were woken.`; - } - const steps = entry.steps(vars) - .map((step, i) => `${i + 1}. ${step}`) - .join("\n"); - return `${entry.title}\nAction required:\n${steps}`; -} diff --git a/src/main/resume-interrupted-tasks.ts b/src/main/resume-interrupted-tasks.ts deleted file mode 100644 index 029a9efe..00000000 --- a/src/main/resume-interrupted-tasks.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type log from "electron-log/main.js"; -import type { AppDatabase } from "./database"; - -interface ResumeInterruptedTasksDeps { - db: AppDatabase; - logger: typeof log; - wakeAgentIfPossible: (agentId: string, companyId: string, trigger: string) => string | null; - publishDomainChanged: () => void; -} - -/** - * Resume tasks that were left behind by a clean shutdown. - * - * The worker-service marks running/queued runs as "interrupted" with a fixed - * summary string on shutdown, but does not reset the owning task. This helper - * rolls those tasks back to "todo", releases their run locks, and re-wakes the - * assigned agents so heartbeat can pick them up again. - */ -export function resumeInterruptedTasks({ - db, - logger, - wakeAgentIfPossible, - publishDomainChanged, -}: ResumeInterruptedTasksDeps): void { - try { - const snapshot = db.listSnapshot(); - const shutdownRuns = snapshot.runs.filter( - (r) => - r.status === "interrupted" && - (r.summary === "Application shut down." || r.summary === "Worker process crashed."), - ); - if (shutdownRuns.length === 0) return; - - const resumedAgents = new Set(); - for (const run of shutdownRuns) { - try { - if (run.taskId) { - const task = db.getTask(run.taskId); - if (task.status === "in_progress" || task.status === "blocked") { - db.advanceTaskStatus(run.taskId, "todo", "Auto-resumed after app restart."); - db.releaseTaskLock(run.taskId, run.id); - } - } - if (!resumedAgents.has(run.agentId)) { - resumedAgents.add(run.agentId); - wakeAgentIfPossible(run.agentId, run.companyId, "app_restart_resume"); - } - } catch (error) { - logger.warn( - `[bootstrap] Skipped shutdown recovery for run ${run.id} (task/agent may have been deleted):`, - error, - ); - } - } - - if (resumedAgents.size > 0) { - logger.info( - `[bootstrap] Resumed ${shutdownRuns.length} interrupted task(s) across ${resumedAgents.size} agent(s) after clean shutdown recovery.`, - ); - publishDomainChanged(); - } - } catch (err) { - logger.warn( - `[bootstrap] Shutdown recovery failed: ${err instanceof Error ? err.message : String(err)}`, - ); - } -} diff --git a/src/main/run-housekeeping.ts b/src/main/run-housekeeping.ts index 40e4df63..fe73ce56 100644 --- a/src/main/run-housekeeping.ts +++ b/src/main/run-housekeeping.ts @@ -136,5 +136,3 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous }, }; } - -export type RunHousekeepingService = ReturnType; diff --git a/src/main/service-graph-orchestration-deps.ts b/src/main/service-graph-orchestration-deps.ts deleted file mode 100644 index ed46097f..00000000 --- a/src/main/service-graph-orchestration-deps.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Helpers for assembling orchestration-service dependencies in service-graph.ts. - * Extracted to keep service-graph.ts under the 400-line cap. - */ - -import type { AppDatabase } from "./database"; -import { createSkillService } from "./skill-service"; -import type { createSessionManagementService } from "./session-management-service"; -import type { ProjectedSkill } from "./skill-types"; - -interface Logger { - info: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; -} - -export function makeListRuntimeSkillEntries(deps: { db: AppDatabase; logger: Logger }) { - let cache: ReturnType | null = null; - return (companyId: string, agentId: string, connectorId: string): ProjectedSkill[] => { - try { - if (!cache) cache = createSkillService({ db: deps.db, profileDir: deps.db.profileDir, logger: deps.logger }); - return cache.listRuntimeSkillEntries(companyId, agentId, connectorId); - } catch (error) { - deps.logger.warn(`[service-graph] listRuntimeSkillEntries failed for agent ${agentId}:`, error); - return []; - } - }; -} - -export function makeSessionHandoffLookup( - deps: { logger: Logger }, - getSessionService: () => ReturnType, -) { - return (agentId: string): string | null => { - try { - return getSessionService().getHandoffForAgent(agentId); - } catch (error) { - deps.logger.warn(`[service-graph] getSessionHandoff failed for agent ${agentId}:`, error); - return null; - } - }; -} - -export function makePreRunRotationEvaluator( - deps: { db: AppDatabase; logger: Logger }, - getSessionService: () => ReturnType, -) { - return (agentId: string, connectorId: string): { rotated: boolean; handoff: string | null; sessionId: string | null } => { - try { - const sessionSvc = getSessionService(); - const agent = deps.db.getAgent(agentId); - if (!agent.sessionId) return { rotated: false, handoff: null, sessionId: null }; - const session = deps.db.getSessionById(agent.sessionId); - if (!session) return { rotated: false, handoff: null, sessionId: agent.sessionId }; - const result = sessionSvc.evaluateRotation(session, connectorId); - if (result.shouldRotate) { - const newSessionId = sessionSvc.rotateSession(session); - const handoff = sessionSvc.getHandoffForAgent(agentId); - return { rotated: true, handoff, sessionId: newSessionId }; - } - return { rotated: false, handoff: null, sessionId: agent.sessionId }; - } catch (error) { - deps.logger.warn(`[service-graph] evaluatePreRunSessionRotation failed for agent ${agentId}:`, error); - return { rotated: false, handoff: null, sessionId: null }; - } - }; -} diff --git a/src/main/service-graph/create-orchestration-service.ts b/src/main/service-graph/create-orchestration-service.ts index 419532ae..5e5b518c 100644 --- a/src/main/service-graph/create-orchestration-service.ts +++ b/src/main/service-graph/create-orchestration-service.ts @@ -97,7 +97,7 @@ export function buildOrchestrationService({ if (!agent.sessionId) return { rotated: false, handoff: null, sessionId: null }; const session = db.getSessionById(agent.sessionId); if (!session) return { rotated: false, handoff: null, sessionId: agent.sessionId }; - const result = sessionService.evaluateRotation(session, connectorId); + const result = sessionService.evaluateRotation(session); if (result.shouldRotate) { const newSessionId = sessionService.rotateSession(session); const handoff = sessionService.getHandoffForAgent(agentId); diff --git a/src/main/session-management-service.ts b/src/main/session-management-service.ts index 13b02165..45e24af9 100644 --- a/src/main/session-management-service.ts +++ b/src/main/session-management-service.ts @@ -6,16 +6,13 @@ interface SessionManagementDependencies { logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void }; } -// D-01/D-02: Exported for testing and external override. Conservative defaults. -export const DEFAULT_THRESHOLDS = { +// Rotation thresholds. Any-of semantics — first trigger wins. +const ROTATION_THRESHOLDS = { maxRuns: 20, maxTokens: 500_000, maxAgeHours: 72, }; -// D-01: Type for per-adapter or company-level threshold overrides -export type RotationThresholds = Partial; - // D-04: Handoff size cap to prevent prompt bloat. // 2000 characters ≈ 500 tokens — enough to resume context without crowding // out the system prompt + user message in an 8k-ish model window. @@ -31,32 +28,24 @@ export function createSessionManagementService({ db, logger }: SessionManagement db.incrementSessionUsage(sessionId, inputTokens, outputTokens, runId); } - // D-01: Any-of thresholds with optional overrides. D-04: Check nativeContextManagement. - // Per D-01: "Configurable per-adapter with company-level overrides" -- accepts optional - // thresholds parameter that merges with defaults. Callers can pass per-adapter or - // company-level overrides; omitted fields fall back to DEFAULT_THRESHOLDS. + // Any-of thresholds — first trigger wins. Pi-mono delegates context + // management to pi-agent-core compaction hooks; AgentCompany applies its + // own rotation thresholds on top of that. function evaluateRotation( session: TaskSessionRecord, - _connectorId: string, - thresholdsOverride?: RotationThresholds, ): { shouldRotate: boolean; reason: string | null } { - // Pi-mono delegates context management to pi-agent-core compaction hooks; - // AgentCompany always applies its own rotation thresholds. - const thresholds = { ...DEFAULT_THRESHOLDS, ...thresholdsOverride }; - - // D-01: Any-of thresholds - first trigger wins - if (session.runCount >= thresholds.maxRuns) { + if (session.runCount >= ROTATION_THRESHOLDS.maxRuns) { return { shouldRotate: true, reason: "max_runs" }; } const totalTokens = session.totalInputTokens + session.totalOutputTokens; - if (totalTokens >= thresholds.maxTokens) { + if (totalTokens >= ROTATION_THRESHOLDS.maxTokens) { return { shouldRotate: true, reason: "max_tokens" }; } const ageMs = Date.now() - new Date(session.createdAt).getTime(); const ageHours = ageMs / (1000 * 60 * 60); - if (ageHours >= thresholds.maxAgeHours) { + if (ageHours >= ROTATION_THRESHOLDS.maxAgeHours) { return { shouldRotate: true, reason: "max_age_hours" }; } diff --git a/src/main/worker/handlers/run-finished-helpers.ts b/src/main/worker/handlers/run-finished-helpers.ts index 01d1ed00..e37b8065 100644 --- a/src/main/worker/handlers/run-finished-helpers.ts +++ b/src/main/worker/handlers/run-finished-helpers.ts @@ -116,7 +116,7 @@ export function accumulateAndRotateSession( event.outputTokens ?? 0, event.runId, ); - const rotationResult = sessionService.evaluateRotation(session, run.connectorId); + const rotationResult = sessionService.evaluateRotation(session); if (rotationResult.shouldRotate) { // Re-read to get updated counts after accumulation. const updatedSession = db.getSessionById(agentRecord.sessionId); diff --git a/src/main/worker/log-redactor.ts b/src/main/worker/log-redactor.ts index 979d1b0c..f08dd298 100644 --- a/src/main/worker/log-redactor.ts +++ b/src/main/worker/log-redactor.ts @@ -1,13 +1,9 @@ /** - * Defensive token redactor for worker process logs. + * Pattern-based token redactor. * - * Pi-mono's HTTP clients are not known to log credentials, but stack traces - * from upstream libraries (or future pi versions) could include header - * fragments containing `Authorization: Bearer ...` or `?access_token=...`. - * To prevent any such fragment from reaching the parent's stderr stream, - * `installLogRedactor()` wraps `console.error`, `console.warn`, `console.log`, - * `process.stdout.write`, and `process.stderr.write` so that every string - * argument is filtered through `redactTokens()` before transmission. + * Complements `buildRedactor` (env-value redaction) by scrubbing common + * credential shapes out of arbitrary strings before they reach the parent + * process — Bearer tokens, provider API keys, OAuth query params. */ const PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [ @@ -29,53 +25,3 @@ export function redactTokens(text: string): string { } return result; } - -function redactArg(arg: unknown): unknown { - if (typeof arg === "string") return redactTokens(arg); - if (arg instanceof Error) { - const redacted = new Error(redactTokens(arg.message)); - redacted.name = arg.name; - if (arg.stack) redacted.stack = redactTokens(arg.stack); - return redacted; - } - return arg; -} - -interface InstallTarget { - console: { log: typeof console.log; warn: typeof console.warn; error: typeof console.error; info: typeof console.info }; - stdoutWrite: typeof process.stdout.write; - stderrWrite: typeof process.stderr.write; -} - -export function installLogRedactor(target?: Partial): () => void { - const cons = target?.console ?? console; - const origLog = cons.log.bind(cons); - const origWarn = cons.warn.bind(cons); - const origError = cons.error.bind(cons); - const origInfo = cons.info.bind(cons); - - cons.log = (...args: unknown[]) => origLog(...args.map(redactArg)); - cons.warn = (...args: unknown[]) => origWarn(...args.map(redactArg)); - cons.error = (...args: unknown[]) => origError(...args.map(redactArg)); - cons.info = (...args: unknown[]) => origInfo(...args.map(redactArg)); - - const stdoutWriteOrig = (target?.stdoutWrite ?? process.stdout.write).bind(process.stdout); - const stderrWriteOrig = (target?.stderrWrite ?? process.stderr.write).bind(process.stderr); - process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => { - if (typeof chunk === "string") return stdoutWriteOrig(redactTokens(chunk), ...rest as never[]); - return stdoutWriteOrig(chunk as never, ...rest as never[]); - }) as typeof process.stdout.write; - process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => { - if (typeof chunk === "string") return stderrWriteOrig(redactTokens(chunk), ...rest as never[]); - return stderrWriteOrig(chunk as never, ...rest as never[]); - }) as typeof process.stderr.write; - - return () => { - cons.log = origLog; - cons.warn = origWarn; - cons.error = origError; - cons.info = origInfo; - process.stdout.write = stdoutWriteOrig; - process.stderr.write = stderrWriteOrig; - }; -} diff --git a/src/renderer/components/execution/ModelManagementShared.tsx b/src/renderer/components/execution/ModelManagementShared.tsx index 229c19fe..2a28a37f 100644 --- a/src/renderer/components/execution/ModelManagementShared.tsx +++ b/src/renderer/components/execution/ModelManagementShared.tsx @@ -1,4 +1,3 @@ -import { CaretDown, CaretRight } from "../ui/icons"; import type { ProfileSnapshot } from "@shared/types"; import { Input } from "../ui"; import { useT } from "../../i18n"; @@ -17,11 +16,6 @@ export interface ModelEntry { export type ConnectorStatus = ProfileSnapshot["connectors"][number]["status"]; -export function parseEnvValue(text: string, key: string): string { - const line = text.split(/\r?\n/).find((l) => l.startsWith(`${key}=`)); - return line ? line.slice(key.length + 1) : ""; -} - /* ------------------------------------------------------------------ */ /* StatusBadge */ /* ------------------------------------------------------------------ */ @@ -68,45 +62,6 @@ export function StatusBadge({ status }: { status: ConnectorStatus }) { ); } -/* ------------------------------------------------------------------ */ -/* ProviderCard */ -/* ------------------------------------------------------------------ */ - -export function ProviderCard({ - title, - badge, - expanded, - onToggle, - children, -}: { - title: string; - badge: React.ReactNode; - expanded: boolean; - onToggle: () => void; - children: React.ReactNode; -}) { - return ( -
- - {expanded &&
{children}
} -
- ); -} - /* ------------------------------------------------------------------ */ /* Toggle switch */ /* ------------------------------------------------------------------ */ diff --git a/src/renderer/components/execution/execution-utils.tsx b/src/renderer/components/execution/execution-utils.tsx index daddd5f1..8ca800bd 100644 --- a/src/renderer/components/execution/execution-utils.tsx +++ b/src/renderer/components/execution/execution-utils.tsx @@ -1,20 +1,11 @@ -import type { ApprovalRecord } from "@shared/types" import { Robot, CheckSquare, Play, ShieldCheck, Gear, FolderOpen, Key, Target, Briefcase, Buildings, } from "../ui/icons" -import { safeJson } from "../../lib/safe-json" export function describeActionError(error: unknown) { return error instanceof Error ? error.message : "The action could not be completed." } -export function parseApprovalPayloadJson(approval: ApprovalRecord): Record | null { - if (!approval.payloadJson) return null - const parsed = safeJson(approval.payloadJson, null) - if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record - return null -} - export const ENTITY_ICONS: Record = { agent: Robot, task: CheckSquare, run: Play, approval: ShieldCheck, connector: Gear, workspace: FolderOpen, secret: Key, goal: Target, project: Briefcase, company: Buildings, diff --git a/src/renderer/lib/platform.ts b/src/renderer/lib/platform.ts index cf0b4ceb..03006962 100644 --- a/src/renderer/lib/platform.ts +++ b/src/renderer/lib/platform.ts @@ -5,7 +5,3 @@ export function isMac(): boolean { export function cmdOrCtrlLabel(): string { return isMac() ? "Cmd" : "Ctrl"; } - -export function cmdOrCtrlSymbol(): string { - return isMac() ? "⌘" : "Ctrl+"; -} diff --git a/src/shared/connector-catalog.ts b/src/shared/connector-catalog.ts index 1dc32cab..87683c76 100644 --- a/src/shared/connector-catalog.ts +++ b/src/shared/connector-catalog.ts @@ -286,9 +286,3 @@ export function findConnectorCatalogEntry(id: string): ConnectorCatalogEntry | u export function listFeaturedOnboardingConnectors(): ConnectorCatalogEntry[] { return CONNECTOR_CATALOG.filter((entry) => entry.onboardingFeatured === true); } - -export function listConnectorsByAuthKind( - kind: ConnectorAuthKind, -): ConnectorCatalogEntry[] { - return CONNECTOR_CATALOG.filter((entry) => entry.auth.type === kind); -} diff --git a/src/shared/contracts/schemas-agent-event.ts b/src/shared/contracts/schemas-agent-event.ts index 4dcf991f..424e7042 100644 --- a/src/shared/contracts/schemas-agent-event.ts +++ b/src/shared/contracts/schemas-agent-event.ts @@ -17,5 +17,3 @@ export const agentEventPushSchema = z.object({ type: z.string(), }).passthrough(), }); - -export type AgentEventPush = z.infer; diff --git a/src/shared/contracts/schemas-credentials.ts b/src/shared/contracts/schemas-credentials.ts index daa90338..eb91e49f 100644 --- a/src/shared/contracts/schemas-credentials.ts +++ b/src/shared/contracts/schemas-credentials.ts @@ -81,7 +81,6 @@ export const credentialSummarySchema = z.object({ }); export type CredentialSummary = z.infer; -export type CredentialPayload = z.infer; export type SaveCredentialInput = z.infer; export type AwsCredential = z.infer; export type AzureCredential = z.infer; diff --git a/src/shared/document-categories.ts b/src/shared/document-categories.ts index 8fb83230..8b812a34 100644 --- a/src/shared/document-categories.ts +++ b/src/shared/document-categories.ts @@ -17,6 +17,4 @@ export const IMPORTANT_DOC_TYPES = [ "sop", ] as const; -export type ImportantDocType = (typeof IMPORTANT_DOC_TYPES)[number]; - export const IMPORTANT_DOC_TYPE_SET: ReadonlySet = new Set(IMPORTANT_DOC_TYPES); diff --git a/src/shared/localization.ts b/src/shared/localization.ts deleted file mode 100644 index 28c59c4d..00000000 --- a/src/shared/localization.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AppLocale } from "./locale"; - -/** - * Returns a locale-appropriate system prompt instruction for AI agents. - * When locale is "zh", the instruction tells the agent to respond entirely in Chinese. - * When locale is "en", the instruction tells the agent to respond entirely in English. - */ -export function getLocalePromptInstruction(locale: AppLocale): string { - if (locale === "zh") { - return [ - "重要语言要求:你必须全程使用中文进行所有沟通。", - "这包括:任务描述、评论、消息、报告、文档、审批、通知 — 所有输出必须是中文。", - "即使系统提示和 API 文档是英文的,你的所有回复和创建的内容必须是中文。", - "不要使用英文回复,除非是代码、变量名、API 端点等技术术语。", - ].join("\n"); - } - return [ - "Important language requirement: You MUST communicate entirely in English.", - "This includes: task descriptions, comments, messages, reports, documents, approvals, notifications — ALL output must be in English.", - "Even if some existing content or context is in another language, all your responses and created content must be in English.", - "Do not respond in other languages, except for code, variable names, API endpoints, and other technical terms.", - ].join("\n"); -} diff --git a/tests/integration/process-recovery.spec.ts b/tests/integration/process-recovery.spec.ts index c4059bcf..5e0a5f68 100644 --- a/tests/integration/process-recovery.spec.ts +++ b/tests/integration/process-recovery.spec.ts @@ -446,25 +446,20 @@ describe("Process Recovery -- Context Preservation (D-02)", () => { adapterType: "gemini-cli", }); - // Add token usage to simulate real work - db.incrementSessionUsage(sessionId, 5000, 2000, runId); + // Drive the session past the maxRuns=20 threshold so rotation fires. + for (let i = 0; i < 20; i++) { + db.incrementSessionUsage(sessionId, 1000, 500, `${runId}-${i}`); + } - // Create session management service to test rotation const sessionService = createSessionManagementService({ db, logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, }); - // Get the session and evaluate rotation with maxRuns:1 override - // (so it triggers rotation since we did 1 run) const session = db.getSessionById(sessionId); expect(session).not.toBeNull(); - const rotationResult = sessionService.evaluateRotation( - session!, - "gemini-cli", - { maxRuns: 1 }, - ); + const rotationResult = sessionService.evaluateRotation(session!); expect(rotationResult.shouldRotate).toBe(true); expect(rotationResult.reason).toBe("max_runs"); diff --git a/tests/unit/connector-matching.test.ts b/tests/unit/connector-matching.test.ts index 229bea1f..86a24477 100644 --- a/tests/unit/connector-matching.test.ts +++ b/tests/unit/connector-matching.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { inferTaskType, getConnectorStrengths, - ROLE_DEPARTMENT_HINTS as _ROLE_DEPARTMENT_HINTS, } from "../../src/main/connector-matching"; describe("connector-matching", () => { diff --git a/tests/unit/edge-case-session.test.ts b/tests/unit/edge-case-session.test.ts index a0944dea..a4ef9bad 100644 --- a/tests/unit/edge-case-session.test.ts +++ b/tests/unit/edge-case-session.test.ts @@ -13,7 +13,7 @@ vi.mock("electron", () => ({ })); import { AppDatabase } from "../../src/main/database"; -import { createSessionManagementService, DEFAULT_THRESHOLDS } from "../../src/main/session-management-service"; +import { createSessionManagementService } from "../../src/main/session-management-service"; import type { ConnectorId } from "@shared/types"; // --------------------------------------------------------------------------- @@ -121,17 +121,17 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { }); // Simulate runs until we reach the maxRuns threshold (20) - for (let i = 0; i < DEFAULT_THRESHOLDS.maxRuns; i++) { + for (let i = 0; i < 20; i++) { db.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); } // Get the updated session from DB const session = db.getSessionById(sessionId); expect(session).not.toBeNull(); - expect(session!.runCount).toBe(DEFAULT_THRESHOLDS.maxRuns); + expect(session!.runCount).toBe(20); // matches maxRuns threshold in session-management-service // Evaluate rotation -- should trigger - const result = sessionService.evaluateRotation(session!, "gemini-cli"); + const result = sessionService.evaluateRotation(session!); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -146,8 +146,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { adapterType: "gemini-cli", }); - // Accumulate tokens past the threshold in one big call - // DEFAULT_THRESHOLDS.maxTokens = 500_000 (total input + output) + // Accumulate tokens past the 500_000 maxTokens threshold in one big call db.incrementSessionUsage(sessionId, 300000, 250000, "big-run"); const session = db.getSessionById(sessionId); @@ -155,7 +154,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const totalTokens = session!.totalInputTokens + session!.totalOutputTokens; expect(totalTokens).toBe(550000); - const result = sessionService.evaluateRotation(session!, "gemini-cli"); + const result = sessionService.evaluateRotation(session!); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -171,7 +170,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { }); // Accumulate enough to trigger rotation - for (let i = 0; i < DEFAULT_THRESHOLDS.maxRuns; i++) { + for (let i = 0; i < 20; i++) { db.incrementSessionUsage(sessionId, 1000, 500, `run-${i}`); } @@ -266,7 +265,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const session = db.getSessionById(sessionId)!; // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. - const result = sessionService.evaluateRotation(session, "claude-pro"); + const result = sessionService.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).not.toBeNull(); }); @@ -288,7 +287,7 @@ describe("Edge Case: Session Rotation During Execution (D-05.3)", () => { const session = db.getSessionById(sessionId)!; // Pi-mono delegates compaction to pi-agent-core; AgentCompany still applies rotation uniformly. - const result = sessionService.evaluateRotation(session, "openai-codex"); + const result = sessionService.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).not.toBeNull(); }); diff --git a/tests/unit/session-management.test.ts b/tests/unit/session-management.test.ts index 8fa1b0f4..f5fd9012 100644 --- a/tests/unit/session-management.test.ts +++ b/tests/unit/session-management.test.ts @@ -14,7 +14,7 @@ vi.mock("electron", () => ({ import { AppDatabase } from "../../src/main/database"; import { getConnectorDefinition } from "../../src/main/connectors"; -import { createSessionManagementService, DEFAULT_THRESHOLDS } from "../../src/main/session-management-service"; +import { createSessionManagementService } from "../../src/main/session-management-service"; import type { ConnectorId, TaskSessionRecord } from "@shared/types"; // --------------------------------------------------------------------------- @@ -291,7 +291,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini-cli"); + const result = svc.evaluateRotation(session); expect(result.shouldRotate).toBe(false); expect(result.reason).toBeNull(); }); @@ -314,7 +314,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini-cli"); + const result = svc.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_runs"); }); @@ -337,7 +337,7 @@ describe("session management service", () => { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "gemini-cli"); + const result = svc.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_tokens"); }); @@ -361,7 +361,7 @@ describe("session management service", () => { createdAt: oldDate, updatedAt: oldDate, }; - const result = svc.evaluateRotation(session, "gemini-cli"); + const result = svc.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).toBe("max_age_hours"); }); @@ -389,91 +389,12 @@ describe("session management service", () => { createdAt: new Date(Date.now() - 200 * 60 * 60 * 1000).toISOString(), // Way old updatedAt: new Date().toISOString(), }; - const result = svc.evaluateRotation(session, "claude-pro"); + const result = svc.evaluateRotation(session); expect(result.shouldRotate).toBe(true); expect(result.reason).not.toBeNull(); }); }); - // ------------------------------------------------------------------------- - // configurable thresholds (D-01) - // ------------------------------------------------------------------------- - describe("configurable thresholds (D-01)", () => { - it("evaluateRotation with custom thresholds override { maxRuns: 5 } triggers at 5 runs", () => { - const svc = createSessionManagementService({ db, logger: mockLogger }); - const session: TaskSessionRecord = { - id: "sess-1", - companyId: "co-1", - agentId: "agent-1", - adapterType: "gemini-cli", - lastRunId: null, - lastError: null, - runCount: 5, - totalInputTokens: 100, - totalOutputTokens: 100, - rotatedAt: null, - rotationReason: null, - handoffMarkdown: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - const result = svc.evaluateRotation(session, "gemini-cli", { maxRuns: 5 }); - expect(result.shouldRotate).toBe(true); - expect(result.reason).toBe("max_runs"); - }); - - it("partial override { maxTokens: 100000 } uses override for tokens but defaults for runs and age", () => { - const svc = createSessionManagementService({ db, logger: mockLogger }); - const session: TaskSessionRecord = { - id: "sess-1", - companyId: "co-1", - agentId: "agent-1", - adapterType: "gemini-cli", - lastRunId: null, - lastError: null, - runCount: 10, // Below default 20 - totalInputTokens: 60000, - totalOutputTokens: 40000, // Total 100000 = meets override threshold - rotatedAt: null, - rotationReason: null, - handoffMarkdown: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - const result = svc.evaluateRotation(session, "gemini-cli", { maxTokens: 100000 }); - expect(result.shouldRotate).toBe(true); - expect(result.reason).toBe("max_tokens"); - }); - - it("omitting override uses DEFAULT_THRESHOLDS", () => { - const svc = createSessionManagementService({ db, logger: mockLogger }); - const session: TaskSessionRecord = { - id: "sess-1", - companyId: "co-1", - agentId: "agent-1", - adapterType: "gemini-cli", - lastRunId: null, - lastError: null, - runCount: 19, // Just below default 20 - totalInputTokens: 200000, - totalOutputTokens: 200000, // Total 400000, below default 500000 - rotatedAt: null, - rotationReason: null, - handoffMarkdown: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - const result = svc.evaluateRotation(session, "gemini-cli"); - expect(result.shouldRotate).toBe(false); - expect(result.reason).toBeNull(); - - // Verify DEFAULT_THRESHOLDS values - expect(DEFAULT_THRESHOLDS.maxRuns).toBe(20); - expect(DEFAULT_THRESHOLDS.maxTokens).toBe(500_000); - expect(DEFAULT_THRESHOLDS.maxAgeHours).toBe(72); - }); - }); - // ------------------------------------------------------------------------- // token accumulation // -------------------------------------------------------------------------