feat(mcp): add opt-in paging for retained unknown heads

Keep complete default delivery and exact filters. Select stable ID pages at the ledger owner before cloning, return filtered totals and continuation offsets, and preserve detached records through the shared session port.

Validate public MCP paging, safe-integer edges, snapshot replay and converted JSON Schemas. Document the bounded Flameox workflow comparison and its token/CPU/latency measurement limits; no real model performance claim.
This commit is contained in:
morluto 2026-10-13 08:08:24 +08:00
parent 34a4bb6508
commit 9a18251426
13 changed files with 311 additions and 18 deletions

View File

@ -78,6 +78,12 @@ unknown metadata. Missing omission counts remain null. Summary output carries
every matching record; actual transport admission still applies. Omitted
`detail` preserves complete canonical bundle delivery; filters require summary.
`list_unknowns` returns every matching current head by default. For a smaller
inline response, supply `offset` and `limit`, then follow `next_offset` until it
is null. `total` counts all matching heads; each page preserves complete records
in stable unknown ID order. Filters apply before paging. Pages reflect current
session state, so keep the unknown ledger unchanged while traversing it.
For a portable exported bundle, the CLI shares this projection:
`rea inspect-evidence-bundle '{"path":"/tmp/evidence.json","detail":"summary","filters":{"operation":"analyze_function"}}' --format json`.
The CLI validates and materializes that local bundle under its existing memory

View File

@ -0,0 +1,60 @@
# Selected unknown retrieval
`list_unknowns` now accepts optional paging. Omitted limits still return all
matching heads. Selection happens before payload cloning in the ledger owner;
MCP returns complete selected records and the total matching count.
## Bounded workflow comparison
The task is to retrieve the first ten complete current unknown heads, in stable
ID order, from a session containing 200 heads. The complete-list workflow reads
all heads and selects ten locally. The selected-page workflow requests
`{"offset":0,"limit":10}`. Both use one real SDK MCP call through
`Client`/`InMemoryTransport`, with the same imported session and no provider.
All six executions returned identical selected records and the expected total.
These are two workflows on the candidate implementation, **not** a historical
before/after binary comparison. The candidate was built from `f09bb8a99` plus
the paging changes. Runtime file-table SHA-256 for the preserved complete-list
copy: `06a457dac90912dceba88f945573897372701f6c6b2e0934d7028023b4646648`.
Node 24.18.0, locked MCP SDK 2.3.1, tokenx 1.3.0, macOS arm64.
Flameox 0.2.9 ran three randomized paired blocks, seed 731, with a 30-second
and 1-GiB limit per execution. Each fresh process performed a first read and
five identical warm reads. The semantic oracle checked the selected answer
SHA-256, `c18d718a71323eb53712040b4b09ffebfc3497000d1ff8f58d57665638077310`.
Every execution and oracle passed.
| Measurement | Complete list | Selected page |
| ----------------------------------------- | ------------: | ------------: |
| Calls / errors per task | 1 / 0 | 1 / 0 |
| Serialized response bytes | 646,855 | 32,603 |
| Estimated request tokens | 12 | 21 |
| Estimated response tokens | 151,097 | 7,647 |
| Estimated full catalog tokens | 764,512 | 764,512 |
| Median first-read elapsed milliseconds | 1,084.05 | 1,082.65 |
| Median warm-read elapsed milliseconds | 3.943 | 0.426 |
| Median first-read CPU milliseconds | 1,333.23 | 1,328.47 |
| Median warm-read CPU milliseconds | 5.012 | 0.591 |
| Median whole-process elapsed milliseconds | 1,752.40 | 1,664.42 |
| Median peak process-family MiB | 598.59 | 554.09 |
Read timings include the MCP call and answer validation. First-read timings
exclude startup, imports, session preparation, handshake and catalog discovery;
the whole-process elapsed measurement includes those, all six reads, token
estimation and cleanup. Whole-process CPU was not measured. Peak RSS includes
preparation and imports; this workload has no child processes. Host caches were
not flushed. Three pairs and fifteen warm reads per workflow describe this
fixture only; they do not establish broad speed or memory improvements.
Token counts estimate serialized request, response and catalog JSON, including
both MCP response representations. They are not measured model tokens. Host
schema injection, prompts, reasoning, retries and actual agent behavior are
unmeasured. The full catalog cost is unchanged between these workflows and can
dominate a first task. Smaller selected delivery does not remove that cost.
Native Flameox evidence was preserved locally as
`14ea4c0ef6595a1b89645299d4e184a582d5e975f7916294d68c1f1b312608d0`,
analysis `f66e125179623591ce00b1ccf0d917c2e90512de55db1061269c042d65e1125d`.
An earlier 512-MiB capture exceeded its memory limit and is excluded from these
measurements. No production provider or model trial was run.

