Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5d62cde
fix(server): recover remote delegated launches after source restart
Lenoridev Nov 2, 2026
39278ad
fix(server): use Node hashing with pinned Effect crypto
Lenoridev Nov 3, 2026
0ae7779
fix(server): retain remote task IDs across deferred launches
Lenoridev Nov 3, 2026
bace21a
fix(server): stop retrying remote launches for missing projects
Lenoridev Nov 3, 2026
b8c828a
fix(server): stop remote tasks with their parent thread
maria-rcks Nov 3, 2026
434c355
fix(clients): show current remote launch recovery warnings
maria-rcks Nov 3, 2026
8c478af
fix(server): preserve recovery warnings across repeated stops
maria-rcks Nov 3, 2026
1217268
Merge remote-tracking branch 'github/main' into pr-30249
maria-rcks Nov 6, 2026
bc593f7
fix(mobile): use live remote child for grouped subagent avatars
maria-rcks Nov 6, 2026
567ba75
fix(server): keep launch identity when a peer link is relabeled
maria-rcks Nov 6, 2026
25f9788
Merge remote-tracking branch 'github/main' into pr-30249
maria-rcks Nov 10, 2026
f4be834
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
juliusmarminge Nov 11, 2026
c53694d
test(server): trim remote launch recovery tests to regressions
juliusmarminge Nov 11, 2026
5def361
fix(server): leave inherited modes to the remote destination
juliusmarminge Nov 11, 2026
f491a64
fix(server): a refused first remote launch reports no uncertainty
juliusmarminge Nov 11, 2026
bba0b92
fix(server): return definite remote launch refusals
Lenoridev Nov 12, 2026
2a8cb70
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 12, 2026
eb3c8f3
fix(web): skip unattached remote tasks in response links
Lenoridev Nov 12, 2026
f6d072b
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 12, 2026
ec196dd
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 13, 2026
1c5a110
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 14, 2026
bef0bfc
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 14, 2026
fd5c66f
Merge remote-tracking branch 'origin/main' into fix/remote-launch-rec…
Lenoridev Nov 15, 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
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
import * as DateTime from "effect/DateTime";
import { describe, expect, it } from "vite-plus/test";
import { subagentCardElapsed } from "./subagent-card-presentation";
import { resolveSubagentRowPresentation, subagentThreadTarget } from "./threadAgentsPresentation";

describe("subagent card", () => {
const start = DateTime.makeUnsafe("2026-09-21T12:00:00Z");
const end = DateTime.makeUnsafe("2026-09-21T12:01:00Z");
const later = DateTime.makeUnsafe("2026-09-21T12:02:00Z");
const done = { status: "completed" as const, startedAt: start, completedAt: end };

it("keeps a pending remote launch visible without opening a nonexistent thread", () => {
const pending = {
title: null,
prompt: "Audit the timestamps",
status: "running" as const,
result: null,
childThreadId: null,
remoteChild: { environmentId: "environment-box" as never, label: "Box" },
};
expect(resolveSubagentRowPresentation(pending, true)).toMatchObject({
runsOn: "Box",
statusLabel: "Launching",
canOpenThread: false,
});
expect(subagentThreadTarget(pending, "environment-here" as never, () => true)).toBeNull();
const launchError = "Launch unconfirmed; work may still be running on Box.";
expect(
resolveSubagentRowPresentation(
{
...pending,
status: "cancelled",
result: "Cancelled.",
remoteChild: { ...pending.remoteChild, launchError },
},
true,
),
).toMatchObject({ statusLabel: "Cancelled", detail: launchError, canOpenThread: false });
});

it("counts live work but never uses a settled agent's age as its duration", () => {
const unfinished = { ...done, completedAt: null };
expect(subagentCardElapsed([unfinished], DateTime.toEpochMillis(later))).toBeNull();
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/threads/thread-subagent-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export function ThreadSubagentGroup(props: {
completedAt: live?.completedAt ?? item.completedAt,
result: live?.result ?? item.result,
progress: live?.progress ?? item.progress,
remoteChild: live?.remoteChild ?? item.remoteChild,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
model: live?.model ?? null,
modelSelection: live?.modelSelection,
};
Expand All @@ -152,7 +153,7 @@ export function ThreadSubagentGroup(props: {
<View className="flex-row items-center">
{agents.slice(0, 3).map((agent, index) => (
<View key={agent.item.id} style={{ marginLeft: index === 0 ? 0 : -7 }}>
<SubagentAvatar item={agent.item} config={configOf(agent.item)} />
<SubagentAvatar item={agent} config={configOf(agent)} />
</View>
))}
{agents.length > 3 ? (
Expand Down
17 changes: 13 additions & 4 deletions apps/mobile/src/features/threads/threadAgentsPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ export function subagentThreadTarget(
return { environmentId, threadId: subagent.childThreadId };
}
const remote = subagent.remoteChild;
return remote !== undefined && isKnownEnvironment(remote.environmentId)
return remote !== undefined &&
remote.threadId !== undefined &&
isKnownEnvironment(remote.environmentId)
? { environmentId: remote.environmentId, threadId: remote.threadId }
: null;
}
Expand Down Expand Up @@ -99,12 +101,19 @@ export function resolveSubagentRowPresentation(
const live = isActiveSubagentStatus(subagent.status);
return {
title: rowTitle(subagent),
detail: subagentDetailPreview(subagent),
statusLabel: rowStatusLabel(subagent.status),
detail: subagentDetailPreview({
...subagent,
result: subagent.remoteChild?.launchError ?? subagent.result,
}),
statusLabel:
live && subagent.remoteChild !== undefined && subagent.remoteChild.threadId === undefined
? "Launching"
: rowStatusLabel(subagent.status),
tone: rowTone(subagent.status),
live,
canOpenThread:
subagent.childThreadId !== null || (subagent.remoteChild !== undefined && canOpenRemote),
subagent.childThreadId !== null ||
(subagent.remoteChild?.threadId !== undefined && canOpenRemote),
runsOn: subagent.remoteChild?.label ?? null,
};
}
Expand Down
23 changes: 21 additions & 2 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1971,8 +1971,27 @@ const make = Effect.gen(function* () {
...(task.remoteChild === undefined
? {}
: {
remoteChild: task.remoteChild,
link: formatThreadLink(task.remoteChild, task.remoteChild.title),
...(task.remoteChild.threadId === undefined
? {}
: {
link: formatThreadLink(
{
environmentId: task.remoteChild.environmentId,
threadId: task.remoteChild.threadId,
},
task.remoteChild.title,
),
}),
remoteChild: {
environmentId: task.remoteChild.environmentId,
label: task.remoteChild.label,
...(task.remoteChild.launchError === undefined
? {}
: { launchError: task.remoteChild.launchError }),
...(task.remoteChild.threadId === undefined
? {}
: { threadId: task.remoteChild.threadId }),
},
}),
childRunId: null,
childNodeId: task.id,
Expand Down
38 changes: 38 additions & 0 deletions apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const clientLaunchHarness = (input: {
readonly hostedAppUrl?: string;
/** The orchestrator refuses the new thread's mode, as under a managed ceiling. */
readonly refusedByCeiling?: true;
readonly launchFailure?: "missing" | "lookup";
}) => {
const projectId = ProjectId.make("project:client-target");
const modelSelection = {
Expand Down Expand Up @@ -125,6 +126,18 @@ const clientLaunchHarness = (input: {
checkWorktreeBase: input.checkWorktreeBase ?? (() => Effect.void),
launch: (launch) => {
input.launched.push(launch);
if (input.launchFailure)
return Effect.fail(
new ThreadLaunch.ThreadLaunchError({
operation: "resolve-project",
commandId: launch.commandId,
projectId: launch.projectId,
cause:
input.launchFailure === "missing"
? new Project.ProjectNotFoundError({ projectId: launch.projectId })
: new Error("Project storage unavailable"),
}),
);
if (input.refusedByCeiling) {
return Effect.fail(
new ThreadLaunch.ThreadLaunchError({
Expand Down Expand Up @@ -934,6 +947,31 @@ it.effect("deleting a project through MCP also discards its clone record", () =>
}),
);

it.effect.each(["missing", "lookup"] as const)(
"classifies %s project launch failures",
(launchFailure) =>
Effect.gen(function* () {
const { projectId, dependencies } = clientLaunchHarness({
runtimeModeCeiling: "full-access",
launched: [],
launchFailure,
});
const toolkit = yield* ProjectToolkit.pipe(
Effect.provide(
McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe(
Layer.provide(dependencies),
),
),
);
const result = yield* toolkit
.handle("t3_thread_launch", { projectId, title: "Missing project" })
.pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies));
expect(result.at(-1)?.result).toMatchObject({
code: launchFailure === "missing" ? "invalid_request" : "orchestration_error",
});
}),
);

it.effect("an agent cannot delete the built-in scratch project, even with force", () =>
Effect.gen(function* () {
const deleted: Array<ProjectId> = [];
Expand Down
8 changes: 8 additions & 0 deletions apps/server/src/mcp/toolkits/project/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import * as OrchestrationMcp from "../../OrchestratorMcpService.ts";
import { newCommandId, readCaller, resolveProjectId, unavailable } from "../../threadAccess.ts";
import { ProjectToolkit } from "./tools.ts";

const isProjectNotFoundError = Schema.is(Project.ProjectNotFoundError);

const isAboveCeilingError = Schema.is(OrchestratorRuntimeModeAboveCeilingError);

function projectFailure(error: Project.ProjectServiceError) {
Expand Down Expand Up @@ -303,6 +305,12 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
message: error.message,
});
}
if (error._tag === "ThreadLaunchError" && isProjectNotFoundError(error.cause)) {
return new OrchestratorMcpFailure({
code: "invalid_request",
message: error.cause.message,
});
}
// The administrator's ceiling refuses the mode; say so rather than "unavailable".
if (error._tag === "ThreadLaunchError" && isAboveCeilingError(error.cause)) {
return new OrchestratorMcpFailure({
Expand Down
Loading
Loading