Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ import { extractRunId, getRunStore } from './run-harness/store';
import {
substituteSecrets,
redactSecrets,
redactSecretString,
MissingSecretError,
getSecretStore,
} from './core/secrets';
import { currentRequestContext } from './observability/request-id';
import type { TransportMessageContext } from './transports';
import { RecoveryTrajectoryLedger, type RecoveryResultStatus } from './recovery';


function redactActVariablesForTelemetry(toolName: string, args: Record<string, unknown>): Record<string, unknown> {
Expand Down Expand Up @@ -335,6 +337,7 @@ export class MCPServer {
private activityTracker: ActivityTracker | null = null;
private operationController: OperationController | null = null;
private hintEngine: HintEngine | null = null;
private recoveryLedger: RecoveryTrajectoryLedger | null = null;
private options: MCPServerOptions;
private profileWarningShown = false;
private exposedTier: ToolTier = 1;
Expand Down Expand Up @@ -438,6 +441,14 @@ export class MCPServer {
this.hintEngine.enableLogging(hintsDir);
this.hintEngine.enableLearning(hintsDir);

// Initialize passive recovery trajectory ledger (#1017). Default-on with the
// existing .openchrome harness logs; set OPENCHROME_RECOVERY_LEDGER=0 to disable.
if (process.env.OPENCHROME_RECOVERY_LEDGER !== '0') {
this.recoveryLedger = new RecoveryTrajectoryLedger({
dirPath: path.join(process.cwd(), '.openchrome', 'recovery'),
});
}

// Initialize task journal
getTaskJournal().init().catch((err: unknown) => {
console.error('[MCPServer] Task journal init failed:', err);
Expand Down Expand Up @@ -1812,6 +1823,8 @@ export class MCPServer {

// End activity tracking (success)
this.activityTracker!.endCall(callId, 'success');
result = redactSecrets(result);
this.recordRecoveryTrajectory(callId, toolName, sessionId, toolArgs, result.isError ? 'no_progress' : 'success', result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact tool result before recording recovery trajectory

recordRecoveryTrajectory is called with the raw result before the existing redactSecrets(result) pass runs, so any tool response that echoes a substituted secret can be persisted to .openchrome/recovery/trajectory.jsonl via observationSummary. This only occurs when a tool result includes literal secret text (for example after ${SECRET:...} substitution), but in that case it creates a disk-level secret leak even though normal MCP responses are redacted.

Useful? React with 👍 / 👎.

getDashboardState().recordToolEnd(callId, 'success');

// Record Prometheus metrics
Expand Down Expand Up @@ -1999,11 +2012,13 @@ export class MCPServer {
return finalResult;
} catch (error) {
const message = formatError(error);
const redactedMessage = redactSecretString(message);
const abortReason = isClientDisconnect(error) ? 'client_disconnect' : null;
const aborted = abortReason !== null;

// End activity tracking (error)
this.activityTracker!.endCall(callId, aborted ? 'aborted' : 'error', message);
this.recordRecoveryTrajectory(callId, toolName, sessionId, toolArgs, aborted ? 'aborted' : 'error', undefined, redactedMessage);
getDashboardState().recordToolEnd(callId, aborted ? 'aborted' : 'error', message);

// Audit log failed invocation — same correlation fields as success path.
Expand Down Expand Up @@ -2430,6 +2445,53 @@ export class MCPServer {
* Get a tool handler by name (for internal server-side plan execution).
* Returns null if the tool is not registered.
*/

private recordRecoveryTrajectory(
callId: string,
toolName: string,
sessionId: string,
toolArgs: Record<string, unknown>,
resultStatus: RecoveryResultStatus,
result?: MCPResult,
error?: string,
): void {
if (!this.recoveryLedger || !this.activityTracker) return;

try {
const recent = this.activityTracker.getRecentCalls(3, sessionId);
const current = recent.find((call) => call.id === callId);
const tabId = typeof toolArgs.tabId === 'string' ? toolArgs.tabId : undefined;
const previousTrajectory = this.recoveryLedger.getLastNode(sessionId, tabId);
const previousFailed =
previousTrajectory?.resultStatus === 'error' ||
previousTrajectory?.resultStatus === 'no_progress' ||
previousTrajectory?.resultStatus === 'aborted';
Comment on lines +2465 to +2468
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude aborted attempts from recovery detection

Recovery detection treats aborted as a prior failure, so a tool call aborted due client disconnect can cause the next successful different tool in the same context to be labeled recovered. That inflates recovery telemetry with non-recovery events (cancellation/network interruptions), making downstream analysis inaccurate; previousFailed should only include actual failure modes such as error/no_progress.

Useful? React with 👍 / 👎.

const recovered =
resultStatus === 'success' &&
previousTrajectory !== undefined &&
previousFailed &&
previousTrajectory.toolName !== toolName;
const progressStatus =
resultStatus === 'error' || resultStatus === 'no_progress' || current?.result === 'error'
? 'stuck'
: 'unknown';

this.recoveryLedger.record({
sessionId,
tabId,
toolName,
args: toolArgs,
resultStatus: recovered ? 'recovered' : resultStatus,
progressStatus,
error,
result,
recoveryTool: recovered ? toolName : undefined,
});
} catch {
// Recovery telemetry is best-effort and must not affect tool behavior.
}
}

getToolHandler(toolName: string): ToolHandler | null {
const registry = this.tools.get(toolName);
return registry ? registry.handler : null;
Expand Down
12 changes: 12 additions & 0 deletions src/recovery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export {
RecoveryTrajectoryLedger,
summarizeArgs,
summarizeResult,
} from './trajectory-ledger';
export type {
RecoveryProgressStatus,
RecoveryResultStatus,
RecoveryTrajectoryLedgerOptions,
RecoveryTrajectoryNode,
RecoveryTrajectoryNodeInput,
} from './trajectory-ledger';
Loading
Loading