Files
qiming/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/syncService.ts
2026-06-11 21:53:08 +08:00

2510 lines
101 KiB
TypeScript

import log from "electron-log";
import { getDeviceId } from "../system/deviceId";
import { getDomainTokenKey } from "@shared/utils/domain";
import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from "../../db";
import {
normalizePlanStepDispatchPolicy,
normalizeRiskApprovalPolicy,
normalizeRouteDecisionPolicy,
readDigitalEmployeeUiState,
saveDigitalEmployeeUiState,
upsertDigitalEmployeeApprovalFromRemote,
type DigitalEmployeeApprovalUpdateInput,
type DigitalEmployeePlanStepDispatchPolicy,
type DigitalEmployeeRiskApprovalPolicy,
type DigitalEmployeeRouteDecisionPolicy,
} from "./stateService";
import {
SKILL_POLICIES_SETTING_KEY,
normalizeDigitalEmployeeSkillPolicies,
readDigitalEmployeeSkillPolicies,
type DigitalEmployeeSkillPolicies,
} from "./skillExecutionPolicy";
const DEFAULT_SYNC_PATH = "/api/digital-employee/sync/outbox";
const DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH = "/api/digital-employee/policies/plan-step-dispatch";
const DEFAULT_SKILL_POLICY_PATH = "/api/digital-employee/policies/skills";
const DEFAULT_RISK_APPROVAL_POLICY_PATH = "/api/digital-employee/policies/risk-approval";
const DEFAULT_ROUTE_DECISION_POLICY_PATH = "/api/digital-employee/policies/route-decision";
const DEFAULT_APPROVAL_UPDATES_PATH = "/api/digital-employee/approvals/updates";
const DEFAULT_APPROVAL_DECISIONS_PATH = "/api/digital-employee/approvals/decisions";
const DEFAULT_NOTIFICATION_UPDATES_PATH = "/api/digital-employee/notifications/updates";
const DEFAULT_NOTIFICATION_ACTIONS_PATH = "/api/digital-employee/notifications/actions";
const DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH = "/api/digital-employee/leases/plan-step-dispatch";
const MANAGEMENT_SYNC_RECORD_PATH =
"/system/content/content-digital-employee-sync";
const DIGITAL_EMPLOYEE_SYNC_CONTRACT_VERSION = "qimingclaw.digital_employee.sync.v1";
const DEFAULT_INTERVAL_MS = 30_000;
const DEFAULT_BATCH_SIZE = 50;
const REQUEST_TIMEOUT_MS = 15_000;
const POLICY_PULL_THROTTLE_MS = 60_000;
const MAX_RETRY_DELAY_MS = 30 * 60_000;
const FAILED_RETRY_CANDIDATE_MULTIPLIER = 4;
const ATTEMPT_HISTORY_RETENTION_MS = 30 * 24 * 60 * 60_000;
let syncTimer: ReturnType<typeof setInterval> | null = null;
let syncing = false;
let lastSyncAt: string | null = null;
let lastSyncError: string | null = null;
let policyPulling = false;
let lastPolicyPullAttemptMs = 0;
let skillPolicyPulling = false;
let lastSkillPolicyPullAttemptMs = 0;
let riskApprovalPolicyPulling = false;
let lastRiskApprovalPolicyPullAttemptMs = 0;
let routeDecisionPolicyPulling = false;
let lastRouteDecisionPolicyPullAttemptMs = 0;
let approvalUpdatesPulling = false;
let lastApprovalUpdatesPullAttemptMs = 0;
let notificationUpdatesPulling = false;
let lastNotificationUpdatesPullAttemptMs = 0;
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
if (!ensureDigitalEmployeeSchema()) return null;
return getDb();
}
interface DigitalEmployeeSyncConfig {
enabled: boolean;
endpoint: string | null;
policyEndpoint: string | null;
skillPolicyEndpoint: string | null;
riskApprovalPolicyEndpoint: string | null;
routeDecisionPolicyEndpoint: string | null;
approvalUpdatesEndpoint: string | null;
approvalDecisionsEndpoint: string | null;
notificationUpdatesEndpoint: string | null;
notificationActionsEndpoint: string | null;
leaseEndpoint: string | null;
clientKey: string | null;
authToken: string | null;
batchSize: number;
}
export interface DigitalEmployeeSkillPolicyPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
policies: DigitalEmployeeSkillPolicies;
error?: string | null;
}
export interface DigitalEmployeePlanStepDispatchPolicyPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
policy: DigitalEmployeePlanStepDispatchPolicy;
error?: string | null;
}
export interface DigitalEmployeeRiskApprovalPolicyPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
policy: DigitalEmployeeRiskApprovalPolicy;
error?: string | null;
}
export interface DigitalEmployeeRouteDecisionPolicyPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
policy: DigitalEmployeeRouteDecisionPolicy;
error?: string | null;
}
export interface DigitalEmployeeApprovalUpdatesPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
applied: number;
ignored: number;
conflicted?: number;
error?: string | null;
}
export interface DigitalEmployeeApprovalDecisionSubmitResult {
ok: boolean;
endpoint: string | null;
submitted_at: string | null;
error?: string | null;
}
export interface DigitalEmployeeNotificationUpdatesPullResult {
ok: boolean;
skipped?: boolean;
endpoint: string | null;
pulled_at: string | null;
applied: number;
ignored: number;
error?: string | null;
}
export interface DigitalEmployeeNotificationActionSubmitResult {
ok: boolean;
endpoint: string | null;
submitted_at: string | null;
error?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLease {
lease_id: string;
state: "active" | "released" | "expired";
owner_device_id: string;
acquired_at: string;
expires_at: string;
released_at?: string | null;
release_reason?: string | null;
remote_lease_id?: string | null;
remote_owner_device_id?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLeaseResult {
ok: boolean;
rejected?: boolean;
fallback?: boolean;
endpoint: string | null;
lease?: DigitalEmployeePlanStepRemoteLease | null;
reason?: string | null;
error?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLeaseAcquireInput {
planId?: string | null;
stepId: string;
taskId?: string | null;
triggerSource?: string | null;
ttlMs?: number | null;
occurredAt?: string | null;
}
export interface DigitalEmployeePlanStepRemoteLeaseReleaseInput {
planId?: string | null;
stepId: string;
taskId?: string | null;
runId?: string | null;
leaseId?: string | null;
remoteLeaseId?: string | null;
reason?: string | null;
occurredAt?: string | null;
}
interface OutboxRow {
id: string;
entity_type: string;
entity_id: string;
operation: string;
payload: string;
attempts: number;
last_attempt_at?: string | null;
}
export interface DigitalEmployeeSyncAttempt {
attemptNo: number;
status: string;
error: string | null;
endpoint: string | null;
startedAt: string;
finishedAt: string;
}
export interface DigitalEmployeeSyncFailureGroup {
entityType: string;
count: number;
}
export interface DigitalEmployeeSyncFailureSummary {
total: number;
dueForRetry: number;
backoff: number;
byEntityType: DigitalEmployeeSyncFailureGroup[];
}
export interface DigitalEmployeeSyncFailure {
id: string;
entityType: string;
entityId: string;
entitySummary: DigitalEmployeeSyncEntitySummary | null;
operation: string;
attempts: number;
lastAttemptAt: string | null;
nextRetryAt: string | null;
dueForRetry: boolean;
managementRecordUrl: string | null;
attemptHistory: DigitalEmployeeSyncAttempt[];
error: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeSyncEntitySummary {
title: string | null;
status: string | null;
summary: string | null;
}
interface SyncAck {
outbox_id?: string;
id?: string;
entity_type?: string;
entity_id?: string;
remote_id?: string;
}
interface SyncResponse {
success?: boolean;
code?: string | number;
status?: string | number;
acked?: SyncAck[];
data?: {
acked?: SyncAck[];
};
message?: string;
}
interface EventBusinessDetail {
plan_id: string | null;
task_id: string | null;
run_id: string | null;
kind: string | null;
message: string | null;
payload: Record<string, unknown>;
occurred_at: string | null;
}
export interface DigitalEmployeeSyncStatus {
running: boolean;
syncing: boolean;
enabled: boolean;
endpoint: string | null;
missingCredentials: string[];
pending: number;
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
failureSummary: DigitalEmployeeSyncFailureSummary;
recentFailures: DigitalEmployeeSyncFailure[];
}
export function startDigitalEmployeeSyncWorker(): void {
if (syncTimer) return;
syncTimer = setInterval(() => {
void flushDigitalEmployeeSyncOutbox().catch((error) => {
lastSyncError = normalizeError(error);
log.warn("[DigitalEmployeeSync] Scheduled sync failed:", error);
});
}, DEFAULT_INTERVAL_MS);
void flushDigitalEmployeeSyncOutbox().catch((error) => {
lastSyncError = normalizeError(error);
log.debug("[DigitalEmployeeSync] Initial sync skipped/failed:", error);
});
log.info("[DigitalEmployeeSync] Worker started");
}
export function stopDigitalEmployeeSyncWorker(): void {
if (!syncTimer) return;
clearInterval(syncTimer);
syncTimer = null;
log.info("[DigitalEmployeeSync] Worker stopped");
}
export function getDigitalEmployeeSyncStatus(): DigitalEmployeeSyncStatus {
const config = resolveSyncConfig();
return {
running: Boolean(syncTimer),
syncing,
enabled: config.enabled,
endpoint: config.endpoint,
missingCredentials: missingSyncCredentialLabels(config),
pending: countOutbox("pending"),
failed: countOutbox("failed"),
lastSyncAt,
lastSyncError,
failureSummary: readFailureSummary(),
recentFailures: readRecentFailures(config),
};
}
export async function pullDigitalEmployeePlanStepDispatchPolicy(
options: { force?: boolean } = {},
): Promise<DigitalEmployeePlanStepDispatchPolicyPullResult> {
const currentState = readDigitalEmployeeUiState();
const currentPolicy = currentState.planStepDispatchPolicy ?? normalizePlanStepDispatchPolicy(null);
if (policyPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().policyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().policyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
lastPolicyPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.policyEndpoint || missingCredentials.length > 0) {
const error = !config.policyEndpoint ? "management policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
return recordPlanStepDispatchPolicyPullFailure(currentState, currentPolicy, config.policyEndpoint, error);
}
policyPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.policyEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const remotePolicy = readRemotePlanStepDispatchPolicy(response);
if (!remotePolicy) throw new Error("management policy response missing plan step dispatch policy");
const pulledAt = new Date().toISOString();
const policy = normalizePlanStepDispatchPolicy({
...remotePolicy,
source: "remote",
remote_updated_at: stringField(remotePolicy.updated_at) || stringField(remotePolicy.remote_updated_at) || null,
last_pulled_at: pulledAt,
last_pull_error: null,
});
saveDigitalEmployeeUiState({
action: "pull_plan_step_dispatch_policy",
metadata: {
source: "management-api",
endpoint: config.policyEndpoint,
ok: true,
plan_step_dispatch_policy: policy,
},
state: {
...currentState,
planStepDispatchPolicy: policy,
},
});
return { ok: true, endpoint: config.policyEndpoint, pulled_at: pulledAt, policy, error: null };
} catch (error) {
return recordPlanStepDispatchPolicyPullFailure(currentState, currentPolicy, config.policyEndpoint, normalizeError(error));
} finally {
policyPulling = false;
}
}
export async function pullDigitalEmployeeSkillPolicies(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeSkillPolicyPullResult> {
const currentPolicies = readDigitalEmployeeSkillPolicies();
if (skillPolicyPulling) {
return {
ok: true,
skipped: true,
endpoint: resolveSyncConfig().skillPolicyEndpoint,
pulled_at: currentPolicies.last_pulled_at ?? null,
policies: currentPolicies,
};
}
const nowMs = Date.now();
if (!options.force && nowMs - lastSkillPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return {
ok: true,
skipped: true,
endpoint: resolveSyncConfig().skillPolicyEndpoint,
pulled_at: currentPolicies.last_pulled_at ?? null,
policies: currentPolicies,
};
}
lastSkillPolicyPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.skillPolicyEndpoint || missingCredentials.length > 0) {
const error = !config.skillPolicyEndpoint ? "management skill policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
return recordSkillPolicyPullFailure(currentPolicies, config.skillPolicyEndpoint, error);
}
skillPolicyPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.skillPolicyEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const remotePayload = readRemoteSkillPolicyPayload(response);
if (!remotePayload) throw new Error("management policy response missing skill policies");
const remoteSkills = readRemoteSkillPolicyRecords(remotePayload);
if (!remoteSkills) throw new Error("management policy response missing skill policy records");
const pulledAt = new Date().toISOString();
const policies = normalizeDigitalEmployeeSkillPolicies({
version: 1,
local_skills: currentPolicies.local_skills,
remote_skills: remoteSkills,
source: "remote",
remote_updated_at: stringField(remotePayload.updated_at) || stringField(remotePayload.updatedAt) || stringField(remotePayload.remote_updated_at) || null,
last_pulled_at: pulledAt,
last_pull_error: null,
});
writeSetting(SKILL_POLICIES_SETTING_KEY, policies);
return { ok: true, endpoint: config.skillPolicyEndpoint, pulled_at: pulledAt, policies, error: null };
} catch (error) {
return recordSkillPolicyPullFailure(currentPolicies, config.skillPolicyEndpoint, normalizeError(error));
} finally {
skillPolicyPulling = false;
}
}
export async function pullDigitalEmployeeRiskApprovalPolicy(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeRiskApprovalPolicyPullResult> {
const currentState = readDigitalEmployeeUiState();
const currentPolicy = currentState.riskApprovalPolicy ?? normalizeRiskApprovalPolicy(null);
if (riskApprovalPolicyPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastRiskApprovalPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().riskApprovalPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
lastRiskApprovalPolicyPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.riskApprovalPolicyEndpoint || missingCredentials.length > 0) {
const error = !config.riskApprovalPolicyEndpoint ? "management risk approval policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, error);
}
riskApprovalPolicyPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.riskApprovalPolicyEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const remotePolicy = readRemoteRiskApprovalPolicy(response);
if (!remotePolicy) throw new Error("management policy response missing risk approval policy");
const pulledAt = new Date().toISOString();
const policy = normalizeRiskApprovalPolicy({
...remotePolicy,
source: "remote",
remote_updated_at: stringField(remotePolicy.updated_at) || stringField(remotePolicy.updatedAt) || stringField(remotePolicy.remote_updated_at) || null,
last_pulled_at: pulledAt,
last_pull_error: null,
});
saveDigitalEmployeeUiState({
action: "pull_risk_approval_policy",
metadata: {
source: "management-api",
endpoint: config.riskApprovalPolicyEndpoint,
ok: true,
risk_approval_policy: policy,
},
state: {
...currentState,
riskApprovalPolicy: policy,
},
});
return { ok: true, endpoint: config.riskApprovalPolicyEndpoint, pulled_at: pulledAt, policy, error: null };
} catch (error) {
return recordRiskApprovalPolicyPullFailure(currentState, currentPolicy, config.riskApprovalPolicyEndpoint, normalizeError(error));
} finally {
riskApprovalPolicyPulling = false;
}
}
export async function pullDigitalEmployeeRouteDecisionPolicy(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeRouteDecisionPolicyPullResult> {
const currentState = readDigitalEmployeeUiState();
const currentPolicy = currentState.routeDecisionPolicy ?? normalizeRouteDecisionPolicy(null);
if (routeDecisionPolicyPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().routeDecisionPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastRouteDecisionPolicyPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().routeDecisionPolicyEndpoint, pulled_at: currentPolicy.last_pulled_at ?? null, policy: currentPolicy };
}
lastRouteDecisionPolicyPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.routeDecisionPolicyEndpoint || missingCredentials.length > 0) {
const error = !config.routeDecisionPolicyEndpoint ? "management route decision policy endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
return recordRouteDecisionPolicyPullFailure(currentState, currentPolicy, config.routeDecisionPolicyEndpoint, error);
}
routeDecisionPolicyPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.routeDecisionPolicyEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const remotePolicy = readRemoteRouteDecisionPolicy(response);
if (!remotePolicy) throw new Error("management policy response missing route decision policy");
const pulledAt = new Date().toISOString();
const policy = normalizeRouteDecisionPolicy({
...remotePolicy,
source: "remote",
remote_updated_at: stringField(remotePolicy.updated_at) || stringField(remotePolicy.updatedAt) || stringField(remotePolicy.remote_updated_at) || null,
last_pulled_at: pulledAt,
last_pull_error: null,
});
saveDigitalEmployeeUiState({
action: "pull_route_decision_policy",
metadata: {
source: "management-api",
endpoint: config.routeDecisionPolicyEndpoint,
ok: true,
route_decision_policy: policy,
},
state: {
...currentState,
routeDecisionPolicy: policy,
},
});
return { ok: true, endpoint: config.routeDecisionPolicyEndpoint, pulled_at: pulledAt, policy, error: null };
} catch (error) {
return recordRouteDecisionPolicyPullFailure(currentState, currentPolicy, config.routeDecisionPolicyEndpoint, normalizeError(error));
} finally {
routeDecisionPolicyPulling = false;
}
}
export async function pullDigitalEmployeeApprovalUpdates(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeApprovalUpdatesPullResult> {
if (approvalUpdatesPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().approvalUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastApprovalUpdatesPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().approvalUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
lastApprovalUpdatesPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.approvalUpdatesEndpoint || missingCredentials.length > 0) {
return {
ok: false,
endpoint: config.approvalUpdatesEndpoint,
pulled_at: new Date().toISOString(),
applied: 0,
ignored: 0,
error: !config.approvalUpdatesEndpoint ? "management approval updates endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`,
};
}
approvalUpdatesPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.approvalUpdatesEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const updates = readRemoteApprovalUpdates(response);
const pulledAt = new Date().toISOString();
let applied = 0;
let ignored = 0;
let conflicted = 0;
for (const update of updates) {
const result = upsertDigitalEmployeeApprovalFromRemote({ ...update, source: update.source ?? "management-api" });
if (result.ok && result.conflict) conflicted += 1;
else if (result.ok && !result.skipped) applied += 1;
else ignored += 1;
}
recordApprovalSyncEvent("approval_updates_pulled", pulledAt, {
action: "pull_approval_updates",
endpoint: config.approvalUpdatesEndpoint,
ok: true,
applied,
ignored,
conflicted,
source: "management-api",
});
return { ok: true, endpoint: config.approvalUpdatesEndpoint, pulled_at: pulledAt, applied, ignored, conflicted, error: null };
} catch (error) {
const pulledAt = new Date().toISOString();
const message = normalizeError(error);
recordApprovalSyncEvent("approval_updates_pull_failed", pulledAt, {
action: "pull_approval_updates",
endpoint: config.approvalUpdatesEndpoint,
ok: false,
error: message,
source: "management-api",
});
return { ok: false, endpoint: config.approvalUpdatesEndpoint, pulled_at: pulledAt, applied: 0, ignored: 0, error: message };
} finally {
approvalUpdatesPulling = false;
}
}
export async function submitDigitalEmployeeApprovalDecision(
input: Record<string, unknown>,
): Promise<DigitalEmployeeApprovalDecisionSubmitResult> {
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
const submittedAt = new Date().toISOString();
if (!config.approvalDecisionsEndpoint || missingCredentials.length > 0) {
const error = !config.approvalDecisionsEndpoint ? "management approval decisions endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
recordApprovalSyncEvent("approval_decision_submit_failed", submittedAt, {
action: "submit_approval_decision",
ok: false,
endpoint: config.approvalDecisionsEndpoint,
error,
approval_id: stringField(input.approval_id ?? input.approvalId) || null,
decision: stringField(input.decision) || null,
source: "qimingclaw-approval-sync",
});
return { ok: false, endpoint: config.approvalDecisionsEndpoint, submitted_at: submittedAt, error };
}
try {
const response = await fetchJsonWithAuth(config, config.approvalDecisionsEndpoint, {
...input,
submitted_at: submittedAt,
device_id: getDeviceId(),
});
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
recordApprovalSyncEvent("approval_decision_submitted", submittedAt, {
action: "submit_approval_decision",
ok: true,
endpoint: config.approvalDecisionsEndpoint,
approval_id: stringField(input.approval_id ?? input.approvalId) || null,
decision: stringField(input.decision) || null,
source: "qimingclaw-approval-sync",
});
return { ok: true, endpoint: config.approvalDecisionsEndpoint, submitted_at: submittedAt, error: null };
} catch (error) {
const message = normalizeError(error);
recordApprovalSyncEvent("approval_decision_submit_failed", submittedAt, {
action: "submit_approval_decision",
ok: false,
endpoint: config.approvalDecisionsEndpoint,
error: message,
approval_id: stringField(input.approval_id ?? input.approvalId) || null,
decision: stringField(input.decision) || null,
source: "qimingclaw-approval-sync",
});
return { ok: false, endpoint: config.approvalDecisionsEndpoint, submitted_at: submittedAt, error: message };
}
}
export async function pullDigitalEmployeeNotificationUpdates(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeNotificationUpdatesPullResult> {
if (notificationUpdatesPulling) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().notificationUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
const nowMs = Date.now();
if (!options.force && nowMs - lastNotificationUpdatesPullAttemptMs < POLICY_PULL_THROTTLE_MS) {
return { ok: true, skipped: true, endpoint: resolveSyncConfig().notificationUpdatesEndpoint, pulled_at: null, applied: 0, ignored: 0 };
}
lastNotificationUpdatesPullAttemptMs = nowMs;
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
if (!config.notificationUpdatesEndpoint || missingCredentials.length > 0) {
const pulledAt = new Date().toISOString();
const error = !config.notificationUpdatesEndpoint ? "management notification updates endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
recordNotificationSyncEvent("notification_updates_pull_failed", pulledAt, {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: false,
applied: 0,
ignored: 0,
error,
source: "management-api",
});
return { ok: false, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied: 0, ignored: 0, error };
}
notificationUpdatesPulling = true;
try {
const response = await fetchJsonWithAuth(config, config.notificationUpdatesEndpoint);
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const updates = readRemoteNotificationUpdates(response);
const pulledAt = new Date().toISOString();
const currentState = readDigitalEmployeeUiState();
const notificationStateById: Record<string, unknown> = { ...(currentState.notificationStateById ?? {}) };
let applied = 0;
let ignored = 0;
for (const update of updates) {
if (!update.notificationId) {
ignored += 1;
continue;
}
const currentOverlay = objectRecord(notificationStateById[update.notificationId]) ?? {};
notificationStateById[update.notificationId] = {
...currentOverlay,
...update.overlay,
};
applied += 1;
}
saveDigitalEmployeeUiState({
action: "notification_updates_pulled",
metadata: {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: true,
applied,
ignored,
source: "management-api",
},
state: {
...currentState,
notificationStateById,
},
});
return { ok: true, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied, ignored, error: null };
} catch (error) {
const pulledAt = new Date().toISOString();
const message = normalizeError(error);
recordNotificationSyncEvent("notification_updates_pull_failed", pulledAt, {
action: "pull_notification_updates",
endpoint: config.notificationUpdatesEndpoint,
ok: false,
applied: 0,
ignored: 0,
error: message,
source: "management-api",
});
return { ok: false, endpoint: config.notificationUpdatesEndpoint, pulled_at: pulledAt, applied: 0, ignored: 0, error: message };
} finally {
notificationUpdatesPulling = false;
}
}
export async function submitDigitalEmployeeNotificationAction(
input: Record<string, unknown>,
): Promise<DigitalEmployeeNotificationActionSubmitResult> {
const config = resolveSyncConfig();
const missingCredentials = missingSyncCredentialLabels(config);
const submittedAt = new Date().toISOString();
const notificationId = stringField(input.notification_id ?? input.notificationId) || null;
const notificationAction = stringField(input.action) || null;
const status = stringField(input.status) || null;
const reply = stringField(input.reply ?? input.content ?? input.message);
if (!config.notificationActionsEndpoint || missingCredentials.length > 0) {
const error = !config.notificationActionsEndpoint ? "management notification actions endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`;
recordNotificationSyncEvent("notification_action_submit_failed", submittedAt, {
action: "submit_notification_action",
ok: false,
endpoint: config.notificationActionsEndpoint,
error,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: false, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error };
}
try {
const response = await fetchJsonWithAuth(config, config.notificationActionsEndpoint, {
...input,
acted_at: stringField(input.acted_at ?? input.actedAt) || submittedAt,
submitted_at: submittedAt,
device_id: getDeviceId(),
});
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
recordNotificationSyncEvent("notification_action_submitted", submittedAt, {
action: "submit_notification_action",
ok: true,
endpoint: config.notificationActionsEndpoint,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: true, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error: null };
} catch (error) {
const message = normalizeError(error);
recordNotificationSyncEvent("notification_action_submit_failed", submittedAt, {
action: "submit_notification_action",
ok: false,
endpoint: config.notificationActionsEndpoint,
error: message,
notification_id: notificationId,
notification_action: notificationAction,
status,
reply_length: reply ? reply.length : null,
source: "qimingclaw-notification-sync",
});
return { ok: false, endpoint: config.notificationActionsEndpoint, submitted_at: submittedAt, error: message };
}
}
export async function acquireDigitalEmployeePlanStepRemoteLease(
input: DigitalEmployeePlanStepRemoteLeaseAcquireInput,
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
const config = resolveSyncConfig();
const endpoint = buildLeaseOperationEndpoint(config.leaseEndpoint, "acquire");
const missingCredentials = missingSyncCredentialLabels(config);
if (!endpoint || missingCredentials.length > 0) {
return {
ok: false,
fallback: true,
endpoint,
error: !endpoint ? "management lease endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`,
};
}
const occurredAt = input.occurredAt || new Date().toISOString();
const ttlMs = Math.max(60_000, Math.min(Number(input.ttlMs) || 5 * 60_000, 30 * 60_000));
try {
const response = await fetchJsonWithAuth(config, endpoint, {
client_key: config.clientKey,
device_id: getDeviceId(),
plan_id: stringField(input.planId) || null,
step_id: input.stepId,
task_id: stringField(input.taskId) || null,
trigger_source: stringField(input.triggerSource) || null,
ttl_ms: ttlMs,
occurred_at: occurredAt,
});
const rejectedReason = readPlanStepLeaseRejectionReason(response);
if (rejectedReason) return { ok: false, rejected: true, endpoint, reason: rejectedReason, lease: readRemotePlanStepLease(response, occurredAt, ttlMs) };
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
const lease = readRemotePlanStepLease(response, occurredAt, ttlMs);
if (!lease) throw new Error("management lease response missing plan step lease");
return { ok: true, endpoint, lease };
} catch (error) {
return { ok: false, fallback: true, endpoint, error: normalizeError(error) };
}
}
export async function releaseDigitalEmployeePlanStepRemoteLease(
input: DigitalEmployeePlanStepRemoteLeaseReleaseInput,
): Promise<DigitalEmployeePlanStepRemoteLeaseResult> {
const config = resolveSyncConfig();
const endpoint = buildLeaseOperationEndpoint(config.leaseEndpoint, "release");
const missingCredentials = missingSyncCredentialLabels(config);
if (!endpoint || missingCredentials.length > 0) {
return {
ok: false,
endpoint,
error: !endpoint ? "management lease endpoint unavailable" : `missing credentials: ${missingCredentials.join(", ")}`,
};
}
const occurredAt = input.occurredAt || new Date().toISOString();
try {
const response = await fetchJsonWithAuth(config, endpoint, {
client_key: config.clientKey,
device_id: getDeviceId(),
plan_id: stringField(input.planId) || null,
step_id: input.stepId,
task_id: stringField(input.taskId) || null,
run_id: stringField(input.runId) || null,
lease_id: stringField(input.leaseId) || null,
remote_lease_id: stringField(input.remoteLeaseId) || stringField(input.leaseId) || null,
reason: stringField(input.reason) || null,
occurred_at: occurredAt,
});
const responseError = readSyncResponseError(response as SyncResponse);
if (responseError) throw new Error(responseError);
return { ok: true, endpoint, lease: readRemotePlanStepLease(response, occurredAt, 0) };
} catch (error) {
return { ok: false, endpoint, error: normalizeError(error) };
}
}
export async function flushDigitalEmployeeSyncOutbox(
options: { force?: boolean } = {},
): Promise<DigitalEmployeeSyncStatus> {
if (syncing) return getDigitalEmployeeSyncStatus();
const config = resolveSyncConfig();
if (
!config.enabled ||
!config.endpoint ||
missingSyncCredentialLabels(config).length > 0
) {
lastSyncError = null;
return getDigitalEmployeeSyncStatus();
}
const rows = readPendingOutbox(config.batchSize, options.force === true);
if (rows.length === 0) {
lastSyncError = null;
return getDigitalEmployeeSyncStatus();
}
syncing = true;
const startedAt = new Date().toISOString();
markRowsSyncing(rows, startedAt);
try {
const response = await postOutbox(config, rows);
const finishedAt = new Date().toISOString();
const acked = response.data?.acked || response.acked || [];
const responseError = readSyncResponseError(response);
if (responseError) throw new Error(responseError);
const syncedRows = acked.length > 0
? rows.filter((row) => acked.some((ack) => ackMatchesRow(ack, row)))
: rows;
const unackedRows = acked.length > 0
? rows.filter((row) => !acked.some((ack) => ackMatchesRow(ack, row)))
: [];
recordSyncAttempts(syncedRows, config, "synced", null, startedAt, finishedAt);
if (acked.length > 0) {
markRowsSynced(acked, finishedAt, rows);
} else {
markRowsSynced(
rows.map((row) => ({
outbox_id: row.id,
entity_type: row.entity_type,
entity_id: row.entity_id,
})),
finishedAt,
);
}
if (unackedRows.length > 0) {
const unackedError = "management sync response did not acknowledge item";
recordSyncAttempts(
unackedRows,
config,
"failed",
unackedError,
startedAt,
finishedAt,
);
markRowsFailed(unackedRows, finishedAt, unackedError);
lastSyncError = unackedError;
} else {
lastSyncError = null;
}
lastSyncAt = finishedAt;
} catch (error) {
const finishedAt = new Date().toISOString();
lastSyncError = normalizeError(error);
recordSyncAttempts(rows, config, "failed", lastSyncError, startedAt, finishedAt);
markRowsFailed(rows, finishedAt, lastSyncError);
log.warn("[DigitalEmployeeSync] Flush failed:", lastSyncError);
} finally {
syncing = false;
}
return getDigitalEmployeeSyncStatus();
}
function readSyncResponseError(response: SyncResponse): string | null {
if (response.success === false) {
return response.message || "digital employee sync rejected";
}
const code = normalizeResponseCode(response.code);
if (code && code !== "0000") {
return response.message || `digital employee sync rejected: ${code}`;
}
const status = normalizeResponseCode(response.status);
if (status && ["error", "fail", "failed", "failure"].includes(status.toLowerCase())) {
return response.message || "digital employee sync rejected";
}
return null;
}
function normalizeResponseCode(value: unknown): string | null {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed || null;
}
if (typeof value === "number" && Number.isFinite(value)) {
return String(value);
}
return null;
}
function resolveSyncConfig(): DigitalEmployeeSyncConfig {
const config = (readSetting("digital_employee_sync_config") || {}) as Record<
string,
unknown
>;
const step1 = (readSetting("step1_config") || {}) as Record<string, unknown>;
const endpointOverride =
typeof config.endpoint === "string" && config.endpoint.trim()
? config.endpoint.trim()
: null;
const serverHost =
typeof step1.serverHost === "string" && step1.serverHost.trim()
? step1.serverHost.trim()
: null;
const endpoint = endpointOverride || buildEndpoint(serverHost);
const policyEndpointOverride =
typeof config.planStepDispatchPolicyEndpoint === "string" && config.planStepDispatchPolicyEndpoint.trim()
? config.planStepDispatchPolicyEndpoint.trim()
: typeof config.policyEndpoint === "string" && config.policyEndpoint.trim()
? config.policyEndpoint.trim()
: null;
const policyEndpoint = policyEndpointOverride || buildEndpoint(serverHost, DEFAULT_PLAN_STEP_DISPATCH_POLICY_PATH);
const skillPolicyEndpointOverride =
typeof config.skillPolicyEndpoint === "string" && config.skillPolicyEndpoint.trim()
? config.skillPolicyEndpoint.trim()
: null;
const skillPolicyEndpoint = skillPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_SKILL_POLICY_PATH);
const riskApprovalPolicyEndpointOverride =
typeof config.riskApprovalPolicyEndpoint === "string" && config.riskApprovalPolicyEndpoint.trim()
? config.riskApprovalPolicyEndpoint.trim()
: null;
const riskApprovalPolicyEndpoint = riskApprovalPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_RISK_APPROVAL_POLICY_PATH);
const routeDecisionPolicyEndpointOverride =
typeof config.routeDecisionPolicyEndpoint === "string" && config.routeDecisionPolicyEndpoint.trim()
? config.routeDecisionPolicyEndpoint.trim()
: null;
const routeDecisionPolicyEndpoint = routeDecisionPolicyEndpointOverride || buildEndpoint(serverHost, DEFAULT_ROUTE_DECISION_POLICY_PATH);
const approvalUpdatesEndpointOverride =
typeof config.approvalUpdatesEndpoint === "string" && config.approvalUpdatesEndpoint.trim()
? config.approvalUpdatesEndpoint.trim()
: null;
const approvalUpdatesEndpoint = approvalUpdatesEndpointOverride || buildEndpoint(serverHost, DEFAULT_APPROVAL_UPDATES_PATH);
const approvalDecisionsEndpointOverride =
typeof config.approvalDecisionsEndpoint === "string" && config.approvalDecisionsEndpoint.trim()
? config.approvalDecisionsEndpoint.trim()
: null;
const approvalDecisionsEndpoint = approvalDecisionsEndpointOverride || buildEndpoint(serverHost, DEFAULT_APPROVAL_DECISIONS_PATH);
const notificationUpdatesEndpointOverride =
typeof config.notificationUpdatesEndpoint === "string" && config.notificationUpdatesEndpoint.trim()
? config.notificationUpdatesEndpoint.trim()
: null;
const notificationUpdatesEndpoint = notificationUpdatesEndpointOverride || buildEndpoint(serverHost, DEFAULT_NOTIFICATION_UPDATES_PATH);
const notificationActionsEndpointOverride =
typeof config.notificationActionsEndpoint === "string" && config.notificationActionsEndpoint.trim()
? config.notificationActionsEndpoint.trim()
: null;
const notificationActionsEndpoint = notificationActionsEndpointOverride || buildEndpoint(serverHost, DEFAULT_NOTIFICATION_ACTIONS_PATH);
const leaseEndpointOverride =
typeof config.planStepDispatchLeaseEndpoint === "string" && config.planStepDispatchLeaseEndpoint.trim()
? config.planStepDispatchLeaseEndpoint.trim()
: null;
const leaseEndpoint = leaseEndpointOverride || buildEndpoint(serverHost, DEFAULT_PLAN_STEP_DISPATCH_LEASE_PATH);
const disabled = config.enabled === false;
const batchSize =
typeof config.batchSize === "number" && config.batchSize > 0
? Math.min(Math.floor(config.batchSize), 200)
: DEFAULT_BATCH_SIZE;
return {
enabled: !disabled,
endpoint,
policyEndpoint,
skillPolicyEndpoint,
riskApprovalPolicyEndpoint,
routeDecisionPolicyEndpoint,
approvalUpdatesEndpoint,
approvalDecisionsEndpoint,
notificationUpdatesEndpoint,
notificationActionsEndpoint,
leaseEndpoint,
clientKey: readClientKey(serverHost),
authToken: readAuthToken(serverHost),
batchSize,
};
}
function buildLeaseOperationEndpoint(baseEndpoint: string | null, operation: "acquire" | "release"): string | null {
if (!baseEndpoint) return null;
return `${baseEndpoint.replace(/\/+$/, "")}/${operation}`;
}
function buildEndpoint(serverHost: string | null, path = DEFAULT_SYNC_PATH): string | null {
if (!serverHost) return null;
const normalized = serverHost.startsWith("http")
? serverHost
: `https://${serverHost}`;
return `${normalized.replace(/\/+$/, "")}${path}`;
}
function missingSyncCredentialLabels(
config: DigitalEmployeeSyncConfig,
): string[] {
return [
config.clientKey ? "" : "客户端 Key",
config.authToken ? "" : "登录 token",
].filter((value): value is string => Boolean(value));
}
function readClientKey(serverHost: string | null): string | null {
const username = readSetting("auth.username");
const globalKey = readSetting("auth.saved_key");
if (serverHost && typeof username === "string" && username.trim()) {
const domain = serverHost.replace(/^https?:\/\//, "").replace(/\/+$/, "");
const domainKey = readSetting(`auth.saved_keys.${domain}_${username}`);
if (typeof domainKey === "string" && domainKey.trim()) return domainKey;
}
return typeof globalKey === "string" && globalKey.trim() ? globalKey : null;
}
function readAuthToken(serverHost: string | null): string | null {
const oneShotToken = readSetting("auth.token");
if (typeof oneShotToken === "string" && oneShotToken.trim()) {
return oneShotToken;
}
if (serverHost) {
const domainToken = readSetting(getDomainTokenKey(serverHost));
if (typeof domainToken === "string" && domainToken.trim()) {
return domainToken;
}
}
return null;
}
function readPendingOutbox(limit: number, force: boolean): OutboxRow[] {
const db = getDigitalEmployeeDb();
if (!db) return [];
const candidateLimit = force
? limit
: limit * FAILED_RETRY_CANDIDATE_MULTIPLIER;
const rows = db
.prepare(
`
SELECT id, entity_type, entity_id, operation, payload, attempts, last_attempt_at
FROM digital_sync_outbox
WHERE status IN ('pending', 'failed')
ORDER BY created_at ASC
LIMIT ?
`,
)
.all(candidateLimit) as OutboxRow[];
if (force) return rows.slice(0, limit);
return rows.filter(isOutboxRowDueForRetry).slice(0, limit);
}
function isOutboxRowDueForRetry(row: OutboxRow): boolean {
if (!row.last_attempt_at) return true;
const lastAttemptAt = Date.parse(row.last_attempt_at);
if (!Number.isFinite(lastAttemptAt)) return true;
return Date.now() >= lastAttemptAt + retryDelayMs(row.attempts);
}
function retryDelayMs(attempts: number): number {
const exponent = Math.max(0, Math.min(attempts - 1, 10));
return Math.min(DEFAULT_INTERVAL_MS * 2 ** exponent, MAX_RETRY_DELAY_MS);
}
async function postOutbox(
config: DigitalEmployeeSyncConfig,
rows: OutboxRow[],
): Promise<SyncResponse> {
return fetchJsonWithAuth(config, config.endpoint!, {
client_key: config.clientKey,
device_id: getDeviceId(),
contract_version: DIGITAL_EMPLOYEE_SYNC_CONTRACT_VERSION,
items: rows.map(buildSyncItem),
}) as Promise<SyncResponse>;
}
async function fetchJsonWithAuth(
config: DigitalEmployeeSyncConfig,
endpoint: string,
requestBody?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
const response = await fetch(endpoint, {
method: requestBody ? "POST" : "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.authToken}`,
"X-Qiming-Client-Key": config.clientKey!,
"X-Qiming-Device-Id": getDeviceId(),
},
body: requestBody ? JSON.stringify(requestBody) : undefined,
signal: controller.signal,
});
const text = await response.text();
const responseBody = text ? (JSON.parse(text) as Record<string, unknown>) : {};
if (!response.ok) {
throw new Error(stringField(responseBody.message) || `HTTP ${response.status}`);
}
return responseBody;
} finally {
clearTimeout(timer);
}
}
function readRemotePlanStepDispatchPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
const data = objectRecord(response.data) ?? response;
return objectRecord(data.plan_step_dispatch_policy)
?? objectRecord(data.planStepDispatchPolicy)
?? objectRecord(data.policy)
?? (hasPlanStepDispatchPolicyFields(data) ? data : null);
}
function readRemoteSkillPolicyPayload(response: Record<string, unknown>): Record<string, unknown> | null {
const data = objectRecord(response.data) ?? response;
return objectRecord(data.skill_policies)
?? objectRecord(data.skillPolicies)
?? objectRecord(data.policies)
?? objectRecord(data.policy)
?? (hasSkillPolicyWrapperFields(data) || looksLikeSkillPolicyMap(data) ? data : null);
}
function readRemoteSkillPolicyRecords(payload: Record<string, unknown>): Record<string, unknown> | null {
return objectRecord(payload.remote_skills)
?? objectRecord(payload.remoteSkills)
?? objectRecord(payload.skills)
?? (looksLikeSkillPolicyMap(payload) ? payload : null);
}
function readRemoteRiskApprovalPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
const data = objectRecord(response.data) ?? response;
return objectRecord(data.risk_approval_policy)
?? objectRecord(data.riskApprovalPolicy)
?? objectRecord(data.policy)
?? (hasRiskApprovalPolicyFields(data) ? data : null);
}
function readRemoteRouteDecisionPolicy(response: Record<string, unknown>): Record<string, unknown> | null {
const data = objectRecord(response.data) ?? response;
return objectRecord(data.route_decision_policy)
?? objectRecord(data.routeDecisionPolicy)
?? objectRecord(data.policy)
?? (hasRouteDecisionPolicyFields(data) ? data : null);
}
function readRemoteApprovalUpdates(response: Record<string, unknown>): DigitalEmployeeApprovalUpdateInput[] {
const data = objectRecord(response.data) ?? response;
const raw = Array.isArray(data.approvals)
? data.approvals
: Array.isArray(data.updates)
? data.updates
: Array.isArray(data.items)
? data.items
: [];
return raw
.map((item) => objectRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item))
.map((item) => ({
approvalId: stringField(item.approval_id ?? item.approvalId ?? item.id) || null,
remoteId: stringField(item.remote_id ?? item.remoteId) || null,
planId: stringField(item.plan_id ?? item.planId) || null,
taskId: stringField(item.task_id ?? item.taskId) || null,
runId: stringField(item.run_id ?? item.runId) || null,
title: stringField(item.title) || null,
status: stringField(item.status) || null,
payload: objectRecord(item.payload) ?? item,
updatedAt: stringField(item.updated_at ?? item.updatedAt ?? item.remote_updated_at ?? item.remoteUpdatedAt) || null,
createdAt: stringField(item.created_at ?? item.createdAt) || null,
source: stringField(item.source) || "management-api",
}))
.filter((item) => Boolean(item.approvalId || item.remoteId));
}
function readRemoteNotificationUpdates(response: Record<string, unknown>): Array<{ notificationId: string | null; overlay: Record<string, unknown> }> {
const data = objectRecord(response.data) ?? response;
const raw = Array.isArray(data.notifications)
? data.notifications
: Array.isArray(data.updates)
? data.updates
: Array.isArray(data.items)
? data.items
: Array.isArray(response)
? response
: [];
return raw
.map((item) => objectRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item))
.map((item) => {
const notificationId = stringField(item.notification_id ?? item.notificationId ?? item.id) || null;
const overlay: Record<string, unknown> = {};
const status = stringField(item.status);
const readAt = stringField(item.read_at ?? item.readAt);
const dismissedAt = stringField(item.dismissed_at ?? item.dismissedAt);
const reply = stringField(item.reply ?? item.content ?? item.message);
const repliedAt = stringField(item.replied_at ?? item.repliedAt);
const remoteUpdatedAt = stringField(item.remote_updated_at ?? item.remoteUpdatedAt ?? item.updated_at ?? item.updatedAt);
const source = stringField(item.source) || "management-api";
if (status) overlay.status = status;
if (readAt) overlay.read_at = readAt;
if (dismissedAt) overlay.dismissed_at = dismissedAt;
if (reply) overlay.reply = reply;
if (repliedAt) overlay.replied_at = repliedAt;
if (remoteUpdatedAt) overlay.remote_updated_at = remoteUpdatedAt;
overlay.source = source;
return { notificationId, overlay };
});
}
function readPlanStepLeaseRejectionReason(response: Record<string, unknown>): string | null {
if (response.ok === false || response.success === false || response.accepted === false || response.granted === false) {
return stringField(response.reason ?? response.error ?? response.message) || "remote_lease_rejected";
}
const data = objectRecord(response.data);
if (data && (data.ok === false || data.success === false || data.accepted === false || data.granted === false)) {
return stringField(data.reason ?? data.error ?? data.message) || "remote_lease_rejected";
}
const status = normalizeResponseCode(response.status ?? data?.status);
if (status && ["rejected", "denied", "conflict", "locked"].includes(status.toLowerCase())) {
return stringField(response.reason ?? response.message ?? data?.reason ?? data?.message) || "remote_lease_rejected";
}
return null;
}
function readRemotePlanStepLease(
response: Record<string, unknown>,
occurredAt: string,
ttlMs: number,
): DigitalEmployeePlanStepRemoteLease | null {
const data = objectRecord(response.data) ?? response;
const lease = objectRecord(data.lease)
?? objectRecord(data.plan_step_dispatch_lease)
?? objectRecord(data.planStepDispatchLease)
?? (hasPlanStepLeaseFields(data) ? data : null);
if (!lease) return null;
const remoteLeaseId = stringField(lease.remote_lease_id ?? lease.remoteLeaseId ?? lease.lease_id ?? lease.leaseId);
const leaseId = stringField(lease.lease_id ?? lease.leaseId) || remoteLeaseId;
const ownerDeviceId = stringField(lease.owner_device_id ?? lease.ownerDeviceId) || getDeviceId();
const expiresAt = stringField(lease.expires_at ?? lease.expiresAt)
|| (ttlMs > 0 ? new Date(new Date(occurredAt).getTime() + ttlMs).toISOString() : occurredAt);
if (!leaseId || !ownerDeviceId || !expiresAt) return null;
return {
lease_id: leaseId,
state: normalizePlanStepRemoteLeaseState(lease.state),
owner_device_id: ownerDeviceId,
acquired_at: stringField(lease.acquired_at ?? lease.acquiredAt) || occurredAt,
expires_at: expiresAt,
released_at: stringField(lease.released_at ?? lease.releasedAt) || null,
release_reason: stringField(lease.release_reason ?? lease.releaseReason) || null,
remote_lease_id: remoteLeaseId || leaseId,
remote_owner_device_id: stringField(lease.remote_owner_device_id ?? lease.remoteOwnerDeviceId ?? lease.owner_device_id ?? lease.ownerDeviceId) || null,
};
}
function hasPlanStepLeaseFields(record: Record<string, unknown>): boolean {
return "lease_id" in record || "leaseId" in record || "remote_lease_id" in record || "remoteLeaseId" in record;
}
function normalizePlanStepRemoteLeaseState(value: unknown): DigitalEmployeePlanStepRemoteLease["state"] {
if (value === "released" || value === "expired") return value;
return "active";
}
function hasPlanStepDispatchPolicyFields(record: Record<string, unknown>): boolean {
return "enabled" in record || "max_per_sweep" in record || "auto_dispatch_ready_steps" in record;
}
function hasSkillPolicyWrapperFields(record: Record<string, unknown>): boolean {
return "remote_skills" in record
|| "remoteSkills" in record
|| "skills" in record
|| "skill_policies" in record
|| "skillPolicies" in record
|| "policies" in record;
}
function hasRiskApprovalPolicyFields(record: Record<string, unknown>): boolean {
return "approval_required_risk_levels" in record
|| "approvalRequiredRiskLevels" in record
|| "approval_required_actions" in record
|| "approvalRequiredActions" in record
|| "auto_reject_actions" in record
|| "autoRejectActions" in record;
}
function hasRouteDecisionPolicyFields(record: Record<string, unknown>): boolean {
return "approval_required_intents" in record
|| "approvalRequiredIntents" in record
|| "blocked_intents" in record
|| "blockedIntents" in record
|| "preferred_route_kinds" in record
|| "preferredRouteKinds" in record
|| "auto_create_governance_commands" in record
|| "autoCreateGovernanceCommands" in record;
}
function looksLikeSkillPolicyMap(record: Record<string, unknown>): boolean {
return Object.values(record).some((value) => {
if (typeof value === "boolean") return true;
const policy = objectRecord(value);
return Boolean(policy && typeof policy.enabled === "boolean");
});
}
function recordPlanStepDispatchPolicyPullFailure(
state: ReturnType<typeof readDigitalEmployeeUiState>,
currentPolicy: DigitalEmployeePlanStepDispatchPolicy,
endpoint: string | null,
error: string,
): DigitalEmployeePlanStepDispatchPolicyPullResult {
const pulledAt = new Date().toISOString();
const policy = normalizePlanStepDispatchPolicy({
...currentPolicy,
last_pulled_at: pulledAt,
last_pull_error: error,
});
saveDigitalEmployeeUiState({
action: "pull_plan_step_dispatch_policy",
metadata: {
source: "management-api",
endpoint,
ok: false,
error,
plan_step_dispatch_policy: policy,
},
state: {
...state,
planStepDispatchPolicy: policy,
},
});
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
}
function recordSkillPolicyPullFailure(
currentPolicies: DigitalEmployeeSkillPolicies,
endpoint: string | null,
error: string,
): DigitalEmployeeSkillPolicyPullResult {
const pulledAt = new Date().toISOString();
const policies = normalizeDigitalEmployeeSkillPolicies({
...currentPolicies,
last_pulled_at: pulledAt,
last_pull_error: error,
});
writeSetting(SKILL_POLICIES_SETTING_KEY, policies);
return { ok: false, endpoint, pulled_at: pulledAt, policies, error };
}
function recordRiskApprovalPolicyPullFailure(
state: ReturnType<typeof readDigitalEmployeeUiState>,
currentPolicy: DigitalEmployeeRiskApprovalPolicy,
endpoint: string | null,
error: string,
): DigitalEmployeeRiskApprovalPolicyPullResult {
const pulledAt = new Date().toISOString();
const policy = normalizeRiskApprovalPolicy({
...currentPolicy,
last_pulled_at: pulledAt,
last_pull_error: error,
});
saveDigitalEmployeeUiState({
action: "pull_risk_approval_policy",
metadata: {
source: "management-api",
endpoint,
ok: false,
error,
risk_approval_policy: policy,
},
state: {
...state,
riskApprovalPolicy: policy,
},
});
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
}
function recordRouteDecisionPolicyPullFailure(
state: ReturnType<typeof readDigitalEmployeeUiState>,
currentPolicy: DigitalEmployeeRouteDecisionPolicy,
endpoint: string | null,
error: string,
): DigitalEmployeeRouteDecisionPolicyPullResult {
const pulledAt = new Date().toISOString();
const policy = normalizeRouteDecisionPolicy({
...currentPolicy,
last_pulled_at: pulledAt,
last_pull_error: error,
});
saveDigitalEmployeeUiState({
action: "pull_route_decision_policy",
metadata: {
source: "management-api",
endpoint,
ok: false,
error,
route_decision_policy: policy,
},
state: {
...state,
routeDecisionPolicy: policy,
},
});
return { ok: false, endpoint, pulled_at: pulledAt, policy, error };
}
function recordApprovalSyncEvent(action: string, occurredAt: string, metadata: Record<string, unknown>): void {
const state = readDigitalEmployeeUiState();
saveDigitalEmployeeUiState({
action,
metadata: { ...metadata, occurred_at: occurredAt },
state,
});
}
function recordNotificationSyncEvent(action: string, occurredAt: string, metadata: Record<string, unknown>): void {
const state = readDigitalEmployeeUiState();
saveDigitalEmployeeUiState({
action,
metadata: { ...metadata, occurred_at: occurredAt },
state,
});
}
function buildSyncItem(row: OutboxRow): Record<string, unknown> {
const payload = parsePayload(row.payload);
const summary = readEntitySummary(row.entity_type, row.entity_id);
return {
outbox_id: row.id,
entity_type: row.entity_type,
entity_id: row.entity_id,
operation: row.operation,
contract_version: DIGITAL_EMPLOYEE_SYNC_CONTRACT_VERSION,
entity_summary: summary,
business_view: buildBusinessView(row, payload, summary),
payload,
};
}
function buildBusinessView(
row: OutboxRow,
payload: unknown,
summary: DigitalEmployeeSyncEntitySummary | null,
): Record<string, unknown> {
const record = objectRecord(payload) ?? {};
const base = {
entity_type: row.entity_type,
local_id: row.entity_id,
operation: row.operation,
title: stringField(record.title) || summary?.title || null,
status: stringField(record.status) || summary?.status || null,
summary: stringField(record.summary) || summary?.summary || null,
};
switch (row.entity_type) {
case "plan":
return {
...base,
objective: stringField(record.objective) || summary?.summary || null,
source: stringField(record.source) || "qimingclaw",
};
case "task":
return {
...base,
plan_id: stringField(record.planId ?? record.plan_id) || null,
assigned_skill_id: stringField(record.assignedSkillId ?? record.assigned_skill_id) || null,
};
case "plan_step":
return planStepBusinessView(base, row, record);
case "run":
return {
...base,
plan_id: stringField(record.planId ?? record.plan_id) || null,
task_id: stringField(record.taskId ?? record.task_id) || null,
started_at: stringField(record.startedAt ?? record.started_at ?? record.now) || null,
finished_at: stringField(record.finishedAt ?? record.finished_at) || null,
};
case "event":
return buildEventBusinessView(base, row, record, summary);
case "artifact":
return {
...base,
plan_id: stringField(record.planId ?? record.plan_id) || null,
task_id: stringField(record.taskId ?? record.task_id) || null,
run_id: stringField(record.runId ?? record.run_id) || null,
kind: stringField(record.kind) || summary?.status || null,
name: stringField(record.name) || summary?.title || null,
uri: stringField(record.uri) || summary?.summary || null,
};
case "approval":
return {
...base,
plan_id: stringField(record.planId ?? record.plan_id) || null,
task_id: stringField(record.taskId ?? record.task_id) || null,
run_id: stringField(record.runId ?? record.run_id) || null,
decision: stringField(record.decision) || null,
};
case "memory":
return {
...base,
key: stringField(record.key) || row.entity_id,
category: stringField(record.category) || null,
source: stringField(record.source) || null,
content_preview: compactPreview(stringField(record.content) || summary?.summary || ""),
};
default:
return base;
}
}
function planStepBusinessView(
base: Record<string, unknown>,
row: OutboxRow,
record: Record<string, unknown>,
): Record<string, unknown> {
const payload = objectRecord(record.payload) ?? {};
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
const lastDispatch = objectRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {};
const dispatchLease = objectRecord(payload.dispatchLease ?? payload.dispatch_lease) ?? {};
const dependsOn = stringArrayField(record.dependsOn ?? record.depends_on);
const blockedDependencies = stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies);
const waitingDependencies = stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies);
const leaseState = stringField(dispatchLease.state) || null;
const leaseExpiresAt = stringField(dispatchLease.expires_at ?? dispatchLease.expiresAt) || null;
const leaseActive = leaseState === "active" && leaseExpiresAt !== null && leaseExpiresAt > new Date().toISOString();
const skipReason = stringField(payload.skipReason ?? payload.skip_reason) || null;
return {
...base,
plan_id: stringField(record.planId ?? record.plan_id) || null,
step_id: row.entity_id,
seq: numberField(record.seq),
depends_on: dependsOn,
dependency_count: dependsOn.length,
dependency_status: stringField(dependencyGraph.status) || null,
blocked_dependencies: blockedDependencies,
blocked_dependency_count: blockedDependencies.length,
waiting_dependencies: waitingDependencies,
waiting_dependency_count: waitingDependencies.length,
preferred_skill_ids: stringArrayField(record.preferredSkillIds ?? record.preferred_skill_ids),
dispatchable: !leaseActive && stringField(record.status) === "ready" && blockedDependencies.length === 0 && waitingDependencies.length === 0,
skip_reason: leaseActive ? "lease_active" : skipReason,
last_dispatch_status: stringField(lastDispatch.status) || null,
lease: Object.keys(dispatchLease).length > 0 ? dispatchLease : null,
lease_id: stringField(dispatchLease.lease_id ?? dispatchLease.leaseId) || null,
lease_state: leaseState,
lease_owner_device_id: stringField(dispatchLease.owner_device_id ?? dispatchLease.ownerDeviceId) || null,
lease_acquired_at: stringField(dispatchLease.acquired_at ?? dispatchLease.acquiredAt) || null,
lease_expires_at: leaseExpiresAt,
lease_released_at: stringField(dispatchLease.released_at ?? dispatchLease.releasedAt) || null,
lease_release_reason: stringField(dispatchLease.release_reason ?? dispatchLease.releaseReason) || null,
lease_source: stringField(dispatchLease.source) || null,
remote_lease_id: stringField(dispatchLease.remote_lease_id ?? dispatchLease.remoteLeaseId) || null,
remote_lease_error: stringField(dispatchLease.remote_error ?? dispatchLease.remoteError) || null,
remote_release_error: stringField(dispatchLease.remote_release_error ?? dispatchLease.remoteReleaseError) || null,
remote_lease_rejected: skipReason === "remote_lease_rejected",
lease_active: leaseActive,
};
}
function buildEventBusinessView(
base: Record<string, unknown>,
row: OutboxRow,
record: Record<string, unknown>,
summary: DigitalEmployeeSyncEntitySummary | null,
): Record<string, unknown> {
const detail = readEventBusinessDetail(row.entity_id, record, summary);
const kind = detail.kind || "event";
return {
...base,
category: eventBusinessCategory(kind),
plan_id: detail.plan_id,
task_id: detail.task_id,
run_id: detail.run_id,
kind,
message: detail.message,
occurred_at: detail.occurred_at,
...eventSpecificBusinessView(kind, detail.payload, row.entity_id),
};
}
function readEventBusinessDetail(
eventId: string,
fallback: Record<string, unknown>,
summary: DigitalEmployeeSyncEntitySummary | null,
): EventBusinessDetail {
const db = getDigitalEmployeeDb();
const row = db?.prepare(`
SELECT plan_id, task_id, run_id, kind, message, payload, occurred_at
FROM digital_events
WHERE id = ?
`).get(eventId) as Record<string, unknown> | undefined;
const eventPayload = parsePayload(stringField(row?.payload) || "{}");
const fallbackPayload = objectRecord(fallback.payload) ?? fallback;
return {
plan_id: stringField(row?.plan_id ?? fallback.plan_id ?? fallback.planId) || null,
task_id: stringField(row?.task_id ?? fallback.task_id ?? fallback.taskId) || null,
run_id: stringField(row?.run_id ?? fallback.run_id ?? fallback.runId) || null,
kind: stringField(row?.kind ?? fallback.kind) || summary?.status || null,
message: stringField(row?.message ?? fallback.message) || summary?.summary || null,
payload: objectRecord(eventPayload) ?? fallbackPayload,
occurred_at: stringField(row?.occurred_at ?? fallback.occurred_at ?? fallback.occurredAt) || null,
};
}
function eventSpecificBusinessView(
kind: string,
payload: Record<string, unknown>,
eventId: string,
): Record<string, unknown> {
if (kind === "route_decision") return routeDecisionBusinessView(payload, eventId);
if (kind === "route_decision_blocked" || kind === "route_decision_approval_required") return routeDecisionPolicyBusinessView(payload);
if (kind === "route_decision_intervention_resolved") return routeDecisionInterventionBusinessView(payload);
if (kind === "approval_decision") return approvalDecisionBusinessView(payload);
if (kind.startsWith("approval_action_")) return approvalActionBusinessView(payload);
if (isApprovalSyncEvent(kind)) return approvalSyncBusinessView(payload);
if (isApprovalSubscriptionEvent(kind)) return approvalSubscriptionBusinessView(payload);
if (kind.startsWith("artifact_access_") || kind.startsWith("artifact_retention_") || kind === "artifact_version_observed") {
return artifactEventBusinessView(payload);
}
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
if (kind === "risk_approval_required" || kind === "risk_policy_rejected") return riskPolicyBusinessView(payload);
if (isPlanStepDependencyEvent(kind)) return planStepDependencyBusinessView(payload);
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
if (isPlanStepLeaseEvent(kind)) return planStepLeaseBusinessView(payload);
if (isPlanStepDispatchPolicyEvent(kind)) return planStepDispatchPolicyBusinessView(payload);
if (kind === "governance_provide_task_input") return governanceProvideTaskInputBusinessView(payload);
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
return {};
}
function routeDecisionBusinessView(payload: Record<string, unknown>, eventId: string): Record<string, unknown> {
const decision = objectRecord(payload.routeDecision ?? payload.route_decision) ?? payload;
const policyResult = stringField(decision.policy_result ?? decision.policyResult) || "accepted";
const reasonCodes = stringArrayField(decision.reason_codes ?? decision.reasonCodes);
const createdCommandId = stringField(decision.created_command_id ?? decision.createdCommandId) || null;
return {
route_decision_id: stringField(decision.id) || eventId,
route_kind: stringField(decision.route_kind ?? decision.routeKind) || "ChatOnly",
intent_kind: stringField(decision.intent_kind ?? decision.intentKind) || "chat",
policy_result: policyResult,
reason_codes: reasonCodes,
candidate_skills: stringArrayField(decision.candidate_skills ?? decision.candidateSkills),
created_command_id: createdCommandId,
policy_source: stringField(decision.policy_source ?? decision.policySource) || null,
blocked_reason: stringField(decision.blocked_reason ?? decision.blockedReason) || null,
approval_id: stringField(decision.approval_id ?? decision.approvalId) || null,
matched_intent: stringField(decision.matched_intent ?? decision.matchedIntent) || null,
attention_level: routeDecisionAttentionLevel(policyResult, reasonCodes, createdCommandId),
};
}
function routeDecisionPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const policyResult = stringField(payload.policy_result ?? payload.decision) || stringField(payload.status) || "blocked";
const reasonCodes = stringArrayField(payload.reason_codes ?? payload.reasonCodes);
return {
route_kind: stringField(payload.route_kind ?? payload.routeKind) || "ChatOnly",
intent_kind: stringField(payload.intent_kind ?? payload.intentKind) || "chat",
policy_result: policyResult,
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
blocked_reason: stringField(payload.blocked_reason ?? payload.blockedReason) || null,
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
matched_intent: stringField(payload.matched_intent ?? payload.matchedIntent) || null,
reason_codes: reasonCodes,
candidate_skills: stringArrayField(payload.candidate_skills ?? payload.candidateSkills),
attention_level: policyResult === "approval_required" ? "action" : "error",
};
}
function routeDecisionInterventionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
route_decision_id: stringField(payload.route_decision_id ?? payload.routeDecisionId) || null,
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
route_kind: stringField(payload.route_kind ?? payload.routeKind) || "ChatOnly",
intent_kind: stringField(payload.intent_kind ?? payload.intentKind) || "chat",
decision: stringField(payload.decision) || null,
status: stringField(payload.status) || null,
policy_result: stringField(payload.policy_result ?? payload.policyResult) || null,
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
blocked_reason: stringField(payload.blocked_reason ?? payload.blockedReason) || null,
matched_intent: stringField(payload.matched_intent ?? payload.matchedIntent) || null,
created_command_id: stringField(payload.created_command_id ?? payload.createdCommandId) || null,
command_accepted: payload.command_accepted === true || payload.commandAccepted === true,
command_error: stringField(payload.command_error ?? payload.commandError) || null,
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
attention_level: stringField(payload.command_error ?? payload.commandError) ? "error" : "action",
};
}
function approvalActionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const comment = objectRecord(payload.comment_summary);
const delegate = objectRecord(payload.delegate_summary);
const sla = objectRecord(payload.sla_summary);
const risk = objectRecord(payload.risk_summary);
const workflow = objectRecord(payload.workflow) ?? {};
return {
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
action: stringField(payload.action) || null,
status: stringField(payload.status) || null,
actor: stringField(payload.actor) || null,
workflow_id: stringField(workflow.workflow_id ?? workflow.workflowId) || null,
step_id: stringField(payload.step_id ?? payload.stepId) || null,
step_status: stringField(payload.step_status ?? payload.stepStatus) || null,
participant_id: stringField(payload.participant_id ?? payload.participantId) || null,
participant_label: stringField(payload.participant_label ?? payload.participantLabel) || null,
risk_level: stringField(risk?.risk_level ?? payload.risk_level) || null,
due_at: stringField(sla?.due_at ?? payload.due_at) || null,
delegate_to: stringField(delegate?.target ?? delegate?.label ?? payload.delegate_to) || null,
comment_length: numberField(comment?.length ?? payload.comment_length),
};
}
function approvalDecisionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const workflow = objectRecord(payload.workflow) ?? {};
return {
approval_id: stringField(payload.approvalId ?? payload.approval_id) || null,
action: "decision",
decision: stringField(payload.decision) || null,
previous_status: stringField(payload.previousStatus ?? payload.previous_status) || null,
next_status: stringField(payload.nextStatus ?? payload.next_status) || null,
workflow_id: stringField(workflow.workflow_id ?? workflow.workflowId) || null,
step_id: stringField(payload.step_id ?? payload.stepId) || null,
step_status: stringField(payload.step_status ?? payload.stepStatus) || null,
current_step_id: stringField(payload.current_step_id ?? payload.currentStepId) || null,
next_step_id: stringField(payload.next_step_id ?? payload.nextStepId) || null,
participant_id: stringField(payload.participant_id ?? payload.participantId) || null,
participant_label: stringField(payload.participant_label ?? payload.participantLabel) || null,
actor: stringField(payload.actor ?? payload.source) || null,
decided_at: stringField(payload.decidedAt ?? payload.decided_at) || null,
plan_id: stringField(payload.planId ?? payload.plan_id) || null,
task_id: stringField(payload.taskId ?? payload.task_id) || null,
run_id: stringField(payload.runId ?? payload.run_id) || null,
};
}
function approvalSyncBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const metadata = objectRecord(payload.metadata) ?? payload;
return {
action: stringField(metadata.action ?? payload.action) || null,
approval_id: stringField(metadata.approval_id ?? metadata.approvalId) || null,
decision: stringField(metadata.decision) || null,
source: stringField(metadata.source) || null,
endpoint: stringField(metadata.endpoint) || null,
ok: typeof metadata.ok === "boolean" ? metadata.ok : null,
applied: numberField(metadata.applied),
ignored: numberField(metadata.ignored),
error: stringField(metadata.error) || null,
};
}
function approvalSubscriptionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const metadata = objectRecord(payload.metadata) ?? {};
const state = objectRecord(payload.state) ?? {};
const preferences = objectRecord(metadata.approval_subscriptions)
?? objectRecord(state.approvalSubscriptionPreferences)
?? {};
return {
action: stringField(metadata.action ?? payload.action) || "update_approval_subscriptions",
preferences_enabled: typeof preferences.enabled === "boolean" ? preferences.enabled : null,
subscribed_actions: stringArrayField(preferences.subscribed_actions ?? preferences.subscribedActions),
subscribed_risk_levels: stringArrayField(preferences.subscribed_risk_levels ?? preferences.subscribedRiskLevels),
notify_on_timeout: typeof preferences.notify_on_timeout === "boolean" ? preferences.notify_on_timeout : null,
updated_at: stringField(preferences.updated_at ?? preferences.updatedAt) || null,
};
}
function riskPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
action_kind: stringField(payload.action_kind ?? payload.actionKind) || null,
action_label: stringField(payload.action_label ?? payload.actionLabel) || null,
risk_level: stringField(payload.risk_level ?? payload.riskLevel) || null,
decision: stringField(payload.decision) || null,
status: stringField(payload.status) || null,
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
policy_source: stringField(payload.policy_source ?? payload.policySource) || null,
approval_id: stringField(payload.approval_id ?? payload.approvalId) || null,
correlation_id: stringField(payload.correlation_id ?? payload.correlationId) || null,
actor: stringField(payload.actor) || null,
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
task_id: stringField(payload.task_id ?? payload.taskId) || null,
run_id: stringField(payload.run_id ?? payload.runId) || null,
};
}
function artifactEventBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const retention = objectRecord(payload.retention_summary);
return {
artifact_id: stringField(payload.artifact_id ?? payload.artifactId) || null,
action: stringField(payload.action) || null,
status: stringField(payload.status) || null,
retention_status: stringField(payload.retention_status ?? retention?.retention_status) || null,
retention_until: stringField(payload.retention_until ?? retention?.retention_until) || null,
actor: stringField(payload.actor) || null,
};
}
function skillCallBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
call_id: stringField(payload.call_id ?? payload.callId) || null,
source: stringField(payload.source) || null,
server_id: stringField(payload.server_id ?? payload.serverId) || null,
tool_name: stringField(payload.tool_name ?? payload.toolName) || null,
skill_id: stringField(payload.skill_id ?? payload.skillId) || null,
decision: stringField(payload.decision) || null,
reason_codes: stringArrayField(payload.reason_codes ?? payload.reasonCodes),
};
}
function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
return {
step_id: stringField(payload.step_id ?? payload.stepId) || null,
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
task_id: stringField(payload.task_id ?? payload.taskId) || null,
run_id: stringField(payload.run_id ?? payload.runId) || null,
status: stringField(payload.status) || null,
reason: stringField(payload.reason) || null,
trigger_source: stringField(payload.trigger_source ?? payload.triggerSource) || null,
orchestration_id: stringField(payload.orchestration_id ?? payload.orchestrationId) || null,
};
}
function planStepLeaseBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const lease = objectRecord(payload.lease) ?? payload;
return {
step_id: stringField(payload.step_id ?? payload.stepId) || null,
plan_id: stringField(payload.plan_id ?? payload.planId) || null,
task_id: stringField(payload.task_id ?? payload.taskId) || null,
run_id: stringField(payload.run_id ?? payload.runId) || null,
lease_id: stringField(payload.lease_id ?? payload.leaseId ?? lease.lease_id ?? lease.leaseId) || null,
lease_state: stringField(payload.lease_state ?? payload.leaseState ?? lease.state) || null,
lease_owner_device_id: stringField(payload.lease_owner_device_id ?? payload.leaseOwnerDeviceId ?? lease.owner_device_id ?? lease.ownerDeviceId) || null,
lease_acquired_at: stringField(lease.acquired_at ?? lease.acquiredAt) || null,
lease_expires_at: stringField(payload.lease_expires_at ?? payload.leaseExpiresAt ?? lease.expires_at ?? lease.expiresAt) || null,
lease_released_at: stringField(lease.released_at ?? lease.releasedAt) || null,
lease_release_reason: stringField(lease.release_reason ?? lease.releaseReason) || null,
lease_source: stringField(payload.lease_source ?? payload.leaseSource ?? lease.source) || null,
remote_lease_id: stringField(payload.remote_lease_id ?? payload.remoteLeaseId ?? lease.remote_lease_id ?? lease.remoteLeaseId) || null,
remote_lease_error: stringField(payload.remote_lease_error ?? payload.remoteLeaseError ?? lease.remote_error ?? lease.remoteError) || null,
remote_release_error: stringField(payload.remote_release_error ?? payload.remoteReleaseError ?? lease.remote_release_error ?? lease.remoteReleaseError) || null,
remote_lease_rejected: payload.remote_lease_rejected === true || stringField(payload.reason) === "remote_lease_rejected",
reason: stringField(payload.reason) || null,
};
}
function planStepDependencyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const dependencyGraph = objectRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
const dependencies = stringArrayField(payload.dependsOn ?? payload.depends_on);
return {
step_id: stringField(payload.stepId ?? payload.step_id) || null,
plan_id: stringField(payload.planId ?? payload.plan_id) || null,
status: stringField(payload.status ?? dependencyGraph.status) || null,
dependency_count: dependencies.length,
blocked_dependencies: stringArrayField(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies),
waiting_dependencies: stringArrayField(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies),
satisfied_dependencies: stringArrayField(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies),
};
}
function planStepDispatchPolicyBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const metadata = objectRecord(payload.metadata) ?? {};
const state = objectRecord(payload.state) ?? {};
const policy = objectRecord(metadata.plan_step_dispatch_policy)
?? objectRecord(state.planStepDispatchPolicy)
?? {};
return {
action: stringField(metadata.action ?? payload.action) || "update_plan_step_dispatch_policy",
source: stringField(metadata.source ?? policy.source) || null,
pull_ok: typeof metadata.ok === "boolean" ? metadata.ok : null,
pull_error: stringField(metadata.error ?? policy.last_pull_error) || null,
pull_endpoint: stringField(metadata.endpoint) || null,
policy_enabled: typeof policy.enabled === "boolean" ? policy.enabled : null,
max_per_sweep: numberField(policy.max_per_sweep),
auto_dispatch_ready_steps: typeof policy.auto_dispatch_ready_steps === "boolean" ? policy.auto_dispatch_ready_steps : null,
updated_at: stringField(policy.updated_at) || null,
remote_updated_at: stringField(policy.remote_updated_at) || null,
last_pulled_at: stringField(policy.last_pulled_at) || null,
};
}
function governanceProvideTaskInputBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const commandPayload = objectRecord(payload.payload) ?? {};
const result = objectRecord(payload.result) ?? {};
const input = stringField(commandPayload.input ?? payload.input);
return {
command_kind: stringField(payload.commandKind ?? payload.command_kind) || "provide_task_input",
plan_id: stringField(payload.planId ?? payload.plan_id ?? commandPayload.plan_id ?? result.plan_id) || null,
task_id: stringField(payload.taskId ?? payload.task_id ?? commandPayload.task_id ?? result.task_id) || null,
source: stringField(commandPayload.source ?? result.source ?? payload.source) || null,
notification_id: stringField(commandPayload.notification_id ?? payload.notification_id) || null,
input_length: input ? input.length : null,
};
}
function notificationBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
const metadata = objectRecord(payload.metadata) ?? {};
const state = objectRecord(payload.state) ?? {};
const preferences = objectRecord(metadata.notification_preferences) ?? objectRecord(state.notificationPreferences) ?? {};
const overlays = objectRecord(state.notificationStateById) ?? {};
const notificationId = stringField(metadata.notification_id ?? payload.notification_id ?? payload.notificationId) || null;
const overlay = notificationId ? objectRecord(overlays[notificationId]) ?? {} : {};
const reply = stringField(overlay.reply ?? metadata.reply ?? payload.reply);
return {
notification_id: notificationId,
action: stringField(metadata.action ?? payload.action) || null,
notification_action: stringField(metadata.notification_action ?? payload.notification_action) || null,
status: stringField(overlay.status ?? payload.status) || null,
read_at: stringField(overlay.read_at ?? payload.read_at ?? payload.readAt) || null,
dismissed_at: stringField(overlay.dismissed_at ?? payload.dismissed_at ?? payload.dismissedAt) || null,
replied_at: stringField(overlay.replied_at ?? payload.replied_at ?? payload.repliedAt) || null,
source: stringField(metadata.source) || null,
endpoint: stringField(metadata.endpoint) || null,
ok: typeof metadata.ok === "boolean" ? metadata.ok : null,
applied: numberField(metadata.applied),
ignored: numberField(metadata.ignored),
error: stringField(metadata.error) || null,
reply_length: numberField(metadata.reply_length) ?? (reply ? reply.length : null),
preferences_enabled: typeof preferences.enabled === "boolean" ? preferences.enabled : null,
muted_kinds: stringArrayField(preferences.muted_kinds ?? preferences.mutedKinds),
muted_levels: stringArrayField(preferences.muted_levels ?? preferences.mutedLevels),
};
}
function eventBusinessCategory(kind: string): string {
if (kind === "route_decision" || kind === "route_decision_blocked" || kind === "route_decision_approval_required" || kind === "route_decision_intervention_resolved") return "route";
if (kind.startsWith("approval_")) return "approval";
if (kind.startsWith("risk_")) return "approval";
if (isApprovalSyncEvent(kind)) return "approval";
if (isApprovalSubscriptionEvent(kind)) return "approval";
if (kind.startsWith("artifact_")) return "artifact";
if (kind.startsWith("skill_call_")) return "skill";
if (isPlanStepDependencyEvent(kind)) return "dispatch";
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
if (isPlanStepLeaseEvent(kind)) return "dispatch";
if (isPlanStepDispatchPolicyEvent(kind)) return "dispatch";
if (isNotificationEvent(kind)) return "notification";
if (kind.startsWith("governance_")) return "governance";
return "runtime";
}
function isApprovalSubscriptionEvent(kind: string): boolean {
return kind === "digital_workday_update_approval_subscriptions";
}
function isApprovalSyncEvent(kind: string): boolean {
return kind === "digital_workday_approval_updates_pulled"
|| kind === "digital_workday_approval_updates_pull_failed"
|| kind === "digital_workday_approval_decision_submitted"
|| kind === "digital_workday_approval_decision_submit_failed";
}
function isPlanStepDependencyEvent(kind: string): boolean {
return kind === "plan_step_ready" || kind === "plan_step_blocked" || kind === "plan_step_waiting";
}
function isPlanStepDispatchPolicyEvent(kind: string): boolean {
return kind === "digital_workday_update_plan_step_dispatch_policy"
|| kind === "digital_workday_pull_plan_step_dispatch_policy";
}
function isPlanStepLeaseEvent(kind: string): boolean {
return kind === "plan_step_lease_acquired" || kind === "plan_step_lease_released" || kind === "plan_step_lease_skipped";
}
function isNotificationEvent(kind: string): boolean {
return kind === "digital_workday_update_notification_preferences"
|| kind === "digital_workday_notification_read"
|| kind === "digital_workday_notification_dismiss"
|| kind === "digital_workday_notification_reply"
|| kind === "digital_workday_notification_updates_pulled"
|| kind === "digital_workday_notification_updates_pull_failed"
|| kind === "digital_workday_notification_action_submitted"
|| kind === "digital_workday_notification_action_submit_failed";
}
function routeDecisionAttentionLevel(policyResult: string, reasonCodes: string[], createdCommandId: string | null): "info" | "action" | "warn" | "error" {
if (policyResult && policyResult !== "accepted") return "error";
if (reasonCodes.includes("candidate_skills_disabled") || reasonCodes.includes("route_action_missing_context")) return "warn";
if (createdCommandId) return "action";
return "info";
}
function compactPreview(value: string, maxLength = 120): string | null {
const compact = value.replace(/\s+/g, " ").trim();
if (!compact) return null;
return compact.length > maxLength ? `${compact.slice(0, maxLength)}...` : compact;
}
function markRowsSyncing(rows: OutboxRow[], now: string): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
const stmt = db.prepare(`
UPDATE digital_sync_outbox
SET status = 'syncing', attempts = attempts + 1, last_attempt_at = ?, updated_at = ?
WHERE id = ?
`);
for (const row of rows) stmt.run(now, now, row.id);
});
tx();
}
function markRowsSynced(
acks: SyncAck[],
now: string,
sourceRows: OutboxRow[] = [],
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
const outboxStmt = db.prepare(`
UPDATE digital_sync_outbox
SET status = 'synced', error = NULL, updated_at = ?
WHERE id = ?
`);
for (const ack of acks) {
const outboxId =
ack.outbox_id ||
ack.id ||
sourceRows.find((row) => ackMatchesRow(ack, row))?.id;
if (!outboxId) continue;
outboxStmt.run(now, outboxId);
updateEntitySyncStatus(ack, now);
}
});
tx();
}
function ackMatchesRow(ack: SyncAck, row: OutboxRow): boolean {
const outboxId = ack.outbox_id || ack.id;
if (outboxId) return outboxId === row.id;
return ack.entity_type === row.entity_type && ack.entity_id === row.entity_id;
}
function recordSyncAttempts(
rows: OutboxRow[],
config: DigitalEmployeeSyncConfig,
status: "synced" | "failed",
error: string | null,
startedAt: string,
finishedAt: string,
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
const stmt = db.prepare(`
INSERT INTO digital_sync_attempts (
outbox_id, entity_type, entity_id, operation, attempt_no,
status, error, endpoint, started_at, finished_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const row of rows) {
stmt.run(
row.id,
row.entity_type,
row.entity_id,
row.operation,
row.attempts + 1,
status,
error,
config.endpoint,
startedAt,
finishedAt,
);
}
db.prepare("DELETE FROM digital_sync_attempts WHERE finished_at < ?").run(
new Date(Date.parse(finishedAt) - ATTEMPT_HISTORY_RETENTION_MS).toISOString(),
);
});
tx();
}
function updateEntitySyncStatus(ack: SyncAck, now: string): void {
if (!ack.entity_type || !ack.entity_id) return;
const table = entityTable(ack.entity_type);
if (!table) return;
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
UPDATE ${table}
SET sync_status = 'synced',
remote_id = COALESCE(?, remote_id),
last_synced_at = ?,
sync_error = NULL
WHERE id = ?
`).run(ack.remote_id || null, now, ack.entity_id);
}
function markRowsFailed(rows: OutboxRow[], now: string, error: string): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
const stmt = db.prepare(`
UPDATE digital_sync_outbox
SET status = 'failed', error = ?, updated_at = ?
WHERE id = ?
`);
for (const row of rows) {
stmt.run(error, now, row.id);
markEntityFailed(row.entity_type, row.entity_id, error);
}
});
tx();
}
function markEntityFailed(entityType: string, entityId: string, error: string): void {
const table = entityTable(entityType);
const db = getDigitalEmployeeDb();
if (!db || !table) return;
db.prepare(`
UPDATE ${table}
SET sync_status = 'failed', sync_error = ?
WHERE id = ?
`).run(error, entityId);
}
function countOutbox(status: string): number {
const db = getDigitalEmployeeDb();
if (!db) return 0;
const row = db
.prepare("SELECT COUNT(*) AS count FROM digital_sync_outbox WHERE status = ?")
.get(status) as { count: number } | undefined;
return row?.count ?? 0;
}
function readFailureSummary(): DigitalEmployeeSyncFailureSummary {
const db = getDigitalEmployeeDb();
if (!db) return { total: 0, dueForRetry: 0, backoff: 0, byEntityType: [] };
const rows = db
.prepare(
`
SELECT entity_type, attempts, last_attempt_at
FROM digital_sync_outbox
WHERE status = 'failed'
`,
)
.all() as Array<{
entity_type: string;
attempts: number;
last_attempt_at: string | null;
}>;
const groups = new Map<string, number>();
let dueForRetry = 0;
for (const row of rows) {
groups.set(row.entity_type, (groups.get(row.entity_type) || 0) + 1);
const nextRetryAt = nextRetryAtForFailure(row.last_attempt_at, row.attempts);
if (!nextRetryAt || Date.now() >= Date.parse(nextRetryAt)) {
dueForRetry += 1;
}
}
return {
total: rows.length,
dueForRetry,
backoff: rows.length - dueForRetry,
byEntityType: Array.from(groups.entries())
.map(([entityType, count]) => ({ entityType, count }))
.sort((left, right) => right.count - left.count),
};
}
function readRecentFailures(
config: DigitalEmployeeSyncConfig,
limit = 3,
): DigitalEmployeeSyncFailure[] {
const db = getDigitalEmployeeDb();
if (!db) return [];
const rows = db
.prepare(
`
SELECT id, entity_type, entity_id, operation, attempts,
last_attempt_at, error, created_at, updated_at
FROM digital_sync_outbox
WHERE status = 'failed'
ORDER BY COALESCE(last_attempt_at, updated_at, created_at) DESC
LIMIT ?
`,
)
.all(limit) as Array<{
id: string;
entity_type: string;
entity_id: string;
operation: string;
attempts: number;
last_attempt_at: string | null;
error: string | null;
created_at: string;
updated_at: string;
}>;
return rows.map((row) => {
const nextRetryAt = nextRetryAtForFailure(row.last_attempt_at, row.attempts);
return {
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,
nextRetryAt,
dueForRetry: !nextRetryAt || Date.now() >= Date.parse(nextRetryAt),
managementRecordUrl: buildManagementRecordUrl(config, row),
attemptHistory: readAttemptHistory(row.id),
error: row.error,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
});
}
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 "plan_step":
return "SELECT title, status, NULL AS objective, payload FROM digital_plan_steps WHERE id = ?";
case "schedule":
return "SELECT name AS title, status, description AS objective, payload FROM digital_schedules WHERE id = ?";
case "schedule_run":
return "SELECT id AS title, status, error AS objective, payload FROM digital_schedule_runs 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 = ?";
case "memory":
return "SELECT key AS title, status, content AS summary, payload FROM digital_memories WHERE id = ?";
default:
return null;
}
}
function stringField(value: unknown): string {
return typeof value === "string" && value.trim() ? value.trim() : "";
}
function stringArrayField(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map(stringField).filter(Boolean);
}
function numberField(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function objectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function readAttemptHistory(
outboxId: string,
limit = 5,
): DigitalEmployeeSyncAttempt[] {
const db = getDigitalEmployeeDb();
if (!db) return [];
const rows = db
.prepare(
`
SELECT attempt_no, status, error, endpoint, started_at, finished_at
FROM digital_sync_attempts
WHERE outbox_id = ?
ORDER BY finished_at DESC, id DESC
LIMIT ?
`,
)
.all(outboxId, limit) as Array<{
attempt_no: number;
status: string;
error: string | null;
endpoint: string | null;
started_at: string;
finished_at: string;
}>;
return rows.map((row) => ({
attemptNo: row.attempt_no,
status: row.status,
error: row.error,
endpoint: row.endpoint,
startedAt: row.started_at,
finishedAt: row.finished_at,
}));
}
function buildManagementRecordUrl(
config: DigitalEmployeeSyncConfig,
row: {
id: string;
entity_type: string;
entity_id: string;
},
): string | null {
if (!config.endpoint) return null;
try {
const endpoint = new URL(config.endpoint);
const url = new URL(MANAGEMENT_SYNC_RECORD_PATH, endpoint.origin);
url.searchParams.set("device_id", getDeviceId());
url.searchParams.set("entity_type", row.entity_type);
url.searchParams.set("entity_id", row.entity_id);
url.searchParams.set("outbox_id", row.id);
return url.toString();
} catch {
return null;
}
}
function nextRetryAtForFailure(
lastAttemptAt: string | null,
attempts: number,
): string | null {
if (!lastAttemptAt) return null;
const lastAttemptTime = Date.parse(lastAttemptAt);
if (!Number.isFinite(lastAttemptTime)) return null;
return new Date(lastAttemptTime + retryDelayMs(attempts)).toISOString();
}
function entityTable(entityType: string): string | null {
switch (entityType) {
case "plan":
return "digital_plans";
case "task":
return "digital_tasks";
case "plan_step":
return "digital_plan_steps";
case "schedule":
return "digital_schedules";
case "schedule_run":
return "digital_schedule_runs";
case "run":
return "digital_runs";
case "event":
return "digital_events";
case "artifact":
return "digital_artifacts";
case "approval":
return "digital_approvals";
case "memory":
return "digital_memories";
default:
return null;
}
}
function parsePayload(payload: string): unknown {
try {
return JSON.parse(payload);
} catch {
return payload;
}
}
function normalizeError(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}