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:
ZacharyZhang-NY committed 2026-04-18 23:37:37 -04:00
1 parent e0e43d6127
commit f041336721
33 files changed
+143 -481

No files matched your search

+2
View File
@@ -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);
+3 -2
View File
@@ -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,
+3 -2
View File
@@ -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,
-9
View File
@@ -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,
-2
View File
@@ -148,5 +148,3 @@ export function createCheckpointEngine(deps: CheckpointDeps) {
},
};
}
export type CheckpointEngineService = ReturnType<typeof createCheckpointEngine>;
-2
View File
@@ -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;
+4 -4
View File
@@ -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,
+83
View File
@@ -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");
}
},
},
];
}
+2
View File
@@ -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) {
+3 -2
View File
@@ -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,
+3 -2
View File
@@ -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
View File
@@ -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;
+6 -23
View File
@@ -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}`;
}
-67
View File
@@ -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)}`,
);
}
}
-2
View File
@@ -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);
+8 -19
View File
@@ -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);
+4 -58
View File
@@ -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;
};
}