Skip to content

feat: delegate task#3549

Open
bahachammakhi wants to merge 1 commit into
pingdotgg:mainfrom
bahachammakhi:baha-clone
Open

feat: delegate task#3549
bahachammakhi wants to merge 1 commit into
pingdotgg:mainfrom
bahachammakhi:baha-clone

Conversation

@bahachammakhi

@bahachammakhi bahachammakhi commented Jun 25, 2026

Copy link
Copy Markdown

What Changed

Why

UI Changes

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
Introduces parallel child agent runs sharing a worktree and new orchestration/MCP paths; mis-coordinated edits or routing mistakes could amplify cost or conflicts, though depth-1 delegation and bounded waits limit blast radius.

Overview
Adds agent task delegation: lead threads can fan out independent sub-tasks to child threads through new MCP tools delegate_tasks and collect_delegated_tasks, wired into the HTTP MCP server with a TaskOrchestrator that spawns children in the same worktree, starts turns, and polls completion within bounded waits so tool calls stay under client timeouts.

Server & contracts: New @t3tools/contracts delegation schemas (DelegateTaskInput, results, limits), optional parentThreadId / taskLabel on thread create/events/projections, DB migration 033_ProjectionThreadDelegation, and configurable taskRouting rules plus TaskModelRouter for per-sub-task model selection (modelHint, label rules, explicit override).

MCP behavior: delegate_tasks starts up to 8 tasks with concurrency caps, optionally waits ~30s inline, then returns running / completed / error; children cannot delegate again (depth-1). collect_delegated_tasks finishes polling running handles.

UI: Sidebar nests delegated children under their lead (nestDelegatedChildren) with indent and a sub-task marker.

Also in diff: Desktop packaging rebrands production app to T3 Code Baha / com.t3tools.t3code.baha (product name, app id, signing tests)—separate from delegation.

Reviewed by Cursor Bugbot for commit c5e06ec. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add task delegation MCP tools with child thread spawning and sidebar grouping

  • Adds delegate_tasks and collect_delegated_tasks MCP tools via a new DelegationToolkit, allowing providers to spawn and collect results from delegated child threads
  • Implements TaskOrchestrator service that spawns child threads via the orchestration engine, polls for completion with a 30s bound, and guards against recursive delegation via isDelegatedChild
  • Routes delegated tasks to models using configurable rules in a new taskRouting section of server settings, with fallback to the parent model
  • Persists parentThreadId and taskLabel on threads via a new DB migration (033) and propagates these fields through contracts, projections, and snapshot queries
  • Indents delegated child threads under their parent in the sidebar with a marker and taskLabel as title
📊 Macroscope summarized c5e06ec. 22 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a53153dd-33f7-4e78-8991-cb88d6e38dc7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jun 25, 2026

@macroscopeapp macroscopeapp Bot left a comment

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.

Effect service review: one convention issue on the newly added TaskOrchestrator service.

Posted via Macroscope — Effect Service Conventions

Comment on lines +40 to +56
export interface TaskOrchestratorShape {
/**
* Spawn a child thread per sub-task and start its turn. Returns immediately
* with a handle per task (`status: "running"`, or `"error"` if spawning
* failed). Never blocks on completion.
*/
readonly startTasks: (input: TaskOrchestratorStartInput) => Effect.Effect<DelegateTasksResult>;
/**
* Poll the given child threads up to `waitMs` and return their results;
* children still running at the deadline come back as `status: "running"`.
*/
readonly collectTasks: (
input: TaskOrchestratorCollectInput,
) => Effect.Effect<DelegateTasksResult>;
/** True if `threadId` was spawned as a delegated child during this process. */
readonly isDelegatedChild: (threadId: ThreadId) => Effect.Effect<boolean>;
}

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.

The service interface is declared as a standalone TaskOrchestratorShape and then passed into Context.Service. Per the Effect service conventions, define the interface inline in the Context.Service declaration and refer to the inferred shape as TaskOrchestrator["Service"] rather than retaining a FooShape type.

Fix: inline these members into the tag declaration (Context.Service<TaskOrchestrator, { readonly startTasks: ...; readonly collectTasks: ...; readonly isDelegatedChild: ... }>()) and remove TaskOrchestratorShape. Then in Layers/TaskOrchestrator.ts drop the type TaskOrchestratorShape import and change TaskOrchestratorShape["startTasks"], TaskOrchestratorShape["collectTasks"], and TaskOrchestratorShape["isDelegatedChild"] to TaskOrchestrator["Service"][...]. The input types (TaskOrchestratorStartInput/TaskOrchestratorCollectInput) can stay as standalone helper types.

Posted via Macroscope — Effect Service Conventions

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort and found 7 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

});

const isDelegatedChild: TaskOrchestratorShape["isDelegatedChild"] = (threadId: ThreadId) =>
Ref.get(registry).pipe(Effect.map((current) => current.has(threadId)));

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.

Depth guard ignores persisted parent

High Severity

The TaskOrchestrator uses an in-memory registry for delegated task metadata that isn't durable across server restarts. This can bypass the depth-1 delegation guard, allowing sub-tasks to delegate further. It also prevents collect_delegated_tasks from finding and collecting results for existing delegated tasks if threadIds are not explicitly provided.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

