mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
Correct: `state: "updated"` mapped to no `OrchState`, so the channel carried a value no consumer could interpret. Reading the row back only on the no-status path, and publishing nothing when no row matched, both match what the A2A side already does. --- Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs. - `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK - complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline - 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately. Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written. ⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch). Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
import { getDbInstance } from "@/lib/db/core.ts";
|
|
import { emit } from "@/lib/events/eventBus";
|
|
|
|
/**
|
|
* Publish an `agent.task.updated` transition for the orchestration canvas (Fase 2, Task B2).
|
|
* Best-effort: a listener throwing must never break the DB write path that triggered it.
|
|
*/
|
|
function emitAgentTaskUpdated(source: "cloud-agent" | "a2a", taskId: string, state: string): void {
|
|
try {
|
|
emit("agent.task.updated", { source, taskId, state, timestamp: Date.now() });
|
|
} catch {
|
|
/* listeners never derail the write path */
|
|
}
|
|
}
|
|
|
|
export interface CloudAgentTaskRow {
|
|
id: string;
|
|
provider_id: string;
|
|
external_id: string | null;
|
|
status: string;
|
|
prompt: string;
|
|
source: string;
|
|
options: string;
|
|
result: string | null;
|
|
activities: string;
|
|
error: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
completed_at: string | null;
|
|
}
|
|
|
|
export function createCloudAgentTaskTable(): void {
|
|
const db = getDbInstance();
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS cloud_agent_tasks (
|
|
id TEXT PRIMARY KEY,
|
|
provider_id TEXT NOT NULL,
|
|
external_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'queued',
|
|
prompt TEXT NOT NULL,
|
|
source TEXT NOT NULL,
|
|
options TEXT DEFAULT '{}',
|
|
result TEXT,
|
|
activities TEXT DEFAULT '[]',
|
|
error TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
completed_at TEXT
|
|
)
|
|
`);
|
|
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_provider
|
|
ON cloud_agent_tasks(provider_id)
|
|
`);
|
|
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_status
|
|
ON cloud_agent_tasks(status)
|
|
`);
|
|
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_cloud_agent_tasks_created
|
|
ON cloud_agent_tasks(created_at DESC)
|
|
`);
|
|
}
|
|
|
|
export function insertCloudAgentTask(task: CloudAgentTaskRow): void {
|
|
const db = getDbInstance();
|
|
db.prepare(
|
|
`
|
|
INSERT INTO cloud_agent_tasks (
|
|
id, provider_id, external_id, status, prompt, source,
|
|
options, result, activities, error, created_at, updated_at, completed_at
|
|
) VALUES (
|
|
@id, @provider_id, @external_id, @status, @prompt, @source,
|
|
@options, @result, @activities, @error, @created_at, @updated_at, @completed_at
|
|
)
|
|
`
|
|
).run(task);
|
|
emitAgentTaskUpdated("cloud-agent", task.id, task.status);
|
|
}
|
|
|
|
// Whitelist of allowed columns for update operations
|
|
const ALLOWED_UPDATE_COLUMNS = new Set([
|
|
"status",
|
|
"prompt",
|
|
"source",
|
|
"options",
|
|
"result",
|
|
"activities",
|
|
"error",
|
|
"completed_at",
|
|
]);
|
|
|
|
export function updateCloudAgentTask(
|
|
id: string,
|
|
updates: Partial<Omit<CloudAgentTaskRow, "id">>
|
|
): void {
|
|
const db = getDbInstance();
|
|
|
|
// Validate keys against whitelist to prevent SQL injection
|
|
const validUpdates: Partial<Omit<CloudAgentTaskRow, "id">> = {};
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (ALLOWED_UPDATE_COLUMNS.has(key)) {
|
|
(validUpdates as Record<string, unknown>)[key] = value;
|
|
}
|
|
}
|
|
|
|
const fields = Object.keys(validUpdates)
|
|
.map((key) => `${key} = @${key}`)
|
|
.join(", ");
|
|
|
|
if (!fields) return; // No valid updates
|
|
|
|
db.prepare(
|
|
`
|
|
UPDATE cloud_agent_tasks
|
|
SET ${fields}, updated_at = datetime('now')
|
|
WHERE id = @id
|
|
`
|
|
).run({ id, ...validUpdates });
|
|
// Publish the row's real status: an update that only touches result/activities/error must
|
|
// not fabricate a state the canvas has never heard of. No row means nothing was written.
|
|
const state = (validUpdates.status as string | undefined) ?? getCloudAgentTaskById(id)?.status;
|
|
if (state) emitAgentTaskUpdated("cloud-agent", id, state);
|
|
}
|
|
|
|
export function getCloudAgentTaskById(id: string): CloudAgentTaskRow | null {
|
|
const db = getDbInstance();
|
|
return db
|
|
.prepare("SELECT * FROM cloud_agent_tasks WHERE id = ?")
|
|
.get(id) as CloudAgentTaskRow | null;
|
|
}
|
|
|
|
export function getCloudAgentTasksByProvider(providerId: string, limit = 50): CloudAgentTaskRow[] {
|
|
const db = getDbInstance();
|
|
return db
|
|
.prepare(
|
|
"SELECT * FROM cloud_agent_tasks WHERE provider_id = ? ORDER BY created_at DESC LIMIT ?"
|
|
)
|
|
.all(providerId, limit) as CloudAgentTaskRow[];
|
|
}
|
|
|
|
export function getCloudAgentTasksByStatus(status: string, limit = 50): CloudAgentTaskRow[] {
|
|
const db = getDbInstance();
|
|
return db
|
|
.prepare("SELECT * FROM cloud_agent_tasks WHERE status = ? ORDER BY created_at DESC LIMIT ?")
|
|
.all(status, limit) as CloudAgentTaskRow[];
|
|
}
|
|
|
|
export function getAllCloudAgentTasks(limit = 100): CloudAgentTaskRow[] {
|
|
const db = getDbInstance();
|
|
return db
|
|
.prepare("SELECT * FROM cloud_agent_tasks ORDER BY created_at DESC LIMIT ?")
|
|
.all(limit) as CloudAgentTaskRow[];
|
|
}
|
|
|
|
export function deleteCloudAgentTask(id: string): void {
|
|
const db = getDbInstance();
|
|
db.prepare("DELETE FROM cloud_agent_tasks WHERE id = ?").run(id);
|
|
}
|