From c47050964a6de52f07046cd323fb3cce34ddb1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 16 Apr 2026 23:07:01 -0400 Subject: [PATCH] refactor: dedupe heartbeat-title check; JOIN thread visibility; typed skill service guard (P2-43, P2-44, P2-45) --- src/main/api-server/routes-tasks.ts | 3 +- src/main/database/threads.ts | 8 +++-- src/main/ipc-core-task-graph-handlers.ts | 3 +- src/main/ipc-skill-handlers.ts | 43 ++++++++++++++---------- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/main/api-server/routes-tasks.ts b/src/main/api-server/routes-tasks.ts index 240d5e58..64739ee0 100644 --- a/src/main/api-server/routes-tasks.ts +++ b/src/main/api-server/routes-tasks.ts @@ -3,6 +3,7 @@ import { taskInputSchema } from "@shared/contracts"; import { IMPORTANT_DOC_TYPES } from "@shared/document-categories"; import { canAssignTask, canUpdateTask } from "../agent-permissions"; import { inferTaskType } from "../connector-matching"; +import { isHeartbeatTaskTitle } from "../orchestration/heartbeat-task"; import { validateTaskCreation } from "../task-quality-gates"; import { requireParam, type AgentApiServer } from "./server-core"; @@ -303,7 +304,7 @@ export function registerTaskRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Current agent is not allowed to assign this task to the selected agent" }); return; } - const isHeartbeat = input.title.startsWith("[Heartbeat]"); + const isHeartbeat = isHeartbeatTaskTitle(input.title); const taskValidation = validateTaskCreation({ title: input.title, description: input.description ?? "" }, isHeartbeat); if (!taskValidation.valid) { server.json(res, 422, { error: taskValidation.errors.join("; ") }); diff --git a/src/main/database/threads.ts b/src/main/database/threads.ts index 04f6265f..4c05b1d8 100644 --- a/src/main/database/threads.ts +++ b/src/main/database/threads.ts @@ -132,13 +132,15 @@ export function buildThreadMethods(getDb: () => DatabaseSync, readAll: ReadAll) companyId: string, options: { limit?: number; offset?: number } = {}, ): ConversationEventRecord[] { - if (!getThread(threadId, companyId)) return []; const limit = options.limit ?? 50; const offset = options.offset ?? 0; const rows = readAll( `SELECT id, thread_id, actor_id, actor_type, event_type, body, metadata_json, created_at - FROM conversation_events WHERE thread_id = ? ORDER BY created_at ASC LIMIT ? OFFSET ?`, - threadId, limit, offset, + FROM conversation_events + WHERE thread_id = ? + AND thread_id IN (SELECT id FROM conversation_threads WHERE id = ? AND company_id = ?) + ORDER BY created_at ASC LIMIT ? OFFSET ?`, + threadId, threadId, companyId, limit, offset, ); return rows.map((r) => ({ id: r.id as string, diff --git a/src/main/ipc-core-task-graph-handlers.ts b/src/main/ipc-core-task-graph-handlers.ts index f9bcec71..702cff02 100644 --- a/src/main/ipc-core-task-graph-handlers.ts +++ b/src/main/ipc-core-task-graph-handlers.ts @@ -6,6 +6,7 @@ import { taskInputSchema, } from "@shared/contracts"; import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common"; +import { isHeartbeatTaskTitle } from "./orchestration/heartbeat-task"; import { validateTaskCreation, checkCapabilityMatch } from "./task-quality-gates"; export function registerCoreTaskGraphHandlers({ @@ -100,7 +101,7 @@ export function registerCoreTaskGraphHandlers({ } const isNew = !parsed.id; if (isNew) { - const isHeartbeat = parsed.title.startsWith("[Heartbeat]"); + const isHeartbeat = isHeartbeatTaskTitle(parsed.title); const validation = validateTaskCreation(parsed, isHeartbeat); if (!validation.valid) { return fail("TASK_VALIDATION_FAILED", validation.errors.join("; ")); diff --git a/src/main/ipc-skill-handlers.ts b/src/main/ipc-skill-handlers.ts index d2a77567..606f3b94 100644 --- a/src/main/ipc-skill-handlers.ts +++ b/src/main/ipc-skill-handlers.ts @@ -29,11 +29,13 @@ export function registerSkillHandlers({ skillService, db, }: SkillHandlerDependencies) { - function requireSkillService() { + function requireSkillService(): + | { service: NonNullable; error?: undefined } + | { service?: undefined; error: ReturnType } { if (!skillService) { - return fail("SERVICE_NOT_READY", "Skill service is not initialized."); + return { error: fail("SERVICE_NOT_READY", "Skill service is not initialized.") }; } - return skillService; + return { service: skillService }; } // ---- Import skill from source (GitHub URL, local path, raw URL, skills.sh) ---- @@ -41,8 +43,9 @@ export function registerSkillHandlers({ const parsed = importSkillSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; const result = await svc.importFromSource(parsed.companyId, parsed.source); if (result.ok) { publishDomainChanged(); @@ -55,8 +58,9 @@ export function registerSkillHandlers({ const parsed = listSkillsSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; // Scan workspace .agents/skills/ directories for new skills before listing try { await svc.scanWorkspaceSkills(parsed.companyId); @@ -71,8 +75,9 @@ export function registerSkillHandlers({ const parsed = getSkillDetailSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; return svc.getDetail(parsed.companyId, parsed.skillId); }); @@ -81,8 +86,9 @@ export function registerSkillHandlers({ const parsed = deleteSkillSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; const result = await svc.deleteSkill(parsed.companyId, parsed.skillId); if (result.ok) { publishDomainChanged(); @@ -95,8 +101,9 @@ export function registerSkillHandlers({ const parsed = assignSkillToAgentSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; const result = svc.assignToAgent(parsed.companyId, parsed.agentId, parsed.skillId); if (result.ok) { publishDomainChanged(); @@ -109,8 +116,9 @@ export function registerSkillHandlers({ const parsed = removeSkillFromAgentSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; const result = svc.removeFromAgent(parsed.companyId, parsed.agentId, parsed.skillId); if (result.ok) { publishDomainChanged(); @@ -123,8 +131,9 @@ export function registerSkillHandlers({ const parsed = checkSkillUpdateSchema.parse(payload); const companyError = ensureCompanyExists(parsed.companyId); if (companyError) return companyError; - const svc = requireSkillService(); - if ("ok" in svc) return svc; + const svcResult = requireSkillService(); + if (svcResult.error) return svcResult.error; + const svc = svcResult.service; return svc.checkUpdateStatus(parsed.companyId, parsed.skillId); });