Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@
"tailwindcss": "^4.0.0",
"vite-plus": "catalog:"
},
"productName": "T3 Code (Alpha)"
"productName": "T3 Code Baha"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unrelated Baha desktop branding

High Severity

The change to productName renames the desktop app to "T3 Code Baha" and implicitly changes its bundle ID. This alters production packaging, app updates, and macOS passkey entitlements, which seems outside the PR's stated scope.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

}
16 changes: 12 additions & 4 deletions apps/server/src/mcp/McpHttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import packageJson from "../../package.json" with { type: "json" };
import * as McpInvocationContext from "./McpInvocationContext.ts";
import * as McpSessionRegistry from "./McpSessionRegistry.ts";
import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts";
import { DelegationToolkitHandlersLive } from "./toolkits/delegation/handlers.ts";
import { DelegationToolkit } from "./toolkits/delegation/tools.ts";
import {
PreviewSnapshotToolkitHandlersLive,
PreviewStandardToolkitHandlersLive,
Expand Down Expand Up @@ -208,13 +210,19 @@ export const PreviewToolkitRegistrationLive = Layer.mergeAll(
PreviewSnapshotRegistrationLive,
);

// Exposes the `delegate_tasks` tool to every provider session. Requires
// `TaskOrchestrator`, satisfied by the orchestration runtime (RuntimeServicesLive).
const DelegationToolkitRegistrationLive = McpServer.toolkit(DelegationToolkit).pipe(
Layer.provide(DelegationToolkitHandlersLive),
);

const McpTransportLive = McpServer.layerHttp({
name: "T3 Code",
version: packageJson.version,
path: "/mcp",
}).pipe(Layer.provide(McpAuthMiddlewareLive));

export const layer = PreviewToolkitRegistrationLive.pipe(
Layer.provideMerge(McpTransportLive),
Layer.provide(PreviewAutomationBroker.layer),
);
export const layer = Layer.mergeAll(
PreviewToolkitRegistrationLive,
DelegationToolkitRegistrationLive,
).pipe(Layer.provideMerge(McpTransportLive), Layer.provide(PreviewAutomationBroker.layer));
64 changes: 64 additions & 0 deletions apps/server/src/mcp/toolkits/delegation/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { DelegateTaskResult } from "@t3tools/contracts";
import * as Effect from "effect/Effect";

import { TaskOrchestrator } from "../../../orchestration/Services/TaskOrchestrator.ts";
import * as McpInvocationContext from "../../McpInvocationContext.ts";
import { DelegationToolkit } from "./tools.ts";

// Bounded wait kept comfortably under typical MCP client request timeouts so a
// single tool call never times out, even when sub-tasks take minutes. Slow
// sub-tasks come back 'running' and are picked up by collect_delegated_tasks.
const INLINE_WAIT_MS = 30_000;

const handlers = {
delegate_tasks: (input) =>
Effect.gen(function* () {
const scope = yield* McpInvocationContext.McpInvocationContext;
const orchestrator = yield* TaskOrchestrator;
// Depth-1 recursion guard: a thread that was itself spawned as a
// delegated sub-task may not delegate further.
const isChild = yield* orchestrator.isDelegatedChild(scope.threadId);
if (isChild) {
return yield* Effect.fail({
reason: "Delegated sub-tasks cannot delegate further (depth-1 limit).",
});
}
const started = yield* orchestrator.startTasks({
parentThreadId: scope.threadId,
tasks: input.tasks,
maxConcurrency: input.maxConcurrency,
});
const runningIds = started.results
.filter((result) => result.status === "running")
.map((result) => result.threadId);
if (runningIds.length === 0) {
return started;
}
// Wait a bounded time so fast sub-tasks come back inline; the rest stay
// 'running' for collect_delegated_tasks to pick up later.
const collected = yield* orchestrator.collectTasks({
parentThreadId: scope.threadId,
threadIds: runningIds,
waitMs: INLINE_WAIT_MS,
});
const collectedByThread = new Map<string, DelegateTaskResult>(
collected.results.map((result) => [result.threadId, result]),
);
const results = started.results.map((result) =>
result.status === "error" ? result : (collectedByThread.get(result.threadId) ?? result),
);
return { results };
}),
collect_delegated_tasks: (input) =>
Effect.gen(function* () {
const scope = yield* McpInvocationContext.McpInvocationContext;
const orchestrator = yield* TaskOrchestrator;
return yield* orchestrator.collectTasks({
parentThreadId: scope.threadId,
threadIds: input.threadIds,
waitMs: INLINE_WAIT_MS,
});
}),
Comment on lines +52 to +61

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High delegation/handlers.ts:52

collect_delegated_tasks passes input.threadIds directly to orchestrator.collectTasks without validating they belong to the current thread. This lets callers pass any threadId they know (including other users' delegated children) and receive those task results. The parentThreadId parameter only scopes results when threadIds is omitted, so explicit thread IDs bypass the isolation check.

-      return yield* orchestrator.collectTasks({
-        parentThreadId: scope.threadId,
-        threadIds: input.threadIds,
-        waitMs: INLINE_WAIT_MS,
-      });
+      // Omit threadIds to let orchestrator return only this thread's delegated children
+      return yield* orchestrator.collectTasks({
+        parentThreadId: scope.threadId,
+        waitMs: INLINE_WAIT_MS,
+      });
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/mcp/toolkits/delegation/handlers.ts around lines 52-61:

`collect_delegated_tasks` passes `input.threadIds` directly to `orchestrator.collectTasks` without validating they belong to the current thread. This lets callers pass any `threadId` they know (including other users' delegated children) and receive those task results. The `parentThreadId` parameter only scopes results when `threadIds` is omitted, so explicit thread IDs bypass the isolation check.

} satisfies Parameters<typeof DelegationToolkit.toLayer>[0];

export const DelegationToolkitHandlersLive = DelegationToolkit.toLayer(handlers);
59 changes: 59 additions & 0 deletions apps/server/src/mcp/toolkits/delegation/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
CollectDelegatedTasksRequest,
DelegateTasksRequest,
DelegateTasksResult,
} from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { Tool, Toolkit } from "effect/unstable/ai";

import { TaskOrchestrator } from "../../../orchestration/Services/TaskOrchestrator.ts";
import * as McpInvocationContext from "../../McpInvocationContext.ts";

const dependencies = [McpInvocationContext.McpInvocationContext, TaskOrchestrator];

/** Returned only when the call is refused (e.g. a child trying to delegate). */
export const DelegateTasksFailure = Schema.Struct({
reason: Schema.String,
});

export const DelegateTasksTool = Tool.make("delegate_tasks", {
description:
"Delegate sub-tasks to fresh child agents that run in parallel. Returns quickly: each result is " +
"either finished ('completed'/'error') or still 'running' (with its `threadId`). For any result " +
"with status 'running', call `collect_delegated_tasks` afterwards to get its final message — do not " +
"assume a 'running' task failed. " +
"WHEN TO USE — proactively, without being asked: whenever a request breaks into 2+ parts that do " +
"not depend on each other's output (research several files at once, answer multiple questions, " +
"review/summarize different modules, independent edits). Skip it for a single linear task or when " +
"one step's result feeds the next. " +
"CHOOSING THE MODEL per sub-task — set `modelHint`: 'cheap' for trivial/mechanical lookups (list " +
"files, grep, count, simple summaries), 'balanced' for normal work (default when unsure), 'strong' " +
"for hard reasoning, code review, or tricky debugging. For planning or design sub-tasks, put the word " +
'"plan" in the `label` so they route to the strongest model. Pass `modelSelection` instead only when ' +
"the user named a specific model. " +
"Always give each sub-task a short descriptive `label`. Sub-tasks share this thread's working " +
"directory and run concurrently, so keep their file changes non-overlapping to avoid conflicts.",
parameters: DelegateTasksRequest,
success: DelegateTasksResult,
failure: DelegateTasksFailure,
dependencies,
})
.annotate(Tool.Title, "Delegate sub-tasks")
.annotate(Tool.Destructive, true)
.annotate(Tool.OpenWorld, true);

export const CollectDelegatedTasksTool = Tool.make("collect_delegated_tasks", {
description:
"Collect results for sub-tasks that were still 'running' when `delegate_tasks` (or a prior " +
"`collect_delegated_tasks`) returned. Pass the `threadIds` of the running sub-tasks, or omit them to " +
"collect every still-running sub-task you delegated from this thread. Returns quickly; any task still " +
"not finished comes back as 'running' again — keep calling until none remain 'running'.",
parameters: CollectDelegatedTasksRequest,
success: DelegateTasksResult,
dependencies,
})
.annotate(Tool.Title, "Collect delegated sub-tasks")
.annotate(Tool.Readonly, true)
.annotate(Tool.Idempotent, true);

export const DelegationToolkit = Toolkit.make(DelegateTasksTool, CollectDelegatedTasksTool);
2 changes: 2 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,8 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
interactionMode: event.payload.interactionMode,
branch: event.payload.branch,
worktreePath: event.payload.worktreePath,
parentThreadId: event.payload.parentThreadId ?? null,
taskLabel: event.payload.taskLabel ?? null,
latestTurnId: null,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
parent_thread_id AS "parentThreadId",
task_label AS "taskLabel",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -357,6 +359,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
parent_thread_id AS "parentThreadId",
task_label AS "taskLabel",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -387,6 +391,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
parent_thread_id AS "parentThreadId",
task_label AS "taskLabel",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -749,6 +755,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
parent_thread_id AS "parentThreadId",
task_label AS "taskLabel",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -1181,6 +1189,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
parentThreadId: row.parentThreadId ?? null,
taskLabel: row.taskLabel ?? null,
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1379,6 +1389,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
parentThreadId: row.parentThreadId ?? null,
taskLabel: row.taskLabel ?? null,
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1508,6 +1520,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
parentThreadId: row.parentThreadId ?? null,
taskLabel: row.taskLabel ?? null,
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1642,6 +1656,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
parentThreadId: row.parentThreadId ?? null,
taskLabel: row.taskLabel ?? null,
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1882,6 +1898,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: threadRow.value.interactionMode,
branch: threadRow.value.branch,
worktreePath: threadRow.value.worktreePath,
parentThreadId: threadRow.value.parentThreadId ?? null,
taskLabel: threadRow.value.taskLabel ?? null,
latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
createdAt: threadRow.value.createdAt,
updatedAt: threadRow.value.updatedAt,
Expand Down Expand Up @@ -1976,6 +1994,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: threadRow.value.interactionMode,
branch: threadRow.value.branch,
worktreePath: threadRow.value.worktreePath,
parentThreadId: threadRow.value.parentThreadId ?? null,
taskLabel: threadRow.value.taskLabel ?? null,
latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
createdAt: threadRow.value.createdAt,
updatedAt: threadRow.value.updatedAt,
Expand Down
Loading
Loading