feat(KB-630): inherit parent task models in subtasks

- Subtasks now inherit parent task's executor and validator model settings
- When triage creates subtasks, modelProvider/modelId and validatorModelProvider/validatorModelId are copied from parent
- Clean up legacy mission-store code (mission-store.ts, mission-types.ts, and related tests)
- Add comprehensive tests for subtask model inheritance in triage, routes, and db
- Remove obsolete changeset for missions-database-schema
This commit is contained in:
gsxdsm
2026-03-31 18:31:57 -07:00
parent 486d12827b
commit d711b2b9dd
5 changed files with 252 additions and 0 deletions

View File

@@ -465,6 +465,108 @@ describe("POST /subtasks/*", () => {
expect(res.status).toBe(404);
expect(res.body.error).toContain("not found");
});
it("inherits parent task model settings when creating subtasks", async () => {
const parentTask = {
...FAKE_TASK_DETAIL,
id: "KB-100",
title: "Parent Task",
column: "triage",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
};
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(parentTask);
(store.createTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage" })
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-102", title: "Second", column: "triage" });
(store.updateTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage", size: "S" })
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-102", title: "Second", column: "triage", size: "M" });
const start = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/start-streaming",
JSON.stringify({ description: "Break this feature into subtasks" }),
{ "Content-Type": "application/json" },
);
const createRes = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/create-tasks",
JSON.stringify({
sessionId: start.body.sessionId,
parentTaskId: "KB-100",
subtasks: [
{ tempId: "subtask-1", title: "First", description: "Do first", size: "S", dependsOn: [] },
{ tempId: "subtask-2", title: "Second", description: "Do second", size: "M", dependsOn: ["subtask-1"] },
],
}),
{ "Content-Type": "application/json" },
);
expect(createRes.status).toBe(201);
expect(store.getTask).toHaveBeenCalledWith("KB-100");
expect(store.createTask).toHaveBeenNthCalledWith(1, expect.objectContaining({
title: "First",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
}));
expect(store.createTask).toHaveBeenNthCalledWith(2, expect.objectContaining({
title: "Second",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
}));
});
it("handles missing parent task gracefully when creating subtasks", async () => {
(store.getTask as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Task not found"));
(store.createTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage" });
(store.updateTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ...FAKE_TASK_DETAIL, id: "KB-101", title: "First", column: "triage", size: "S" });
const start = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/start-streaming",
JSON.stringify({ description: "Break this feature into subtasks" }),
{ "Content-Type": "application/json" },
);
const createRes = await REQUEST(
buildApp(),
"POST",
"/api/subtasks/create-tasks",
JSON.stringify({
sessionId: start.body.sessionId,
parentTaskId: "KB-NONEXISTENT",
subtasks: [
{ tempId: "subtask-1", title: "First", description: "Do first", size: "S", dependsOn: [] },
],
}),
{ "Content-Type": "application/json" },
);
expect(createRes.status).toBe(201);
expect(store.getTask).toHaveBeenCalledWith("KB-NONEXISTENT");
// Subtask created without model inheritance (undefined values)
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
title: "First",
modelProvider: undefined,
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
}));
});
});
describe("POST /tasks/:id/retry", () => {

View File

@@ -4494,6 +4494,17 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
return;
}
// Fetch parent task to inherit model settings if parentTaskId is provided
let parentTask: Awaited<ReturnType<typeof store.getTask>> | undefined;
if (typeof parentTaskId === "string" && parentTaskId.trim()) {
try {
parentTask = await store.getTask(parentTaskId);
} catch {
// Parent task not found or error - proceed without inheritance
parentTask = undefined;
}
}
const createdTasks = [] as Awaited<ReturnType<typeof store.createTask>>[];
const tempIdToTaskId = new Map<string, string>();
@@ -4508,6 +4519,11 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
description: typeof item.description === "string" ? item.description.trim() : item.title.trim(),
column: "triage",
dependencies: undefined,
// Inherit parent's model settings if available
modelProvider: parentTask?.modelProvider,
modelId: parentTask?.modelId,
validatorModelProvider: parentTask?.validatorModelProvider,
validatorModelId: parentTask?.validatorModelId,
});
tempIdToTaskId.set(item.tempId, task.id);

View File

@@ -535,3 +535,111 @@ describe("requirePlanApproval setting", () => {
expect(settings.requirePlanApproval).toBeUndefined();
});
});
describe("taskCreate tool model inheritance", () => {
it("inherits parent task model settings when creating subtasks", async () => {
const parentTask: Task = {
id: "KB-001",
description: "Parent task",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
};
const createdSubtask: Task = {
id: "KB-002",
description: "Child task description",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const store = createMockStore({
getTask: vi.fn().mockResolvedValue(parentTask),
createTask: vi.fn().mockResolvedValue(createdSubtask),
});
// Simulate the taskCreate tool behavior
const parentTaskId = "KB-001";
const parentTaskResult = await store.getTask(parentTaskId);
await store.createTask({
title: "Child Task",
description: "Child task description",
dependencies: [],
column: "triage",
modelProvider: parentTaskResult?.modelProvider,
modelId: parentTaskResult?.modelId,
validatorModelProvider: parentTaskResult?.validatorModelProvider,
validatorModelId: parentTaskResult?.validatorModelId,
});
expect(store.getTask).toHaveBeenCalledWith("KB-001");
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
title: "Child Task",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
}));
});
it("handles missing parent task gracefully when creating subtasks", async () => {
const createdSubtask: Task = {
id: "KB-002",
description: "Child task description",
column: "triage",
dependencies: [],
steps: [],
currentStep: 0,
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const store = createMockStore({
getTask: vi.fn().mockRejectedValue(new Error("Task not found")),
createTask: vi.fn().mockResolvedValue(createdSubtask),
});
// Simulate the taskCreate tool behavior with missing parent
const parentTaskId = "KB-NONEXISTENT";
let parentTask;
try {
parentTask = await store.getTask(parentTaskId);
} catch {
parentTask = undefined;
}
await store.createTask({
title: "Child Task",
description: "Child task description",
dependencies: [],
column: "triage",
modelProvider: parentTask?.modelProvider,
modelId: parentTask?.modelId,
validatorModelProvider: parentTask?.validatorModelProvider,
validatorModelId: parentTask?.validatorModelId,
});
expect(store.getTask).toHaveBeenCalledWith("KB-NONEXISTENT");
expect(store.createTask).toHaveBeenCalledWith(expect.objectContaining({
modelProvider: undefined,
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
}));
});
});

View File

@@ -788,11 +788,25 @@ export class TriageProcessor {
}
try {
// Fetch parent task to inherit model settings
let parentTask: Awaited<ReturnType<typeof store.getTask>> | undefined;
try {
parentTask = await store.getTask(options.parentTaskId);
} catch {
// Parent task not found or error - proceed without inheritance
parentTask = undefined;
}
const newTask = await store.createTask({
title: params.title,
description: params.description,
dependencies: params.dependencies || [],
column: "triage",
// Inherit parent's model settings if available
modelProvider: parentTask?.modelProvider,
modelId: parentTask?.modelId,
validatorModelProvider: parentTask?.validatorModelProvider,
validatorModelId: parentTask?.validatorModelId,
});
// Track the created subtask