Files
reverse-engineering-anything/src/server/McpToolRequest.ts
T
gensokyo-dev f72638f6b1 feat(investigation)!: retain records across engine cleanup (#8361)
Separate engine cleanup, investigation reset and terminal disposal in the shared application owner. Keep stable Evidence IDs and full Unknown revision history through engine close and target changes; serialize reset with all record writers and cleanup owners, optionally acknowledging a complete checkpoint before clearing. Bound retained representation without eviction.

Validation: 43 focused files / 533 distinct tests, source/type/lint/schema/documentation checks, nine genuine source cases across eight families, real Ghidra session identity and cleanup, and nine actual local Codex CLI terra/high runs with independent full-result and checkpoint audits. See docs/verification/investigation-lifecycle.md for measurements, partial attempts and verification limits.

Fixes #8347.

BREAKING CHANGE: close_binary retains investigation records. Use reset_investigation to clear them. Cross-process recovery requires an explicitly acknowledged Evidence bundle checkpoint.

Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
2026-10-31 21:34:45 +00:00

167 lines
5.2 KiB
TypeScript

import { AsyncLocalStorage } from "node:async_hooks";
import type { RequestId } from "@modelcontextprotocol/server";
import type { z } from "zod";
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../domain/result.js";
const currentRequest = new AsyncLocalStorage<McpToolRequest>();
/** One SDK request owns validation, cancellation and its publication boundary. */
export class McpToolRequest {
readonly cancellation = new AbortController();
validation?: {
readonly issues: readonly z.core.$ZodIssue[];
readonly input: unknown;
};
#handlerStarted = false;
#finishHandler: () => void = () => undefined;
readonly #handlerDone = new Promise<void>((resolve) => {
this.#finishHandler = resolve;
});
#publicationStarted = false;
#finishPublication: () => void = () => undefined;
readonly #publicationDone = new Promise<void>((resolve) => {
this.#finishPublication = resolve;
});
readonly #admissions = new Map<
BinarySessionPort,
Promise<Result<AnalysisOperationPort, AnalysisError>>
>();
constructor(
readonly id: RequestId,
readonly tool: string,
) {}
beginHandler(): void {
this.#handlerStarted = true;
}
finishHandler(): void {
this.#finishHandler();
}
beginPublication(): void {
this.#publicationStarted = true;
}
finishPublication(): void {
this.#finishPublication();
}
/** A suppressed SDK reply releases admission after the handler's cleanup. */
cancel(reason?: unknown): void {
this.cancellation.abort(reason);
if (!this.#publicationStarted) this.#finishPublication();
}
get holdsAdmission(): boolean {
return this.#admissions.size > 0;
}
/**
* Return the application's result to the SDK while retaining its original
* session admission through publication. Later work on this same request
* reuses that binding, including when reset has already queued behind it.
*/
admit<Value>(
session: BinarySessionPort,
operationName: string,
signal: AbortSignal | undefined,
operation: (analysis: AnalysisOperationPort) => Promise<Value>,
): Promise<Result<Value, AnalysisError>> {
const existing = this.#admissions.get(session);
if (existing !== undefined)
return existing.then(async (binding) => {
if (!binding.ok) return binding;
const cancellation =
signal?.aborted === true ? signal : this.cancellation.signal;
if (cancellation.aborted)
return err(
new AnalysisCancelledError(operationName, {
cause: cancellation.reason,
}),
);
return ok(await operation(binding.value));
});
let resolveBinding!: (
value: Result<AnalysisOperationPort, AnalysisError>,
) => void;
let rejectBinding!: (cause: unknown) => void;
const binding = new Promise<Result<AnalysisOperationPort, AnalysisError>>(
(resolve, reject) => {
resolveBinding = resolve;
rejectBinding = reject;
},
);
// The first operation does not consume binding; a thrown pre-admission
// failure still has an owner even when there is no second operation.
void binding.catch(() => undefined);
this.#admissions.set(session, binding);
let resolveResult!: (value: Result<Value, AnalysisError>) => void;
let rejectResult!: (cause: unknown) => void;
const result = new Promise<Result<Value, AnalysisError>>(
(resolve, reject) => {
resolveResult = resolve;
rejectResult = reject;
},
);
const failed = (cause: unknown): void => {
this.#admissions.delete(session);
rejectBinding(cause);
rejectResult(cause);
};
let held: Promise<Result<Value, AnalysisError>>;
try {
held = session.withAdmittedAnalysis(
operationName,
signal === undefined
? this.cancellation.signal
: AbortSignal.any([signal, this.cancellation.signal]),
async (analysis) => {
resolveBinding(ok(analysis));
try {
const value = await operation(analysis);
resolveResult(ok(value));
await this.#settlePublication();
return value;
} catch (cause: unknown) {
rejectResult(cause);
await this.#settlePublication();
throw cause;
}
},
);
} catch (cause: unknown) {
failed(cause);
return result;
}
void held.then((completed) => {
this.#admissions.delete(session);
if (!completed.ok) {
resolveBinding(completed);
resolveResult(completed);
}
}, failed);
return result;
}
async #settlePublication(): Promise<void> {
if (this.#handlerStarted) await this.#handlerDone;
await this.#publicationDone;
}
}
export const currentMcpToolRequest = (): McpToolRequest | undefined =>
currentRequest.getStore();
export const runMcpToolRequest = <Value>(
request: McpToolRequest,
operation: () => Value,
): Value => currentRequest.run(request, operation);