View File

@ -19,6 +19,10 @@ import type {
UnknownStatus,
UpdateUnknownInput,
} from "../../domain/residualUnknown.js";
import type {
UnknownPage,
UnknownQuery,
} from "../investigation/InvestigationRecordPort.js";
import { err, ok, type Result } from "../../domain/result.js";
import type {
AnalysisExecution,
@ -350,6 +354,10 @@ export abstract class BinarySessionRecords {
return this.#records.listUnknowns(filters);
}
listUnknownsPage(query: UnknownQuery = {}): UnknownPage {
return this.#records.listUnknownsPage(query);
}
verifyUnknownResolution(unknownId: string): Result<
{
readonly valid: boolean;

View File

@ -26,6 +26,7 @@ import {
} from "../../domain/residualUnknown.js";
import { err, ok, type Result } from "../../domain/result.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import type { UnknownPage, UnknownQuery } from "./InvestigationRecordPort.js";
type EvidenceLedgerFailure = EvidenceIntegrityError;
type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>;
@ -326,18 +327,39 @@ export class EvidenceLedger {
readonly domain?: string;
} = {},
): ResidualUnknown[] {
return [...this.#unknownHeads.values()]
return this.listUnknownsPage(filters).items;
}
/** Select a stable page of current heads before detaching record payloads. */
listUnknownsPage(query: UnknownQuery = {}): UnknownPage {
const offset = query.offset ?? 0;
const matching = [...this.#unknownHeads.values()]
.filter(
(unknown) =>
(filters.status === undefined || unknown.status === filters.status) &&
(filters.severity === undefined ||
unknown.severity === filters.severity) &&
(filters.domain === undefined || unknown.domain === filters.domain),
(query.status === undefined || unknown.status === query.status) &&
(query.severity === undefined ||
unknown.severity === query.severity) &&
(query.domain === undefined || unknown.domain === query.domain),
)
.sort((left, right) =>
compareUnicodeCodePoints(left.unknown_id, right.unknown_id),
)
.map((unknown) => structuredClone(unknown));
);
const start = Math.min(offset, matching.length);
const end =
query.limit === undefined
? undefined
: start + Math.min(query.limit, matching.length - start);
const page = matching.slice(start, end);
const nextOffset =
offset <= matching.length && start + page.length < matching.length
? start + page.length
: null;
return {
items: page.map((unknown) => structuredClone(unknown)),
total: matching.length,
offset,
next_offset: nextOffset,
};
}
/** Return current resolution validity; imported invalid states are rejected. */

View File

@ -38,6 +38,21 @@ export interface EvidenceUnknownWriter {
): Result<ResidualUnknown | null, AnalysisError>;
}
export type UnknownPage = {
readonly items: ResidualUnknown[];
readonly total: number;
readonly offset: number;
readonly next_offset: number | null;
};
export interface UnknownQuery {
readonly status?: UnknownStatus;
readonly severity?: ResidualUnknown["severity"];
readonly domain?: string;
readonly offset?: number;
readonly limit?: number;
}
/** Optimistic Unknown revisions and consistency verification. */
export interface UnknownRegistryPort {
recordUnknown(
@ -51,6 +66,7 @@ export interface UnknownRegistryPort {
readonly severity?: ResidualUnknown["severity"];
readonly domain?: string;
}): ResidualUnknown[];
listUnknownsPage(query?: UnknownQuery): UnknownPage;
verifyUnknownResolution(unknownId: string): Result<
{
readonly valid: boolean;

View File

@ -19,6 +19,8 @@ import type {
EvidenceWriter,
EvidenceUnknownWriter,
UnknownRegistryPort,
UnknownPage,
UnknownQuery,
} from "./InvestigationRecordPort.js";
/** One investigation's validated record owner, independent of binary execution. */
@ -124,6 +126,10 @@ export class InvestigationRecords
return this.#evidence.listUnknowns(filters);
}
listUnknownsPage(query: UnknownQuery = {}): UnknownPage {
return this.#evidence.listUnknownsPage(query);
}
/** Verify registry consistency without asserting the answer is true. */
verifyUnknownResolution(unknownId: string): Result<
{

View File

@ -139,7 +139,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"list_unknowns",
"List every current residual-unknown head in deterministic ID order, with optional exact status, severity, and domain filters. Results are complete and inline. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
"List current residual-unknown heads in deterministic ID order, with optional exact status, severity, and domain filters. Results are complete inline by default; supply offset and limit to page the filtered order. total is the matching-head count and next_offset is null when the page exhausts the result. This is read-only; unresolved, contradicted, and non-truth dispositions remain distinct.",
listUnknownsInputSchema,
),
session(

View File

@ -52,6 +52,22 @@ export const listUnknownsInputSchema = z.strictObject({
.optional(),
severity: z.enum(["low", "medium", "high", "critical"]).optional(),
domain: z.string().trim().min(1).optional(),
offset: z
.number()
.int()
.min(0)
.max(Number.MAX_SAFE_INTEGER)
.default(0)
.describe("Zero-based offset in the stable filtered unknown ID order."),
limit: z
.number()
.int()
.positive()
.max(Number.MAX_SAFE_INTEGER)
.optional()
.describe(
"Optional maximum number of heads to return. Omit to return every matching head from offset.",
),
});
/** Exact residual-unknown identity to revalidate. */

View File

@ -555,6 +555,8 @@ export const sessionOutputSchemas = {
z.object({
items: z.array(residualUnknownSchema),
total: z.number().int().min(0),
offset: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
),
record_unknown: lifecycleResultOf(residualUnknownSchema),

View File

@ -2,7 +2,7 @@ import type { JsonValue } from "../domain/jsonValue.js";
/** Exact example inputs for residual-unknown lifecycle contracts. */
export const UNKNOWN_CONTRACT_EXAMPLES = {
list_unknowns: {},
list_unknowns: { offset: 0, limit: 50 },
record_unknown: {
question: "Does this branch require an unavailable external service?",
severity: "medium",

View File

@ -151,18 +151,14 @@ const registerListUnknownsTool = ({
listContract.name,
toolRegistrationOptions(listContract),
(input) => {
const all = session.listUnknowns({
const page = session.listUnknownsPage({
...(input.status === undefined ? {} : { status: input.status }),
...(input.severity === undefined ? {} : { severity: input.severity }),
...(input.domain === undefined ? {} : { domain: input.domain }),
offset: input.offset,
...(input.limit === undefined ? {} : { limit: input.limit }),
});
return server.delivery.toCallToolResult(
ok({
items: all,
total: all.length,
}),
listContract,
);
return server.delivery.toCallToolResult(ok(page), listContract);
},
);
};

View File

@ -4,6 +4,7 @@ import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, expect, it } from "vitest";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import type {
AnalysisClient,
AnalysisProvider,
@ -249,3 +250,158 @@ it("records capture disagreement as a contradicted unknown", async () => {
),
).toEqual(beforeRepeat);
});
it("pages filtered unknown heads through the public MCP contract", async () => {
const session = createTestBinarySession(providerWithCapabilities([]));
const server = createServer({ kind: "session", session });
const client = new Client({ name: "unknown-pagination", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
const created = [
["matching c", "high", "pagination"],
["matching a", "high", "pagination"],
["matching b", "high", "pagination"],
["filtered severity", "medium", "pagination"],
["filtered domain", "high", "other"],
] as const;
for (const [question, severity, domain] of created) {
const result = session.recordUnknown({
question,
severity,
domain,
required_authority: "analyst-inference",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
});
expect(result.ok).toBe(true);
}
const defaultPage = structured(
await client.callTool({ name: "list_unknowns", arguments: {} }),
).result as {
items: { unknown_id: string }[];
total: number;
offset: number;
next_offset: number | null;
};
expect(defaultPage.items.map(({ unknown_id }) => unknown_id)).toEqual(
defaultPage.items.map(({ unknown_id }) => unknown_id).sort(),
);
expect(defaultPage).toMatchObject({ total: 5, offset: 0, next_offset: null });
const detached = session.listUnknownsPage({ limit: 1 });
const detachedHead = detached.items[0];
if (detachedHead === undefined) throw new Error("Expected one unknown head");
const originalQuestion = detachedHead.question;
Reflect.set(detachedHead, "question", "changed outside the ledger");
expect(
session
.listUnknowns()
.find(({ unknown_id }) => unknown_id === detachedHead.unknown_id)
?.question,
).toBe(originalQuestion);
const offsetWithoutLimit = structured(
await client.callTool({
name: "list_unknowns",
arguments: { domain: "pagination", offset: 1 },
}),
).result as typeof defaultPage;
expect(offsetWithoutLimit).toMatchObject({
total: 4,
offset: 1,
next_offset: null,
});
expect(offsetWithoutLimit.items).toHaveLength(3);
const noMatches = structured(
await client.callTool({
name: "list_unknowns",
arguments: { domain: "no-such-domain", limit: 2 },
}),
).result as typeof defaultPage;
expect(noMatches).toMatchObject({
items: [],
total: 0,
offset: 0,
next_offset: null,
});
const beyondEnd = structured(
await client.callTool({
name: "list_unknowns",
arguments: { domain: "pagination", offset: 100, limit: 2 },
}),
).result as typeof defaultPage;
expect(beyondEnd).toMatchObject({
total: 4,
offset: 100,
next_offset: null,
});
expect(beyondEnd.items).toEqual([]);
const maximumSafePage = structured(
await client.callTool({
name: "list_unknowns",
arguments: {
offset: Number.MAX_SAFE_INTEGER,
limit: Number.MAX_SAFE_INTEGER,
},
}),
).result as typeof defaultPage;
expect(maximumSafePage).toMatchObject({
total: 5,
offset: Number.MAX_SAFE_INTEGER,
next_offset: null,
});
expect(maximumSafePage.items).toEqual([]);
const request = {
name: "list_unknowns",
arguments: { domain: "pagination", severity: "high", limit: 2 },
} as const;
const first = structured(await client.callTool(request))
.result as typeof defaultPage;
expect(first).toMatchObject({ total: 3, offset: 0, next_offset: 2 });
expect(first.items).toHaveLength(2);
const second = structured(
await client.callTool({
...request,
arguments: { ...request.arguments, offset: first.next_offset ?? 0 },
}),
).result as typeof defaultPage;
expect(second).toMatchObject({ total: 3, offset: 2, next_offset: null });
expect(second.items).toHaveLength(1);
expect(
[...first.items, ...second.items].map(({ unknown_id }) => unknown_id),
).toEqual(
session
.listUnknowns({ domain: "pagination", severity: "high" })
.map(({ unknown_id }) => unknown_id),
);
expect(
toolContract("list_unknowns").outputSchema.safeParse(
(await client.callTool(request)).structuredContent,
).success,
).toBe(true);
for (const arguments_ of [
{ limit: 0 },
{ offset: -1 },
{ offset: Number.MAX_SAFE_INTEGER + 1 },
{ unexpected: true },
]) {
expect(
(await client.callTool({ name: "list_unknowns", arguments: arguments_ }))
.isError,
).toBe(true);
}
});

View File

@ -412,7 +412,12 @@ describe("MCP investigation history snapshot replay", () => {
toolContract("list_unknowns").outputSchema.parse(
listed.structuredContent,
).result,
).toEqual({ items: [resolved], total: 1 });
).toEqual({
items: [resolved],
total: 1,
offset: 0,
next_offset: null,
});
},
);
});