feat(client): persist digital governance events
This commit is contained in:
@@ -2,11 +2,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
db: null as TestDb | null,
|
||||
ensureSchemaCalls: 0,
|
||||
settings: new Map<string, unknown>(),
|
||||
}));
|
||||
|
||||
vi.mock("../../db", () => ({
|
||||
ensureDigitalEmployeeSchema: () => true,
|
||||
ensureDigitalEmployeeSchema: () => {
|
||||
mockState.ensureSchemaCalls += 1;
|
||||
return true;
|
||||
},
|
||||
getDb: () => mockState.db,
|
||||
readSetting: (key: string) => mockState.settings.get(key) ?? null,
|
||||
writeSetting: (key: string, value: unknown) => mockState.settings.set(key, value),
|
||||
@@ -17,6 +21,7 @@ describe("digital employee state service", () => {
|
||||
vi.resetModules();
|
||||
vi.setSystemTime(new Date("2026-06-07T08:00:00.000Z"));
|
||||
mockState.db = new TestDb();
|
||||
mockState.ensureSchemaCalls = 0;
|
||||
mockState.settings = new Map<string, unknown>();
|
||||
});
|
||||
|
||||
@@ -75,6 +80,7 @@ describe("digital employee state service", () => {
|
||||
entity_id: "computer-chat-run-req-1:approval:approval-1",
|
||||
status: "pending",
|
||||
});
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("updates formal approval records when UI approval decisions are saved", async () => {
|
||||
@@ -135,9 +141,112 @@ describe("digital employee state service", () => {
|
||||
entity_id: "approval-1",
|
||||
status: "pending",
|
||||
});
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("records governance commands as formal runtime records", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
const ids = recordDigitalEmployeeGovernanceCommand({
|
||||
commandKind: "create_plan",
|
||||
commandId: "command-1",
|
||||
correlationId: "corr-1",
|
||||
accepted: true,
|
||||
message: "已创建计划草稿",
|
||||
payload: {
|
||||
title: "生成今日客户跟进计划",
|
||||
objective: "把今日客户跟进事项整理成可执行计划",
|
||||
},
|
||||
result: {
|
||||
plan_id: "plan-customer-followup",
|
||||
status: "draft",
|
||||
},
|
||||
});
|
||||
|
||||
expect(ids).toEqual({
|
||||
planId: "plan-customer-followup",
|
||||
taskId: "governance-task-command-1",
|
||||
runId: "governance-run-command-1",
|
||||
});
|
||||
expect(mockState.db?.plans.get("plan-customer-followup")).toMatchObject({
|
||||
id: "plan-customer-followup",
|
||||
title: "生成今日客户跟进计划",
|
||||
status: "draft",
|
||||
sync_status: "pending",
|
||||
});
|
||||
expect(mockState.db?.tasks.get("governance-task-command-1")).toMatchObject({
|
||||
plan_id: "plan-customer-followup",
|
||||
title: "创建数字员工计划",
|
||||
assigned_skill_id: "qimingclaw-governance-command",
|
||||
});
|
||||
expect(mockState.db?.runs.get("governance-run-command-1")).toMatchObject({
|
||||
plan_id: "plan-customer-followup",
|
||||
task_id: "governance-task-command-1",
|
||||
status: "draft",
|
||||
finished_at: "2026-06-07T08:00:00.000Z",
|
||||
});
|
||||
expect(mockState.db?.events.get("governance-run-command-1:create_plan:command-1")).toMatchObject({
|
||||
kind: "governance_create_plan",
|
||||
message: "已创建计划草稿",
|
||||
});
|
||||
expect(mockState.db?.outbox.get("plan:upsert:plan-customer-followup")).toMatchObject({
|
||||
entity_type: "plan",
|
||||
entity_id: "plan-customer-followup",
|
||||
status: "pending",
|
||||
});
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
interface TestPlanRow {
|
||||
id: string;
|
||||
title: string;
|
||||
objective: string;
|
||||
status: string;
|
||||
payload: string;
|
||||
sync_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TestTaskRow {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
assigned_skill_id: string;
|
||||
payload: string;
|
||||
sync_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TestRunRow {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
task_id: string;
|
||||
status: string;
|
||||
started_at: string;
|
||||
finished_at: string | null;
|
||||
payload: string;
|
||||
sync_status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface TestEventRow {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
task_id: string;
|
||||
run_id: string;
|
||||
kind: string;
|
||||
message: string;
|
||||
payload: string;
|
||||
sync_status: string;
|
||||
occurred_at: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface TestArtifactRow {
|
||||
id: string;
|
||||
plan_id: string;
|
||||
@@ -178,6 +287,10 @@ interface TestOutboxRow {
|
||||
}
|
||||
|
||||
class TestDb {
|
||||
plans = new Map<string, TestPlanRow>();
|
||||
tasks = new Map<string, TestTaskRow>();
|
||||
runs = new Map<string, TestRunRow>();
|
||||
events = new Map<string, TestEventRow>();
|
||||
artifacts = new Map<string, TestArtifactRow>();
|
||||
approvals = new Map<string, TestApprovalRow>();
|
||||
outbox = new Map<string, TestOutboxRow>();
|
||||
@@ -207,11 +320,126 @@ class TestDb {
|
||||
}
|
||||
|
||||
private run(sql: string, args: unknown[]): unknown {
|
||||
if (sql.startsWith("INSERT INTO digital_plans")) return { changes: 1 };
|
||||
if (sql.startsWith("INSERT INTO digital_tasks")) return { changes: 1 };
|
||||
if (sql.startsWith("INSERT INTO digital_runs")) return { changes: 1 };
|
||||
if (sql.startsWith("UPDATE digital_runs")) return { changes: 1 };
|
||||
if (sql.startsWith("INSERT OR IGNORE INTO digital_events")) return { changes: 1 };
|
||||
if (sql.startsWith("INSERT INTO digital_plans")) {
|
||||
const [id, title, objective, status, payload, createdAt, updatedAt] = args as [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
const previous = this.plans.get(id);
|
||||
this.plans.set(id, {
|
||||
id,
|
||||
title,
|
||||
objective,
|
||||
status,
|
||||
payload,
|
||||
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
|
||||
created_at: previous?.created_at ?? createdAt,
|
||||
updated_at: updatedAt,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT INTO digital_tasks")) {
|
||||
const [id, planId, title, status, assignedSkillId, payload, createdAt, updatedAt] = args as [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
const previous = this.tasks.get(id);
|
||||
this.tasks.set(id, {
|
||||
id,
|
||||
plan_id: planId,
|
||||
title,
|
||||
status,
|
||||
assigned_skill_id: assignedSkillId,
|
||||
payload,
|
||||
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
|
||||
created_at: previous?.created_at ?? createdAt,
|
||||
updated_at: updatedAt,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT INTO digital_runs")) {
|
||||
const [id, planId, taskId, status, startedAt, payload, createdAt, updatedAt] = args as [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
const previous = this.runs.get(id);
|
||||
this.runs.set(id, {
|
||||
id,
|
||||
plan_id: planId,
|
||||
task_id: taskId,
|
||||
status,
|
||||
started_at: previous?.started_at ?? startedAt,
|
||||
finished_at: previous?.finished_at ?? null,
|
||||
payload,
|
||||
sync_status: previous?.sync_status === "synced" ? "pending" : previous?.sync_status ?? "pending",
|
||||
created_at: previous?.created_at ?? createdAt,
|
||||
updated_at: updatedAt,
|
||||
});
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("UPDATE digital_runs")) {
|
||||
const [status, finishedAt, updatedAt, id] = args as [string, string, string, string];
|
||||
const previous = this.runs.get(id);
|
||||
if (previous) {
|
||||
this.runs.set(id, {
|
||||
...previous,
|
||||
status,
|
||||
finished_at: finishedAt,
|
||||
updated_at: updatedAt,
|
||||
sync_status: "pending",
|
||||
});
|
||||
}
|
||||
return { changes: previous ? 1 : 0 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT OR IGNORE INTO digital_events")) {
|
||||
const [id, planId, taskId, runId, kind, message, payload, occurredAt, createdAt] = args as [
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
string,
|
||||
];
|
||||
if (!this.events.has(id)) {
|
||||
this.events.set(id, {
|
||||
id,
|
||||
plan_id: planId,
|
||||
task_id: taskId,
|
||||
run_id: runId,
|
||||
kind,
|
||||
message,
|
||||
payload,
|
||||
sync_status: "pending",
|
||||
occurred_at: occurredAt,
|
||||
created_at: createdAt,
|
||||
});
|
||||
}
|
||||
return { changes: 1 };
|
||||
}
|
||||
|
||||
if (sql.startsWith("INSERT INTO digital_artifacts")) {
|
||||
const [id, planId, taskId, runId, kind, name, uri, payload, createdAt] = args as [
|
||||
|
||||
@@ -203,6 +203,19 @@ export interface DigitalEmployeeRuntimeEventRecord {
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeGovernanceCommandRecord {
|
||||
commandKind: string;
|
||||
commandId: string;
|
||||
correlationId?: string;
|
||||
planId?: string | null;
|
||||
scheduleId?: string | null;
|
||||
accepted?: boolean;
|
||||
message?: string;
|
||||
error?: string | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
result?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
function defaultState(): DigitalEmployeeState {
|
||||
return {
|
||||
version: 1,
|
||||
@@ -681,6 +694,140 @@ export function recordDigitalEmployeeRuntimeEvent(
|
||||
tx();
|
||||
}
|
||||
|
||||
export function recordDigitalEmployeeGovernanceCommand(
|
||||
command: DigitalEmployeeGovernanceCommandRecord,
|
||||
): { planId: string; taskId: string; runId: string } {
|
||||
const now = new Date().toISOString();
|
||||
const commandId = command.commandId || `governance-${Date.now()}`;
|
||||
const result = objectRecord(command.result) ?? {};
|
||||
const payload = objectRecord(command.payload) ?? {};
|
||||
const planId = stringValue(command.planId)
|
||||
|| stringValue(result.plan_id)
|
||||
|| stringValue(payload.plan_id)
|
||||
|| `governance-plan-${safeId(commandId)}`;
|
||||
const ids = {
|
||||
planId,
|
||||
taskId: `governance-task-${safeId(commandId)}`,
|
||||
runId: `governance-run-${safeId(commandId)}`,
|
||||
};
|
||||
const title = stringValue(payload.title ?? result.title)
|
||||
|| governanceCommandTitle(command.commandKind);
|
||||
const status = governanceCommandStatus(command);
|
||||
const payloadEnvelope = {
|
||||
source: "qimingclaw-governance-command",
|
||||
commandKind: command.commandKind,
|
||||
commandId,
|
||||
correlationId: command.correlationId,
|
||||
planId,
|
||||
scheduleId: command.scheduleId || stringValue(result.schedule_id) || null,
|
||||
accepted: command.accepted !== false,
|
||||
message: command.message,
|
||||
error: command.error,
|
||||
payload,
|
||||
result,
|
||||
};
|
||||
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db) return ids;
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
upsertPlan({
|
||||
id: ids.planId,
|
||||
title,
|
||||
objective: stringValue(payload.objective ?? payload.description ?? payload.requested_action)
|
||||
|| command.message
|
||||
|| title,
|
||||
status,
|
||||
payload: payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
upsertTask({
|
||||
id: ids.taskId,
|
||||
planId: ids.planId,
|
||||
title: governanceCommandTitle(command.commandKind),
|
||||
status,
|
||||
assignedSkillId: "qimingclaw-governance-command",
|
||||
payload: payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
upsertRun({
|
||||
id: ids.runId,
|
||||
planId: ids.planId,
|
||||
taskId: ids.taskId,
|
||||
status,
|
||||
payload: payloadEnvelope,
|
||||
now,
|
||||
});
|
||||
if (status !== "running" && status !== "pending") {
|
||||
finishRun(ids.runId, status, now);
|
||||
}
|
||||
persistPayloadArtifactsAndApprovals(ids, payloadEnvelope, now);
|
||||
insertEvent(
|
||||
{
|
||||
event_id: `${ids.runId}:${command.commandKind}:${safeId(commandId)}`,
|
||||
kind: `governance_${command.commandKind}`,
|
||||
occurred_at: now,
|
||||
message: command.message || `${governanceCommandTitle(command.commandKind)} 已记录`,
|
||||
plan_id: ids.planId,
|
||||
task_id: ids.taskId,
|
||||
},
|
||||
ids.runId,
|
||||
payloadEnvelope,
|
||||
);
|
||||
});
|
||||
tx();
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
function governanceCommandTitle(commandKind: string): string {
|
||||
switch (commandKind) {
|
||||
case "create_plan":
|
||||
return "创建数字员工计划";
|
||||
case "submit_plan":
|
||||
return "提交数字员工计划";
|
||||
case "approve_plan":
|
||||
return "批准数字员工计划";
|
||||
case "activate_plan":
|
||||
return "激活数字员工计划";
|
||||
case "create_schedule":
|
||||
return "创建数字员工定时任务";
|
||||
case "run_schedule_now":
|
||||
return "立即运行数字员工计划";
|
||||
case "pause_plan":
|
||||
return "暂停数字员工计划";
|
||||
case "cancel_plan":
|
||||
return "取消数字员工计划";
|
||||
default:
|
||||
return `数字员工治理命令:${commandKind || "unknown"}`;
|
||||
}
|
||||
}
|
||||
|
||||
function governanceCommandStatus(
|
||||
command: DigitalEmployeeGovernanceCommandRecord,
|
||||
): string {
|
||||
if (command.accepted === false || command.error) return "failed";
|
||||
const resultStatus = stringValue(command.result?.status);
|
||||
if (resultStatus) return resultStatus;
|
||||
switch (command.commandKind) {
|
||||
case "activate_plan":
|
||||
case "run_schedule_now":
|
||||
return "running";
|
||||
case "create_plan":
|
||||
return "draft";
|
||||
case "submit_plan":
|
||||
return "reviewing";
|
||||
case "approve_plan":
|
||||
return "approved";
|
||||
case "pause_plan":
|
||||
return "paused";
|
||||
case "cancel_plan":
|
||||
return "cancelled";
|
||||
default:
|
||||
return "completed";
|
||||
}
|
||||
}
|
||||
|
||||
function persistSnapshotTables(
|
||||
snapshot: DigitalEmployeeSnapshot,
|
||||
event: DigitalEmployeeStateEvent | null,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
db: null as TestDb | null,
|
||||
ensureSchemaCalls: 0,
|
||||
settings: new Map<string, unknown>(),
|
||||
fetch: vi.fn(),
|
||||
}));
|
||||
@@ -20,7 +21,10 @@ vi.mock("../system/deviceId", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../db", () => ({
|
||||
ensureDigitalEmployeeSchema: () => true,
|
||||
ensureDigitalEmployeeSchema: () => {
|
||||
mockState.ensureSchemaCalls += 1;
|
||||
return true;
|
||||
},
|
||||
getDb: () => mockState.db,
|
||||
readSetting: (key: string) => mockState.settings.get(key) ?? null,
|
||||
}));
|
||||
@@ -31,6 +35,7 @@ describe("digital employee sync service", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-06-07T08:00:00.000Z"));
|
||||
mockState.fetch = vi.fn();
|
||||
mockState.ensureSchemaCalls = 0;
|
||||
vi.stubGlobal("fetch", mockState.fetch);
|
||||
mockState.settings = new Map<string, unknown>([
|
||||
["step1_config", { serverHost: "https://manage.example.com" }],
|
||||
@@ -62,6 +67,7 @@ describe("digital employee sync service", () => {
|
||||
status: "pending",
|
||||
attempts: 0,
|
||||
});
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("marks entity-only acknowledgements as synced", async () => {
|
||||
@@ -103,6 +109,7 @@ describe("digital employee sync service", () => {
|
||||
error: null,
|
||||
}),
|
||||
]);
|
||||
expect(mockState.ensureSchemaCalls).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps unacknowledged rows retryable after a partial backend ack", async () => {
|
||||
@@ -146,6 +153,11 @@ describe("digital employee sync service", () => {
|
||||
});
|
||||
expect(status.recentFailures[0]).toMatchObject({
|
||||
id: "plan:upsert:plan-2",
|
||||
entitySummary: {
|
||||
title: "同步测试计划 plan-2",
|
||||
status: "pending",
|
||||
summary: "用于验证同步失败诊断的业务摘要",
|
||||
},
|
||||
dueForRetry: false,
|
||||
managementRecordUrl:
|
||||
"https://manage.example.com/system/content/content-digital-employee-sync?device_id=device-001&entity_type=plan&entity_id=plan-2&outbox_id=plan%3Aupsert%3Aplan-2",
|
||||
@@ -216,6 +228,10 @@ function insertPlan(id: string): void {
|
||||
function insertEntity(entityType: string, id: string): void {
|
||||
entityMap(entityType)?.set(id, {
|
||||
id,
|
||||
title: `同步测试计划 ${id}`,
|
||||
status: "pending",
|
||||
objective: "用于验证同步失败诊断的业务摘要",
|
||||
payload: "{}",
|
||||
remote_id: null,
|
||||
sync_status: "pending",
|
||||
sync_error: null,
|
||||
@@ -292,6 +308,10 @@ interface TestOutboxRow {
|
||||
|
||||
interface TestSyncEntityRow {
|
||||
id: string;
|
||||
title?: string;
|
||||
status?: string;
|
||||
objective?: string | null;
|
||||
payload?: string;
|
||||
remote_id: string | null;
|
||||
sync_status: string;
|
||||
sync_error: string | null;
|
||||
@@ -402,6 +422,11 @@ class TestDb {
|
||||
}
|
||||
|
||||
private get(sql: string, args: unknown[]): unknown {
|
||||
if (sql.startsWith("SELECT title, status, objective, payload FROM digital_plans")) {
|
||||
const [id] = args as [string];
|
||||
return this.plans.get(id);
|
||||
}
|
||||
|
||||
if (
|
||||
sql.includes("COUNT(*) AS count FROM digital_sync_outbox WHERE status = ?")
|
||||
) {
|
||||
|
||||
@@ -66,6 +66,7 @@ export interface DigitalEmployeeSyncFailure {
|
||||
id: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
entitySummary: DigitalEmployeeSyncEntitySummary | null;
|
||||
operation: string;
|
||||
attempts: number;
|
||||
lastAttemptAt: string | null;
|
||||
@@ -78,6 +79,12 @@ export interface DigitalEmployeeSyncFailure {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DigitalEmployeeSyncEntitySummary {
|
||||
title: string | null;
|
||||
status: string | null;
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
interface SyncAck {
|
||||
outbox_id?: string;
|
||||
id?: string;
|
||||
@@ -580,6 +587,7 @@ function readRecentFailures(
|
||||
id: row.id,
|
||||
entityType: row.entity_type,
|
||||
entityId: row.entity_id,
|
||||
entitySummary: readEntitySummary(row.entity_type, row.entity_id),
|
||||
operation: row.operation,
|
||||
attempts: row.attempts,
|
||||
lastAttemptAt: row.last_attempt_at,
|
||||
@@ -594,6 +602,58 @@ function readRecentFailures(
|
||||
});
|
||||
}
|
||||
|
||||
function readEntitySummary(
|
||||
entityType: string,
|
||||
entityId: string,
|
||||
): DigitalEmployeeSyncEntitySummary | null {
|
||||
const table = entityTable(entityType);
|
||||
const db = getDigitalEmployeeDb();
|
||||
if (!db || !table) return null;
|
||||
const select = entitySummarySelect(entityType);
|
||||
if (!select) return null;
|
||||
const row = db.prepare(select).get(entityId) as Record<string, unknown> | undefined;
|
||||
if (!row) return null;
|
||||
const payload = parsePayload(String(row.payload ?? "{}"));
|
||||
const payloadSummary = payload && typeof payload === "object" && !Array.isArray(payload)
|
||||
? payload as Record<string, unknown>
|
||||
: {};
|
||||
return {
|
||||
title: stringField(row.title) || stringField(payloadSummary.title) || null,
|
||||
status: stringField(row.status) || stringField(payloadSummary.status) || null,
|
||||
summary:
|
||||
stringField(row.summary) ||
|
||||
stringField(row.objective) ||
|
||||
stringField(row.message) ||
|
||||
stringField(payloadSummary.objective) ||
|
||||
stringField(payloadSummary.message) ||
|
||||
stringField(payloadSummary.summary) ||
|
||||
null,
|
||||
};
|
||||
}
|
||||
|
||||
function entitySummarySelect(entityType: string): string | null {
|
||||
switch (entityType) {
|
||||
case "plan":
|
||||
return "SELECT title, status, objective, payload FROM digital_plans WHERE id = ?";
|
||||
case "task":
|
||||
return "SELECT title, status, NULL AS objective, payload FROM digital_tasks WHERE id = ?";
|
||||
case "run":
|
||||
return "SELECT id AS title, status, NULL AS objective, payload FROM digital_runs WHERE id = ?";
|
||||
case "event":
|
||||
return "SELECT kind AS title, kind AS status, message, payload FROM digital_events WHERE id = ?";
|
||||
case "artifact":
|
||||
return "SELECT name AS title, kind AS status, uri AS summary, payload FROM digital_artifacts WHERE id = ?";
|
||||
case "approval":
|
||||
return "SELECT title, status, NULL AS objective, payload FROM digital_approvals WHERE id = ?";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stringField(value: unknown): string {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : "";
|
||||
}
|
||||
|
||||
function readAttemptHistory(
|
||||
outboxId: string,
|
||||
limit = 5,
|
||||
|
||||
Reference in New Issue
Block a user