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 | 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 { 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; 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 { 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 { 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 { 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 { 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 { 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, ): Promise { 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 { 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 = { ...(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, ): Promise { 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 { 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 { 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 { 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; 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 { 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; } async function fetchJsonWithAuth( config: DigitalEmployeeSyncConfig, endpoint: string, requestBody?: Record, ): Promise> { 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) : {}; if (!response.ok) { throw new Error(stringField(responseBody.message) || `HTTP ${response.status}`); } return responseBody; } finally { clearTimeout(timer); } } function readRemotePlanStepDispatchPolicy(response: Record): Record | 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): Record | 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): Record | null { return objectRecord(payload.remote_skills) ?? objectRecord(payload.remoteSkills) ?? objectRecord(payload.skills) ?? (looksLikeSkillPolicyMap(payload) ? payload : null); } function readRemoteRiskApprovalPolicy(response: Record): Record | 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): Record | 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): 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 => 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): Array<{ notificationId: string | null; overlay: Record }> { 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 => Boolean(item)) .map((item) => { const notificationId = stringField(item.notification_id ?? item.notificationId ?? item.id) || null; const overlay: Record = {}; 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 | 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, 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): 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): boolean { return "enabled" in record || "max_per_sweep" in record || "auto_dispatch_ready_steps" in record; } function hasSkillPolicyWrapperFields(record: Record): 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): 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): 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): 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, 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, 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, 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): void { const state = readDigitalEmployeeUiState(); saveDigitalEmployeeUiState({ action, metadata: { ...metadata, occurred_at: occurredAt }, state, }); } function recordNotificationSyncEvent(action: string, occurredAt: string, metadata: Record): void { const state = readDigitalEmployeeUiState(); saveDigitalEmployeeUiState({ action, metadata: { ...metadata, occurred_at: occurredAt }, state, }); } function buildSyncItem(row: OutboxRow): Record { 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 { 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, row: OutboxRow, record: Record, ): Record { 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, row: OutboxRow, record: Record, summary: DigitalEmployeeSyncEntitySummary | null, ): Record { 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, 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 | 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, eventId: string, ): Record { 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, eventId: string): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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): Record { 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(); 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 | undefined; if (!row) return null; const payload = parsePayload(String(row.payload ?? "{}")); const payloadSummary = payload && typeof payload === "object" && !Array.isArray(payload) ? payload as Record : {}; 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 | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : 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); }