feat(backend,frontend): tâches planifiées projet — tool MCP idea_scheduled_task_* et menu utilisateur #305, fix transcript lié indisponible après reboot #314 (QA verte)
#305: nouvel agrégat durable ScheduledProjectTask dans domain (scheduled_task.rs, ids, ports ScheduledProjectTaskStore), source de vérité <app_data_dir>/scheduled-project-tasks/<projectId>.json, schedules bornés V1 interval/daily/weekly avec fuseau, cycle À approuver/Active/Suspendue avec approbation obligatoire des tâches demandées par agent; use cases application/scheduled_task (list/create/update/delete/run_now/reconcile/fire) réutilisant BackgroundTask pour les runs avec distinction planifié/manuel; store infrastructure dédié + scheduler reconcile au boot sans rattrapage en rafale, cwd confiné au projet; 7 tools MCP idea_scheduled_task_* (list/read lecture seule, create/update/delete/run_now/set_enabled write-actions soumises aux permissions natives); commandes Tauri + routes HTTP aux contrats DTO identiques (camelCase nextRunAtMs/lastRunAtMs/lastTaskId/createdBy/updatedBy); frontend: gateways Tauri/HTTP/mock + types ports/domain, surface projet 'Tâches planifiées' (liste utilisateur+agent unifiée, création/édition avec récapitulatif avant activation, approbation/refus, exécuter maintenant, suspendre/reprendre, suppression), catalogage surface web (webSurfaceCatalog/viewPlacement/ViewPanelBody/WebWorkspace); docs/scheduled-project-tasks.md et sdk docs/services.md (047c38b: pas un service plugin V1, pas d'émulation via cron système). #314: correction de 'Conversation liée — transcript indisponible' après reboot: restauration du transcript lié côté application (orchestrator service, lifecycle, session_limit, background sink, terminal registry) et côté frontend (adapters agent/http frames/stream/requestResponse, workStateNormalization, CustomAgentChatView avec état et action utile), tests dto_chat/agent_lifecycle/structured_registry_d1/background_completion_sink étendus. QA verte rapportée par QA sur feature/305-scheduled-project-tasks-mcp, sans bloquant, pour #305 et #314. Hors périmètre worktree: tickets non liés, attachments/, scripts/; aucun push.
This commit is contained in:
@ -328,6 +328,7 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
cols: 80,
|
||||
cellKind: "chat",
|
||||
assignedConversationId: "conversation-1",
|
||||
transcriptUnavailableReason: "missingTranscript",
|
||||
});
|
||||
|
||||
const out = await new TauriAgentGateway().launchAgentChat("proj-1", "agent-2", {
|
||||
@ -355,6 +356,7 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
sessionId: "chat-session-1",
|
||||
cellKind: "chat",
|
||||
assignedConversationId: "conversation-1",
|
||||
transcriptUnavailableReason: "missingTranscript",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -52,6 +52,7 @@ interface LaunchAgentResponse {
|
||||
cellKind?: "chat" | "pty";
|
||||
/** Conversation id minted by this launch (omitted when nothing was assigned). */
|
||||
assignedConversationId?: string;
|
||||
transcriptUnavailableReason?: AgentChatHandle["transcriptUnavailableReason"];
|
||||
}
|
||||
|
||||
function structuredLaunchRoutedToPtyError(
|
||||
@ -269,6 +270,9 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
...(res.assignedConversationId
|
||||
? { assignedConversationId: res.assignedConversationId }
|
||||
: {}),
|
||||
...(res.transcriptUnavailableReason
|
||||
? { transcriptUnavailableReason: res.transcriptUnavailableReason }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -294,12 +294,14 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
sessionId: "chat-1",
|
||||
cellKind: "chat",
|
||||
assignedConversationId: "conv-1",
|
||||
transcriptUnavailableReason: "emptyConversation",
|
||||
},
|
||||
}));
|
||||
await expect(launched).resolves.toEqual({
|
||||
sessionId: "chat-1",
|
||||
cellKind: "chat",
|
||||
assignedConversationId: "conv-1",
|
||||
transcriptUnavailableReason: "emptyConversation",
|
||||
});
|
||||
expect(launch.kind).toBe("agent.launch");
|
||||
expect(launch.payload).toEqual({
|
||||
@ -324,12 +326,14 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
sessionId: "chat-1",
|
||||
conversationId: "conv-replacement",
|
||||
scrollback: [{ kind: "final", content: "Recovered reply" }],
|
||||
transcriptUnavailableReason: "missingTranscript",
|
||||
},
|
||||
}));
|
||||
await expect(reattached).resolves.toEqual({
|
||||
sessionId: "chat-1",
|
||||
conversationId: "conv-replacement",
|
||||
scrollback: [{ kind: "final", content: "Recovered reply" }],
|
||||
transcriptUnavailableReason: "missingTranscript",
|
||||
});
|
||||
expect(attach.kind).toBe("chat.attach");
|
||||
expect(attach.payload).toEqual({
|
||||
|
||||
@ -155,4 +155,5 @@ export interface ChatAttachedPayload {
|
||||
conversationId?: string;
|
||||
engineSessionId?: string;
|
||||
scrollback?: ReplyChunk[];
|
||||
transcriptUnavailableReason?: "missingTranscript" | "emptyConversation";
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
HttpSkillGateway,
|
||||
HttpTemplateGateway,
|
||||
HttpWorkStateGateway,
|
||||
HttpScheduledTaskGateway,
|
||||
} from "./requestResponseGateways";
|
||||
import {
|
||||
HttpAgentGateway,
|
||||
@ -140,6 +141,7 @@ export function createHttpWsGateways(config: HttpWsGatewaysConfig = {}): Gateway
|
||||
device: new HttpDeviceGateway(http),
|
||||
permission: new HttpPermissionGateway(http),
|
||||
workState: new HttpWorkStateGateway(http),
|
||||
scheduledTask: new HttpScheduledTaskGateway(http),
|
||||
conversation: new HttpConversationGateway(http),
|
||||
ticket: new HttpTicketGateway(http, ws),
|
||||
window: new WebWindowGateway(),
|
||||
|
||||
@ -49,6 +49,7 @@ import type {
|
||||
ProfileModelCatalog,
|
||||
ResolvedAgentPermissions,
|
||||
ResolvedAgentSystemPermissions,
|
||||
ScheduledProjectTask,
|
||||
SystemPermissionSet,
|
||||
Skill,
|
||||
SkillScope,
|
||||
@ -78,6 +79,8 @@ import type {
|
||||
SkillGateway,
|
||||
TemplateGateway,
|
||||
WorkStateGateway,
|
||||
ScheduledTaskDefinitionInput,
|
||||
ScheduledTaskGateway,
|
||||
BackgroundTaskAttachment,
|
||||
} from "@/ports";
|
||||
import { normalizeProjectWorkState } from "../workStateNormalization";
|
||||
@ -577,6 +580,45 @@ export class HttpWorkStateGateway implements WorkStateGateway {
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpScheduledTaskGateway implements ScheduledTaskGateway {
|
||||
constructor(private readonly http: HttpInvoker) {}
|
||||
list(projectId: string): Promise<ScheduledProjectTask[]> {
|
||||
return this.http.invoke("list_scheduled_project_tasks", { projectId });
|
||||
}
|
||||
create(projectId: string, input: ScheduledTaskDefinitionInput): Promise<ScheduledProjectTask> {
|
||||
return this.http.invoke("create_scheduled_project_task", {
|
||||
request: { projectId, ...input },
|
||||
});
|
||||
}
|
||||
update(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
auditRevision: number,
|
||||
input: ScheduledTaskDefinitionInput,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
return this.http.invoke("update_scheduled_project_task", {
|
||||
request: { projectId, scheduledTaskId, auditRevision, ...input },
|
||||
});
|
||||
}
|
||||
async delete(projectId: string, scheduledTaskId: string): Promise<void> {
|
||||
await this.http.invoke("delete_scheduled_project_task", { projectId, scheduledTaskId });
|
||||
}
|
||||
runNow(projectId: string, scheduledTaskId: string): Promise<ScheduledProjectTask> {
|
||||
return this.http.invoke("run_scheduled_project_task_now", { projectId, scheduledTaskId });
|
||||
}
|
||||
setEnabled(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
enabled: boolean,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
return this.http.invoke("set_scheduled_project_task_enabled", {
|
||||
projectId,
|
||||
scheduledTaskId,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpConversationGateway implements ConversationGateway {
|
||||
constructor(private readonly http: HttpInvoker) {}
|
||||
async readPage(
|
||||
|
||||
19
frontend/src/adapters/http/scheduledTaskGateway.test.ts
Normal file
19
frontend/src/adapters/http/scheduledTaskGateway.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { HttpInvoker } from "./httpInvoker";
|
||||
import { HttpScheduledTaskGateway } from "./requestResponseGateways";
|
||||
|
||||
describe("HttpScheduledTaskGateway", () => {
|
||||
it("forwards the shared HTTP command contract", async () => {
|
||||
const invoke = vi.fn().mockResolvedValue([]);
|
||||
const gateway = new HttpScheduledTaskGateway({ invoke } as unknown as HttpInvoker);
|
||||
await gateway.list("project-1");
|
||||
await gateway.setEnabled("project-1", "schedule-1", true);
|
||||
expect(invoke).toHaveBeenNthCalledWith(1, "list_scheduled_project_tasks", { projectId: "project-1" });
|
||||
expect(invoke).toHaveBeenNthCalledWith(2, "set_scheduled_project_task_enabled", {
|
||||
projectId: "project-1",
|
||||
scheduledTaskId: "schedule-1",
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -375,6 +375,9 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
...(payload.assignedConversationId
|
||||
? { assignedConversationId: payload.assignedConversationId }
|
||||
: {}),
|
||||
...(payload.transcriptUnavailableReason
|
||||
? { transcriptUnavailableReason: payload.transcriptUnavailableReason }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -404,6 +407,9 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
}
|
||||
: {}),
|
||||
scrollback: payload.scrollback ?? [],
|
||||
...(payload.transcriptUnavailableReason
|
||||
? { transcriptUnavailableReason: payload.transcriptUnavailableReason }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ import { TauriDeviceGateway } from "./device";
|
||||
import { TauriGitGateway } from "./git";
|
||||
import { TauriPermissionGateway } from "./permission";
|
||||
import { TauriWorkStateGateway } from "./workState";
|
||||
import { TauriScheduledTaskGateway } from "./scheduledTask";
|
||||
import { TauriConversationGateway } from "./conversation";
|
||||
import { TauriTicketGateway } from "./ticket";
|
||||
import { TauriWindowGateway } from "./window";
|
||||
@ -78,6 +79,7 @@ export function createTauriGateways(): Gateways {
|
||||
device: new TauriDeviceGateway(),
|
||||
permission: new TauriPermissionGateway(),
|
||||
workState: new TauriWorkStateGateway(),
|
||||
scheduledTask: new TauriScheduledTaskGateway(),
|
||||
conversation: new TauriConversationGateway(),
|
||||
ticket: new TauriTicketGateway(),
|
||||
window: new TauriWindowGateway(),
|
||||
@ -112,6 +114,7 @@ export {
|
||||
TauriGitGateway,
|
||||
TauriPermissionGateway,
|
||||
TauriWorkStateGateway,
|
||||
TauriScheduledTaskGateway,
|
||||
TauriConversationGateway,
|
||||
TauriTicketGateway,
|
||||
TauriWindowGateway,
|
||||
|
||||
@ -83,6 +83,7 @@ import type {
|
||||
ResumableAgent,
|
||||
ResolvedAgentPermissions,
|
||||
ResolvedAgentSystemPermissions,
|
||||
ScheduledProjectTask,
|
||||
ServerExposurePreview,
|
||||
ServerExposureSettings,
|
||||
Skill,
|
||||
@ -168,6 +169,8 @@ import type {
|
||||
SetProjectPluginEnabledInput,
|
||||
SaveOpenCodeProviderProfileInput,
|
||||
SendAgentChatOptions,
|
||||
ScheduledTaskDefinitionInput,
|
||||
ScheduledTaskGateway,
|
||||
SkillGateway,
|
||||
StoppedLiveAgent,
|
||||
SystemGateway,
|
||||
@ -3247,6 +3250,112 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
}
|
||||
}
|
||||
|
||||
export class MockScheduledTaskGateway implements ScheduledTaskGateway {
|
||||
private readonly tasks = new Map<string, ScheduledProjectTask[]>();
|
||||
private nextId = 1;
|
||||
|
||||
/** Test/dev helper for agent-originated pending proposals. */
|
||||
_seed(task: ScheduledProjectTask): void {
|
||||
this.tasks.set(task.projectId, [
|
||||
...(this.tasks.get(task.projectId) ?? []).filter((item) => item.id !== task.id),
|
||||
structuredClone(task),
|
||||
]);
|
||||
}
|
||||
|
||||
async list(projectId: string): Promise<ScheduledProjectTask[]> {
|
||||
return structuredClone(this.tasks.get(projectId) ?? []);
|
||||
}
|
||||
|
||||
async create(
|
||||
projectId: string,
|
||||
input: ScheduledTaskDefinitionInput,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
const now = Date.now();
|
||||
const task: ScheduledProjectTask = {
|
||||
id: `mock-scheduled-${this.nextId++}`,
|
||||
projectId,
|
||||
...structuredClone(input),
|
||||
approval: "approved",
|
||||
sandbox: "projectReadWrite",
|
||||
nextRunAtMs: input.enabled ? now + 3_600_000 : null,
|
||||
createdBy: { kind: "human" },
|
||||
updatedBy: { kind: "human" },
|
||||
createdAtMs: now,
|
||||
updatedAtMs: now,
|
||||
auditRevision: 1,
|
||||
};
|
||||
this.tasks.set(projectId, [...(this.tasks.get(projectId) ?? []), task]);
|
||||
return structuredClone(task);
|
||||
}
|
||||
|
||||
async update(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
auditRevision: number,
|
||||
input: ScheduledTaskDefinitionInput,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
const rows = this.tasks.get(projectId) ?? [];
|
||||
const index = rows.findIndex((task) => task.id === scheduledTaskId);
|
||||
if (index < 0) throw { code: "NOT_FOUND", message: "scheduled task not found" };
|
||||
if (rows[index].auditRevision !== auditRevision) {
|
||||
throw { code: "INVALID", message: "scheduled task revision conflict" };
|
||||
}
|
||||
const updated: ScheduledProjectTask = {
|
||||
...rows[index],
|
||||
...structuredClone(input),
|
||||
approval: "approved",
|
||||
nextRunAtMs: input.enabled ? Date.now() + 3_600_000 : null,
|
||||
updatedBy: { kind: "human" },
|
||||
updatedAtMs: Date.now(),
|
||||
auditRevision: auditRevision + 1,
|
||||
};
|
||||
rows[index] = updated;
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async delete(projectId: string, scheduledTaskId: string): Promise<void> {
|
||||
this.tasks.set(
|
||||
projectId,
|
||||
(this.tasks.get(projectId) ?? []).filter((task) => task.id !== scheduledTaskId),
|
||||
);
|
||||
}
|
||||
|
||||
async runNow(projectId: string, scheduledTaskId: string): Promise<ScheduledProjectTask> {
|
||||
const rows = this.tasks.get(projectId) ?? [];
|
||||
const task = rows.find((row) => row.id === scheduledTaskId);
|
||||
if (!task) throw { code: "NOT_FOUND", message: "scheduled task not found" };
|
||||
const updated = {
|
||||
...task,
|
||||
lastRunAtMs: Date.now(),
|
||||
lastTaskId: `mock-background-${scheduledTaskId}`,
|
||||
auditRevision: task.auditRevision + 1,
|
||||
};
|
||||
rows[rows.indexOf(task)] = updated;
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async setEnabled(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
enabled: boolean,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
const rows = this.tasks.get(projectId) ?? [];
|
||||
const task = rows.find((row) => row.id === scheduledTaskId);
|
||||
if (!task) throw { code: "NOT_FOUND", message: "scheduled task not found" };
|
||||
const updated: ScheduledProjectTask = {
|
||||
...task,
|
||||
enabled,
|
||||
approval: enabled ? "approved" : task.approval === "pending" ? "rejected" : task.approval,
|
||||
nextRunAtMs: enabled ? Date.now() + 3_600_000 : null,
|
||||
updatedBy: { kind: "human" },
|
||||
updatedAtMs: Date.now(),
|
||||
auditRevision: task.auditRevision + 1,
|
||||
};
|
||||
rows[rows.indexOf(task)] = updated;
|
||||
return structuredClone(updated);
|
||||
}
|
||||
}
|
||||
|
||||
export class MockWorkStateGateway implements WorkStateGateway {
|
||||
private states = new Map<string, ProjectWorkState>();
|
||||
private backgroundTaskStreams = new Map<
|
||||
@ -4854,6 +4963,7 @@ export function createMockGateways(): Gateways {
|
||||
device: new MockDeviceGateway(),
|
||||
permission: new MockPermissionGateway(),
|
||||
workState: new MockWorkStateGateway(),
|
||||
scheduledTask: new MockScheduledTaskGateway(),
|
||||
conversation: new MockConversationGateway(),
|
||||
ticket: new MockTicketGateway(systemGateway),
|
||||
window: new MockWindowGateway(),
|
||||
|
||||
@ -36,6 +36,7 @@ describe("createMockGateways", () => {
|
||||
"profile",
|
||||
"project",
|
||||
"remote",
|
||||
"scheduledTask",
|
||||
"skill",
|
||||
"system",
|
||||
"template",
|
||||
|
||||
39
frontend/src/adapters/scheduledTask.test.ts
Normal file
39
frontend/src/adapters/scheduledTask.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const invoke = vi.fn();
|
||||
vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => invoke(...args) }));
|
||||
|
||||
import { TauriScheduledTaskGateway } from "./scheduledTask";
|
||||
|
||||
const input = {
|
||||
name: "Nettoyage",
|
||||
command: { command: "find", args: ["dist", "-delete"], cwd: "." },
|
||||
schedule: { kind: "interval" as const, everyMinutes: 60 },
|
||||
timezone: "Europe/Paris",
|
||||
enabled: true,
|
||||
timeoutMs: 60_000,
|
||||
};
|
||||
|
||||
describe("TauriScheduledTaskGateway", () => {
|
||||
beforeEach(() => invoke.mockReset().mockResolvedValue({}));
|
||||
|
||||
it("uses the backend command envelopes for list/create/update", async () => {
|
||||
const gateway = new TauriScheduledTaskGateway();
|
||||
await gateway.list("p1");
|
||||
await gateway.create("p1", input);
|
||||
await gateway.update("p1", "s1", 4, input);
|
||||
expect(invoke).toHaveBeenNthCalledWith(1, "list_scheduled_project_tasks", { projectId: "p1" });
|
||||
expect(invoke).toHaveBeenNthCalledWith(2, "create_scheduled_project_task", { request: { projectId: "p1", ...input } });
|
||||
expect(invoke).toHaveBeenNthCalledWith(3, "update_scheduled_project_task", { request: { projectId: "p1", scheduledTaskId: "s1", auditRevision: 4, ...input } });
|
||||
});
|
||||
|
||||
it("uses flat identity arguments for actions", async () => {
|
||||
const gateway = new TauriScheduledTaskGateway();
|
||||
await gateway.runNow("p1", "s1");
|
||||
await gateway.setEnabled("p1", "s1", false);
|
||||
await gateway.delete("p1", "s1");
|
||||
expect(invoke).toHaveBeenNthCalledWith(1, "run_scheduled_project_task_now", { projectId: "p1", scheduledTaskId: "s1" });
|
||||
expect(invoke).toHaveBeenNthCalledWith(2, "set_scheduled_project_task_enabled", { projectId: "p1", scheduledTaskId: "s1", enabled: false });
|
||||
expect(invoke).toHaveBeenNthCalledWith(3, "delete_scheduled_project_task", { projectId: "p1", scheduledTaskId: "s1" });
|
||||
});
|
||||
});
|
||||
45
frontend/src/adapters/scheduledTask.ts
Normal file
45
frontend/src/adapters/scheduledTask.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type { ScheduledProjectTask } from "@/domain";
|
||||
import type { ScheduledTaskDefinitionInput, ScheduledTaskGateway } from "@/ports";
|
||||
|
||||
export class TauriScheduledTaskGateway implements ScheduledTaskGateway {
|
||||
list(projectId: string): Promise<ScheduledProjectTask[]> {
|
||||
return invoke("list_scheduled_project_tasks", { projectId });
|
||||
}
|
||||
|
||||
create(projectId: string, input: ScheduledTaskDefinitionInput): Promise<ScheduledProjectTask> {
|
||||
return invoke("create_scheduled_project_task", { request: { projectId, ...input } });
|
||||
}
|
||||
|
||||
update(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
auditRevision: number,
|
||||
input: ScheduledTaskDefinitionInput,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
return invoke("update_scheduled_project_task", {
|
||||
request: { projectId, scheduledTaskId, auditRevision, ...input },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(projectId: string, scheduledTaskId: string): Promise<void> {
|
||||
await invoke("delete_scheduled_project_task", { projectId, scheduledTaskId });
|
||||
}
|
||||
|
||||
runNow(projectId: string, scheduledTaskId: string): Promise<ScheduledProjectTask> {
|
||||
return invoke("run_scheduled_project_task_now", { projectId, scheduledTaskId });
|
||||
}
|
||||
|
||||
setEnabled(
|
||||
projectId: string,
|
||||
scheduledTaskId: string,
|
||||
enabled: boolean,
|
||||
): Promise<ScheduledProjectTask> {
|
||||
return invoke("set_scheduled_project_task_enabled", {
|
||||
projectId,
|
||||
scheduledTaskId,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -114,12 +114,24 @@ function normalizeBackgroundTask(
|
||||
task.conversationId ?? task.conversation_id,
|
||||
);
|
||||
const parentTaskId = optionalString(task.parentTaskId ?? task.parent_task_id);
|
||||
const rawOrigin = isRecord(task.origin) ? task.origin : null;
|
||||
const origin = rawOrigin?.kind === "scheduled"
|
||||
? {
|
||||
kind: "scheduled" as const,
|
||||
scheduledTaskId: stringValue(rawOrigin.scheduledTaskId ?? rawOrigin.scheduled_task_id),
|
||||
plannedAtMs: numberValue(rawOrigin.plannedAtMs ?? rawOrigin.planned_at_ms),
|
||||
}
|
||||
: rawOrigin?.kind === "manual"
|
||||
? { kind: "manual" as const }
|
||||
: undefined;
|
||||
const createdAtMs = task.createdAtMs ?? task.created_at_ms;
|
||||
return {
|
||||
taskId: stringValue(task.taskId, `legacy-task-${index}`),
|
||||
ownerAgentId: stringValue(task.ownerAgentId, fallbackAgentId),
|
||||
projectId: stringValue(task.projectId),
|
||||
...(parentTaskId ? { parentTaskId } : {}),
|
||||
kind: stringValue(task.kind, "background"),
|
||||
...(origin ? { origin } : {}),
|
||||
status: normalizeBackgroundStatus(task.status ?? task.state),
|
||||
exitCode: nullableNumber(task.exitCode),
|
||||
summary: nullableString(task.summary),
|
||||
@ -129,6 +141,7 @@ function normalizeBackgroundTask(
|
||||
...(targetAgentId ? { targetAgentId } : {}),
|
||||
...(conversationId ? { conversationId } : {}),
|
||||
updatedAtMs: numberValue(task.updatedAtMs),
|
||||
...(typeof createdAtMs === "number" ? { createdAtMs } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user