refactor: nuke dead exports, dead files, dead DB column, dead zero-value wrappers
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.
This commit is contained in:
1 parent
e0e43d6127
commit
f041336721
33 files changed
+143
-481
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -148,5 +148,3 @@ export function createCheckpointEngine(deps: CheckpointDeps) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type CheckpointEngineService = ReturnType<typeof createCheckpointEngine>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,12 +6,6 @@ import type { ProjectedSkill } from "../skill-types";
|
||||
|
||||
export type WorkspaceLockMode = "exclusive" | "shared" | "none";
|
||||
|
||||
export interface WorkspaceResolution {
|
||||
workspace: ReturnType<AppDatabase["getWorkspace"]>;
|
||||
lockMode: WorkspaceLockMode;
|
||||
source: "task" | "agent" | "project" | "company_fallback" | "none";
|
||||
}
|
||||
|
||||
export interface AutomationDispatch {
|
||||
companyId: string;
|
||||
taskId?: string;
|
||||
|
||||
@@ -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<SOPId, string> = {
|
||||
"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`;
|
||||
}
|
||||
@@ -132,17 +132,3 @@ export const WAKE_REASONS: Record<WakeReason, WakeReasonEntry> = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
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}`;
|
||||
}
|
||||
@@ -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<string>();
|
||||
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)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,5 +136,3 @@ export function createRunHousekeeping({ db, logger, threadIntegration }: RunHous
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type RunHousekeepingService = ReturnType<typeof createRunHousekeeping>;
|
||||
@@ -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<typeof createSkillService> | 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<typeof createSessionManagementService>,
|
||||
) {
|
||||
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<typeof createSessionManagementService>,
|
||||
) {
|
||||
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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof DEFAULT_THRESHOLDS>;
|
||||
|
||||
// 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" };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<InstallTarget>): () => 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;
|
||||
};
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-card">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="flex w-full cursor-pointer items-center justify-between px-4 py-3 transition hover:bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{expanded ? (
|
||||
<CaretDown size={14} className="text-muted-foreground" />
|
||||
) : (
|
||||
<CaretRight size={14} className="text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-[13px] font-semibold text-foreground">{title}</span>
|
||||
</div>
|
||||
{badge}
|
||||
</button>
|
||||
{expanded && <div className="space-y-3 px-4 pb-4">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Toggle switch */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@@ -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<string, unknown> | null {
|
||||
if (!approval.payloadJson) return null
|
||||
const parsed = safeJson<unknown>(approval.payloadJson, null)
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>
|
||||
return null
|
||||
}
|
||||
|
||||
export const ENTITY_ICONS: Record<string, typeof Robot> = {
|
||||
agent: Robot, task: CheckSquare, run: Play, approval: ShieldCheck, connector: Gear,
|
||||
workspace: FolderOpen, secret: Key, goal: Target, project: Briefcase, company: Buildings,
|
||||
|
||||
@@ -5,7 +5,3 @@ export function isMac(): boolean {
|
||||
export function cmdOrCtrlLabel(): string {
|
||||
return isMac() ? "Cmd" : "Ctrl";
|
||||
}
|
||||
|
||||
export function cmdOrCtrlSymbol(): string {
|
||||
return isMac() ? "⌘" : "Ctrl+";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -17,5 +17,3 @@ export const agentEventPushSchema = z.object({
|
||||
type: z.string(),
|
||||
}).passthrough(),
|
||||
});
|
||||
|
||||
export type AgentEventPush = z.infer<typeof agentEventPushSchema>;
|
||||
@@ -81,7 +81,6 @@ export const credentialSummarySchema = z.object({
|
||||
});
|
||||
|
||||
export type CredentialSummary = z.infer<typeof credentialSummarySchema>;
|
||||
export type CredentialPayload = z.infer<typeof credentialPayloadSchema>;
|
||||
export type SaveCredentialInput = z.infer<typeof saveCredentialInputSchema>;
|
||||
export type AwsCredential = z.infer<typeof awsCredentialSchema>;
|
||||
export type AzureCredential = z.infer<typeof azureCredentialSchema>;
|
||||
|
||||
@@ -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<string> = new Set(IMPORTANT_DOC_TYPES);
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user