...base,
status: "completed",
message: finalAssistantText(detail) ?? "",
} satisfies DelegateTaskResult;

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.

Interrupted sub-tasks marked completed

Medium Severity

The resultFromDetail function incorrectly treats interrupted sub-task turns as completed. This means a parent agent will see stopped or cancelled sub-tasks as successfully finished, potentially including an assistant message, masking their true outcome.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

targetIds,
(threadId) =>
pollUntilDone(threadId, deadlineMs).pipe(
Effect.map((detail) => resultFromDetail(threadId, current.get(threadId), detail)),

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.

Collect allows foreign thread IDs

Medium Severity

The collectTasks function processes input.threadIds without verifying they are delegated children of the input.parentThreadId or managed by this orchestrator instance. This allows a caller to query the status and final assistant message of arbitrary threads, potentially exposing information from other sessions.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

Comment thread apps/desktop/package.json
"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.

return { results: [] };
}
const settings = yield* settingsService.getSettings.pipe(Effect.orDie);
const tasks = input.tasks.slice(0, MAX_DELEGATED_TASKS);

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.

Extra tasks dropped silently

Medium Severity

startTasks truncates input.tasks with slice(0, MAX_DELEGATED_TASKS) but returns a normal success payload. Requests with more than the maximum number of sub-tasks run only the first batch with no error or count indicating the rest were ignored.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

Effect.gen(function* () {
const parent = yield* readDetail(input.parentThreadId);
if (parent === undefined) {
return { results: [] };

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.

Missing parent yields empty success

Medium Severity

If the lead thread is not found in the projection, startTasks returns { results: [] } instead of failing. delegate_tasks can succeed with zero results even when the caller supplied sub-tasks, which looks like a no-op rather than an error.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

? input.threadIds
: Array.from(current.entries())
.filter(([, meta]) => meta.parentThreadId === input.parentThreadId)
.map(([id]) => ThreadId.make(id));

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.

Default collect ignores running filter

Medium Severity

Omitting threadIds on collect_delegated_tasks enqueues every child ever recorded in the registry for that lead, not only those still running. Completed or errored sub-tasks are polled again and returned, contrary to the tool description.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c5e06ec. Configure here.

),
{ concurrency: Math.max(1, Math.min(targetIds.length, MAX_DELEGATED_TASKS)) },
);
return { results };

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.

🟡 Medium Layers/TaskOrchestrator.ts:268

Completed and errored children are never removed from registry. When collectTasks is called without threadIds, it builds targetIds from every registry entry for the lead (lines 242-247), so collect_delegated_tasks keeps returning old finished sub-tasks instead of only the still-running children promised by the request contract. Consider removing entries from registry once their final status is known (either completed or error) so that subsequent collection calls only see still-running children.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/TaskOrchestrator.ts around line 268:

Completed and errored children are never removed from `registry`. When `collectTasks` is called without `threadIds`, it builds `targetIds` from every registry entry for the lead (lines 242-247), so `collect_delegated_tasks` keeps returning old finished sub-tasks instead of only the still-running children promised by the request contract. Consider removing entries from `registry` once their final status is known (either completed or error) so that subsequent collection calls only see still-running children.

Comment on lines +52 to +61
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,
});
}),

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.

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.

🟡 Medium

export const ServerSettingsPatch = Schema.Struct({

taskRouting was added to ServerSettings but is missing from ServerSettingsPatch, so any taskRouting value sent through server.updateSettings is silently dropped because the RPC validates against the patch schema and applyServerSettingsPatch only merges known fields.

🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/settings.ts around line 532:

`taskRouting` was added to `ServerSettings` but is missing from `ServerSettingsPatch`, so any `taskRouting` value sent through `server.updateSettings` is silently dropped because the RPC validates against the patch schema and `applyServerSettingsPatch` only merges known fields.

})
.pipe(Effect.orDie);

yield* Ref.update(registry, (current) => {

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.

🟡 Medium Layers/TaskOrchestrator.ts:199

If thread.create succeeds but thread.turn.start fails, the child thread is created but never added to registry. This leaves a real delegated child that collectTasks cannot find and isDelegatedChild incorrectly reports as false, allowing the depth-1 recursion guard to be bypassed from that orphaned child.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/TaskOrchestrator.ts around line 199:

If `thread.create` succeeds but `thread.turn.start` fails, the child thread is created but never added to `registry`. This leaves a real delegated child that `collectTasks` cannot find and `isDelegatedChild` incorrectly reports as `false`, allowing the depth-1 recursion guard to be bypassed from that orphaned child.

@macroscopeapp

macroscopeapp Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

4 blocking correctness issues found. This PR introduces a substantial new task delegation feature enabling parallel child thread spawning. Multiple unresolved review comments identify security concerns (arbitrary thread access), correctness issues (depth guard bypass), and unrelated desktop branding changes affecting the bundle ID. Human review is warranted for this scope of new capability with open issues.

You can customize Macroscope's approvability policy. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant