Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
663aa06
fix(server): t3_thread_list clamps an oversized limit instead of failing
MarcoDotIO Oct 30, 2026
e762b2f
test(server): record the t3_thread_list limit description in the agen…
MarcoDotIO Oct 30, 2026
4e3d6a6
fix(server): keep the t3_thread_list clamp without growing the agent …
MarcoDotIO Oct 30, 2026
9530bb7
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Oct 31, 2026
68e4ee1
Merge remote-tracking branch 'github/main' into takeover-27814
maria-rcks Oct 31, 2026
fa013b5
test(server): remove redundant thread-list clamp comment
maria-rcks Oct 31, 2026
57604f0
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Oct 31, 2026
d1b1636
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 1, 2026
769b79e
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 1, 2026
6f563b4
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 1, 2026
98949c1
chore: merge main into fix/mcp-hunt
maria-rcks Nov 4, 2026
dc10400
Merge branch 'main' of https://github.com/pingdotgg/t3code into fix/m…
MarcoDotIO Nov 6, 2026
0440a32
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 8, 2026
62942a3
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 9, 2026
d1dde08
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 9, 2026
dc52714
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 10, 2026
f7125d3
Merge upstream/main into fix/mcp-hunt
MarcoDotIO Nov 10, 2026
702308c
Merge upstream main into fix/mcp-hunt
MarcoDotIO Nov 11, 2026
e8f0c5a
Merge main into fix/mcp-hunt
MarcoDotIO Nov 13, 2026
3ad3ffe
Merge main into fix/mcp-hunt
MarcoDotIO Nov 13, 2026
a61c89b
Merge main into fix/mcp-hunt
MarcoDotIO Nov 14, 2026
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
28 changes: 28 additions & 0 deletions apps/server/src/mcp/McpHttpServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const callSnapshot = (args: Record<string, unknown>) => callTool("preview_snapsh
const layerRecordingTools = (calls: Array<unknown>) => {
const RecordingToolkit = Toolkit.make(
OrchestratorToolkit.tools.delegate_task,
OrchestratorToolkit.tools.t3_thread_list,
OrchestratorToolkit.tools.t3_thread_read,
OrchestratorToolkit.tools.t3_thread_wait,
ProjectToolkit.tools.t3_project_update,
Expand All @@ -158,6 +159,7 @@ const layerRecordingTools = (calls: Array<unknown>) => {
RecordingToolkit,
McpToolAccess.toLayer(RecordingToolkit, {
delegate_task: record,
t3_thread_list: record,
t3_thread_read: record,
t3_thread_wait: record,
t3_project_update: record,
Expand Down Expand Up @@ -463,6 +465,32 @@ it.effect.each(["limit", "runLimit", "maxCharsPerItem"])(
},
);

it.effect("clamps a t3_thread_list limit above its maximum instead of rejecting the call", () => {
const calls: Array<unknown> = [];
return Effect.gen(function* () {
// Codex never shows the model this bound, so an over-large page must still list.
yield* callTool("t3_thread_list", { allProjects: true, limit: 200 });
yield* callTool("t3_thread_list", { allProjects: true, limit: 7 });

expect(calls).toEqual([
{ allProjects: true, limit: 100 },
{ allProjects: true, limit: 7 },
]);
}).pipe(Effect.provide(layerRecordingTools(calls)));
});

it.effect("still rejects a t3_thread_list limit below its minimum", () => {
const calls: Array<unknown> = [];
return Effect.gen(function* () {
const rejected = yield* callTool("t3_thread_list", { allProjects: true, limit: 0 }).pipe(
Effect.flip,
);

expect(String(rejected)).toContain("greater than or equal to 1");
expect(calls).toEqual([]);
}).pipe(Effect.provide(layerRecordingTools(calls)));
});

it("keeps cross-tool routing out of the delegate_task description", () => {
const description = OrchestratorToolkit.tools.delegate_task.description ?? "";
expect(description).toContain("Start one child agent with only `task`");
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5135,
"codexAdditionalContextBytes": 6865,
"mcpToolListJsonBytes": 46007
"mcpToolListJsonBytes": 46032
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
32 changes: 17 additions & 15 deletions packages/contracts/src/orchestratorMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,20 @@ export const OrchestratorMcpEnvironmentTarget = Schema.optional(
}),
);

// Codex hides `minimum` and `maximum` from the model, so a value above the maximum clamps on
// decode instead of failing the call; pagination returns the rest. Values below 1 still fail:
// 0 asks for nothing, and answering it with one item would be a different request.
const clampedPositiveInt = (maximum: number) =>
PositiveInt.pipe(
Schema.decodeTo(
PositiveInt.check(Schema.isLessThanOrEqualTo(maximum)),
SchemaTransformation.transform({
decode: (value) => Math.min(value, maximum),
encode: (value) => value,
}),
),
);

export const OrchestratorMcpThreadListInput = Schema.Struct({
environmentId: OrchestratorMcpEnvironmentTarget,
projectId: OrchestratorMcpProjectTarget,
Expand All @@ -339,7 +353,9 @@ export const OrchestratorMcpThreadListInput = Schema.Struct({
snoozed: Schema.optional(Schema.Boolean),
includeSubagents: Schema.optional(Schema.Boolean),
cursor: Schema.optional(NonNegativeInt),
limit: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(100))),
limit: Schema.optional(clampedPositiveInt(100)).annotate({
description: "Max threads, 1 to 100.",
}),
});
export type OrchestratorMcpThreadListInput = typeof OrchestratorMcpThreadListInput.Type;

Expand Down Expand Up @@ -389,20 +405,6 @@ export const OrchestratorMcpThreadListResult = Schema.Struct({
});
export type OrchestratorMcpThreadListResult = typeof OrchestratorMcpThreadListResult.Type;

// Codex hides `minimum` and `maximum` from the model, so a value above the maximum clamps on
// decode instead of failing the call; pagination returns the rest. Values below 1 still fail:
// 0 asks for nothing, and answering it with one item would be a different request.
const clampedPositiveInt = (maximum: number) =>
PositiveInt.pipe(
Schema.decodeTo(
PositiveInt.check(Schema.isLessThanOrEqualTo(maximum)),
SchemaTransformation.transform({
decode: (value) => Math.min(value, maximum),
encode: (value) => value,
}),
),
);

export const OrchestratorMcpThreadReadInput = Schema.Struct({
environmentId: OrchestratorMcpEnvironmentTarget,
threadId: ThreadId,
Expand Down
Loading