Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
efada2f
Add independent validation subagents
ross0x01 Aug 1, 2026
654b8f5
Harden subagent lifecycle handling
ross0x01 Aug 2, 2026
8f9becb
Close subagent outcome edge cases
ross0x01 Aug 2, 2026
ad9509a
Record subagent setup failures
ross0x01 Aug 2, 2026
db244bf
Harden subagent runtime and sidebar
ross0x01 Aug 3, 2026
82a2ab6
Polish subagent recovery UX
ross0x01 Aug 3, 2026
16fe98a
Route validation subagents through cheaper models
ross0x01 Aug 3, 2026
d7318f7
Harden subagent failure cleanup
ross0x01 Aug 3, 2026
f9ca4f3
Remove vulnerability reporting from subagent runtime
ross0x01 Aug 3, 2026
84228a5
Bound subagent transcript cleanup
ross0x01 Aug 3, 2026
043e971
Add explicit subagent lifecycle tools
ross0x01 Aug 4, 2026
478f34d
Harden subagent parent coordination
ross0x01 Aug 4, 2026
dced6ab
Harden subagent provider response handling
ross0x01 Aug 8, 2026
a711dae
Enable subagent testing in preview
ross0x01 Aug 8, 2026
d4a583a
Fix preview subagent Convex routing
ross0x01 Aug 8, 2026
2c8c285
Force structured subagent recovery result
ross0x01 Aug 8, 2026
9ce4a83
Use structured subagent result recovery
ross0x01 Aug 8, 2026
c1a6485
Keep failed validation independent
ross0x01 Aug 8, 2026
608d2fa
Fix paid subagent usage settlement
ross0x01 Aug 8, 2026
d94e53f
Group subagent lifecycle activity
ross0x01 Aug 8, 2026
05cb0db
Measure subagent availability and call attempts
ross0x01 Aug 10, 2026
6dbd303
Merge remote-tracking branch 'origin/main' into codex/independent-val…
ross0x01 Aug 10, 2026
bddf96d
Harden subagent lifecycle UX
ross0x01 Aug 10, 2026
f9c0f32
Settle subagents when parent runs end
ross0x01 Aug 10, 2026
891cb3b
Contain subagent settlement failures
ross0x01 Aug 10, 2026
1e3ad66
Expose short subagent handles
ross0x01 Aug 13, 2026
b2b01af
Merge remote-tracking branch 'origin/main' into codex/independent-val…
ross0x01 Aug 13, 2026
3c80a50
Harden subagent ownership tests
ross0x01 Aug 13, 2026
84c3175
Fix persisted subagent lifecycle navigation
ross0x01 Aug 13, 2026
a205b23
Merge remote-tracking branch 'origin/main' into codex/independent-val…
ross0x01 Aug 13, 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
48 changes: 48 additions & 0 deletions app/api/chat/[id]/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const mockDeleteChatForBackend = jest.fn();
const mockCancelAgentTriggerRun = jest.fn();
const mockCloseAgentApprovalSession = jest.fn();
const mockAssertUserCanAccessChatHistory = jest.fn();
const mockCancelSubagentsForChatDeletion = jest.fn();

jest.mock("next/server", () => ({
NextResponse: class MockNextResponse {
Expand Down Expand Up @@ -48,6 +49,10 @@ jest.mock("@/lib/db/actions", () => ({
deleteChatForBackend: mockDeleteChatForBackend,
}));

jest.mock("@/lib/db/subagents", () => ({
cancelSubagentsForChatDeletion: mockCancelSubagentsForChatDeletion,
}));

jest.mock("@/lib/suspensions", () => ({
assertUserCanAccessChatHistory: mockAssertUserCanAccessChatHistory,
}));
Expand Down Expand Up @@ -89,6 +94,10 @@ describe("DELETE /api/chat/[id]", () => {
mockCancelAgentTriggerRun.mockResolvedValue(true as never);
mockCloseAgentApprovalSession.mockResolvedValue(true as never);
mockDeleteChatForBackend.mockResolvedValue("deleted" as never);
mockCancelSubagentsForChatDeletion.mockResolvedValue({
triggerRunIds: [],
hasMore: false,
} as never);
});

afterEach(() => {
Expand Down Expand Up @@ -130,10 +139,35 @@ describe("DELETE /api/chat/[id]", () => {
expectedTriggerRunId: "run-1",
expectedApprovalSessionId: "approval-session-1",
});
expect(mockCancelSubagentsForChatDeletion).toHaveBeenCalledWith(
"chat-1",
"user-1",
"chat_deleted",
);
expect(calls.slice(0, 2).sort()).toEqual(["cancel", "close"]);
expect(calls[2]).toBe("delete");
});

it("cancels child Trigger runs before deleting their persisted records", async () => {
const { DELETE } = await import("../route");
mockCancelSubagentsForChatDeletion.mockResolvedValue({
triggerRunIds: ["child-run-1"],
hasMore: false,
} as never);

const response = await DELETE(request, paramsFor());

expect(response.status).toBe(200);
expect(mockCancelAgentTriggerRun).toHaveBeenCalledWith("run-1");
expect(mockCancelAgentTriggerRun).toHaveBeenCalledWith("child-run-1");
expect(
mockCancelSubagentsForChatDeletion.mock.invocationCallOrder[0],
).toBeLessThan(mockDeleteChatForBackend.mock.invocationCallOrder[0]);
expect(mockCancelAgentTriggerRun.mock.invocationCallOrder[1]).toBeLessThan(
mockDeleteChatForBackend.mock.invocationCallOrder[0],
);
});

it("does not delete when Trigger cleanup fails", async () => {
const { DELETE } = await import("../route");
mockCancelAgentTriggerRun.mockRejectedValue(
Expand All @@ -146,6 +180,20 @@ describe("DELETE /api/chat/[id]", () => {
expect(mockDeleteChatForBackend).not.toHaveBeenCalled();
});

it("fails closed when child cancellation exceeds the safe batch", async () => {
const { DELETE } = await import("../route");
mockCancelSubagentsForChatDeletion.mockResolvedValue({
triggerRunIds: [],
hasMore: true,
} as never);

const response = await DELETE(request, paramsFor());

expect(response.status).toBe(409);
expect(mockCancelAgentTriggerRun).not.toHaveBeenCalled();
expect(mockDeleteChatForBackend).not.toHaveBeenCalled();
});

it("deletes without calling Trigger when there is no active run", async () => {
const { DELETE } = await import("../route");
mockGetChatById.mockResolvedValue(
Expand Down
20 changes: 17 additions & 3 deletions app/api/chat/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
cancelAgentTriggerRun,
closeAgentApprovalSession,
} from "@/lib/api/agent-approval-session";
import { cancelSubagentsForChatDeletion } from "@/lib/db/subagents";

export const maxDuration = 30;
const MAX_DELETE_SNAPSHOT_ATTEMPTS = 3;
Expand Down Expand Up @@ -43,13 +44,26 @@ export async function DELETE(

const triggerRunId = chat.active_trigger_run_id;
const approvalSessionId = chat.active_agent_approval_session_id;
const [closed, canceled] = await Promise.all([
const childCancellation = await cancelSubagentsForChatDeletion(
chatId,
userId,
"chat_deleted",
);
if (childCancellation.hasMore) {
return new NextResponse("Too many validation runs to delete safely", {
status: 409,
});
}
const [closed, canceled, ...childCancellations] = await Promise.all([
closeAgentApprovalSession(approvalSessionId, "chat-deleted"),
cancelAgentTriggerRun(triggerRunId),
...childCancellation.triggerRunIds.map((childRunId) =>
cancelAgentTriggerRun(childRunId),
),
]);
closedApprovalSession ||= closed;
canceledTriggerRun ||= canceled;

canceledTriggerRun ||=
canceled || childCancellations.some((childCanceled) => childCanceled);
const deleteResult = await deleteChatForBackend({
chatId,
userId,
Expand Down
36 changes: 35 additions & 1 deletion app/api/chats/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const mockFenceAndGetActiveAgentResourcesForUser = jest.fn();
const mockDeleteAllChatsForBackend = jest.fn();
const mockCloseAndCancelAgentResources = jest.fn();
const mockAssertUserCanAccessChatHistory = jest.fn();
const mockCancelSubagentsForUserDeletion = jest.fn();

jest.mock("next/server", () => ({
NextResponse: class MockNextResponse {
Expand Down Expand Up @@ -47,6 +48,10 @@ jest.mock("@/lib/db/actions", () => ({
deleteAllChatsForBackend: mockDeleteAllChatsForBackend,
}));

jest.mock("@/lib/db/subagents", () => ({
cancelSubagentsForUserDeletion: mockCancelSubagentsForUserDeletion,
}));

jest.mock("@/lib/suspensions", () => ({
assertUserCanAccessChatHistory: mockAssertUserCanAccessChatHistory,
}));
Expand Down Expand Up @@ -90,6 +95,10 @@ describe("DELETE /api/chats", () => {
closedApprovalSessions: 2,
} as never);
mockDeleteAllChatsForBackend.mockResolvedValue(undefined as never);
mockCancelSubagentsForUserDeletion.mockResolvedValue({
triggerRunIds: [],
hasMore: false,
} as never);
});

afterEach(() => {
Expand All @@ -99,6 +108,10 @@ describe("DELETE /api/chats", () => {
it("cancels active Trigger runs before deleting chats", async () => {
const { DELETE } = await import("../route");
const calls: string[] = [];
mockCancelSubagentsForUserDeletion.mockResolvedValue({
triggerRunIds: ["child-run-1"],
hasMore: false,
} as never);
mockCloseAndCancelAgentResources.mockImplementation(async () => {
calls.push("cleanup");
return { canceledTriggerRuns: 2, closedApprovalSessions: 2 };
Expand All @@ -120,12 +133,19 @@ describe("DELETE /api/chats", () => {
userId: "user-1",
});
expect(mockCloseAndCancelAgentResources).toHaveBeenCalledWith(
activeResources("run-1", "run-2").resources,
[
...activeResources("run-1", "run-2").resources,
{ chatId: "subagent", triggerRunId: "child-run-1" },
],
"chat-deleted",
);
expect(mockDeleteAllChatsForBackend).toHaveBeenCalledWith({
userId: "user-1",
});
expect(mockCancelSubagentsForUserDeletion).toHaveBeenCalledWith(
"user-1",
"all_chats_deleted",
);
expect(calls).toEqual(["cleanup", "delete"]);
});

Expand Down Expand Up @@ -173,6 +193,20 @@ describe("DELETE /api/chats", () => {
expect(mockDeleteAllChatsForBackend).not.toHaveBeenCalled();
});

it("fails closed when child cancellation exceeds the safe batch", async () => {
const { DELETE } = await import("../route");
mockCancelSubagentsForUserDeletion.mockResolvedValue({
triggerRunIds: [],
hasMore: true,
} as never);

const response = await DELETE(request);

expect(response.status).toBe(409);
expect(mockCloseAndCancelAgentResources).not.toHaveBeenCalled();
expect(mockDeleteAllChatsForBackend).not.toHaveBeenCalled();
});

it("does not delete chats when active runs exceed the safe lookup cap", async () => {
const { DELETE } = await import("../route");
mockFenceAndGetActiveAgentResourcesForUser.mockResolvedValue({
Expand Down
18 changes: 17 additions & 1 deletion app/api/chats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ChatSDKError } from "@/lib/errors";
import { assertUserCanAccessChatHistory } from "@/lib/suspensions";
import { closeAndCancelAgentResources } from "@/lib/api/agent-deletion-cleanup";
import { cancelSubagentsForUserDeletion } from "@/lib/db/subagents";

export const maxDuration = 30;

Expand All @@ -26,8 +27,23 @@ export async function DELETE(req: NextRequest) {
);
}

const childCancellation = await cancelSubagentsForUserDeletion(
userId,
"all_chats_deleted",
);
if (childCancellation.hasMore) {
return new NextResponse("Too many validation runs to delete safely", {
status: 409,
});
}
const cleanup = await closeAndCancelAgentResources(
activeAgentResources.resources,
[
...activeAgentResources.resources,
...childCancellation.triggerRunIds.map((triggerRunId) => ({
chatId: "subagent",
triggerRunId,
})),
],
"chat-deleted",
);
await deleteAllChatsForBackend({ userId });
Expand Down
26 changes: 26 additions & 0 deletions app/api/delete-account/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { stripe } from "../../stripe";
import { workos } from "../../workos";
import { fenceAndGetActiveAgentResourcesForUser } from "@/lib/db/actions";
import { closeAndCancelAgentResources } from "@/lib/api/agent-deletion-cleanup";
import { cancelSubagentsForUserDeletion } from "@/lib/db/subagents";
import { logger } from "@/lib/logger";

const mockConvexMutation = jest.fn();
Expand Down Expand Up @@ -40,6 +41,10 @@ jest.mock("@/lib/api/agent-deletion-cleanup", () => ({
closeAndCancelAgentResources: jest.fn(),
}));

jest.mock("@/lib/db/subagents", () => ({
cancelSubagentsForUserDeletion: jest.fn(),
}));

jest.mock("@/lib/logger", () => ({
logger: {
error: jest.fn(),
Expand Down Expand Up @@ -128,6 +133,10 @@ const mockCloseAndCancelAgentResources =
closeAndCancelAgentResources as jest.MockedFunction<
typeof closeAndCancelAgentResources
>;
const mockCancelSubagentsForUserDeletion =
cancelSubagentsForUserDeletion as jest.MockedFunction<
typeof cancelSubagentsForUserDeletion
>;
const mockLoggerError = logger.error as jest.MockedFunction<
typeof logger.error
>;
Expand Down Expand Up @@ -157,6 +166,10 @@ describe("POST /api/delete-account", () => {
canceledTriggerRuns: 0,
closedApprovalSessions: 0,
} as never);
mockCancelSubagentsForUserDeletion.mockResolvedValue({
triggerRunIds: [],
hasMore: false,
} as never);
mockConvexMutation.mockImplementation(async (functionReference) =>
functionReference === "userDeletion.deleteAllUserDataByService"
? { hasMore: false }
Expand All @@ -170,6 +183,10 @@ describe("POST /api/delete-account", () => {
});

it("removes only the caller's membership for shared organizations", async () => {
mockCancelSubagentsForUserDeletion.mockResolvedValue({
triggerRunIds: ["child-run-1"],
hasMore: false,
} as never);
const callerMembership = {
id: "membership_user",
organizationId: "org_team",
Expand Down Expand Up @@ -218,11 +235,20 @@ describe("POST /api/delete-account", () => {
expect(mockDeleteCustomer).not.toHaveBeenCalled();
expect(mockDeleteOrganization).not.toHaveBeenCalled();
expect(mockDeleteUser).toHaveBeenCalledWith("user_123");
expect(mockCloseAndCancelAgentResources).toHaveBeenCalledWith(
[{ chatId: "subagent", triggerRunId: "child-run-1" }],
"account-deleted",
);
expect(mockConvexMutation.mock.invocationCallOrder[0]).toBeLessThan(
mockFenceAndGetActiveAgentResourcesForUser.mock.invocationCallOrder[0],
);
expect(
mockFenceAndGetActiveAgentResourcesForUser.mock.invocationCallOrder[0],
).toBeLessThan(
mockCancelSubagentsForUserDeletion.mock.invocationCallOrder[0],
);
expect(
mockCancelSubagentsForUserDeletion.mock.invocationCallOrder[0],
).toBeLessThan(
mockCloseAndCancelAgentResources.mock.invocationCallOrder[0],
);
Expand Down
18 changes: 17 additions & 1 deletion app/api/delete-account/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { api } from "@/convex/_generated/api";
import { logger } from "@/lib/logger";
import { fenceAndGetActiveAgentResourcesForUser } from "@/lib/db/actions";
import { closeAndCancelAgentResources } from "@/lib/api/agent-deletion-cleanup";
import { cancelSubagentsForUserDeletion } from "@/lib/db/subagents";

type OrganizationMembership = Awaited<
ReturnType<typeof workos.userManagement.listOrganizationMemberships>
Expand Down Expand Up @@ -284,8 +285,23 @@ export const POST = async (req: NextRequest) => {
}

stage = "close_active_agent_resources";
const childCancellation = await cancelSubagentsForUserDeletion(
userId,
"account_deleted",
);
if (childCancellation.hasMore) {
throw new Error(
"Too many validation runs to delete safely. Please stop active validation runs and retry.",
);
}
await closeAndCancelAgentResources(
activeAgentResources.resources,
[
...activeAgentResources.resources,
...childCancellation.triggerRunIds.map((triggerRunId) => ({
chatId: "subagent",
triggerRunId,
})),
],
"account-deleted",
);

Expand Down
Loading