Q195: a change reloads only what is not already shown, and the snapshot stops carrying records the window never reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 09:20:45 -04:00
1 parent 2f79e4261f
commit 56eac39b1b
31 files changed
+146 -214

No files matched your search

+13 -6
View File
@@ -1,3 +1,4 @@
import { bumpDataVersion, currentDataVersion } from "../services/data-version";
import { createIpcGate } from "./ipc-gate";
import { app, dialog, shell, type BrowserWindow } from "electron";
import type { DatabaseConnection } from "../database/connection";
@@ -69,13 +70,18 @@ export function registerIpcHandlers(deps: IpcDeps): void {
const integrationKeys = new IntegrationKeyStore(deps.connection.db);
const providerOAuth = new ProviderOAuthService(integrationKeys);
const skills = new SkillService(deps.connection.db, deps.artifactRoot);
const notifyDataChanged = coalesce(() => {
const announceChange = coalesce(() => {
try {
if (!deps.mainWindow.isDestroyed() && !deps.mainWindow.webContents.isDestroyed()) {
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" });
deps.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed", version: currentDataVersion() });
}
} catch { /* renderer may have been disposed during agent run */ }
}, 200);
// The version moves at once, so the snapshot an action returns already carries the change its own event announces.
const notifyDataChanged = () => {
bumpDataVersion();
announceChange();
};
const taskExecution = new TaskExecutionService(deps.connection.db, providers, skills, notifyDataChanged);
const social = new SocialService(deps.connection.db);
const files = new FileService(deps.connection.db, deps.artifactRoot);
@@ -456,11 +462,12 @@ export function registerIpcHandlers(deps: IpcDeps): void {
handle("disconnectComposioToolkit", async (input) => {
try {
await composioConnections.disconnectToolkit(input.orgId, input.toolkitSlug, input.label);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("composio_disconnect_failed", error instanceof Error ? error.message : String(error));
} finally {
// A disconnect that failed partway may still have written; the page reloads either way.
notifyDataChanged();
return fail("composio_disconnect_failed", error instanceof Error ? error.message : String(error));
}
});
handle("grantConnector", (input) => mutate(() => new ConnectorGrantsService(deps.connection.db).grantFromBoard(input)));
@@ -478,11 +485,11 @@ export function registerIpcHandlers(deps: IpcDeps): void {
handle("refreshComposioAccounts", async (input) => {
try {
await composioConnections.refreshAccounts(input.orgId);
notifyDataChanged();
return ok(snapshot.load());
} catch (error) {
return fail("composio_refresh_failed", error instanceof Error ? error.message : String(error));
} finally {
notifyDataChanged();
return fail("composio_refresh_failed", error instanceof Error ? error.message : String(error));
}
});
handle("windowMinimize", () => deps.mainWindow.minimize());
+2 -1
View File
@@ -1,3 +1,4 @@
import { bumpDataVersion } from "../services/data-version";
import type { BrowserWindow } from "electron";
import type { DatabaseSync } from "node:sqlite";
import { IPC_CHANNELS } from "../../shared/ipc";
@@ -22,7 +23,7 @@ export function startProviderBackgroundWorker(input: ProviderBackgroundWorkerInp
try {
const changedCount = await service.recoverBackgroundResponses();
if (changedCount > 0 && !input.mainWindow.isDestroyed()) {
input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" });
input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed", version: bumpDataVersion() });
}
} finally {
running = false;
+2 -1
View File
@@ -1,3 +1,4 @@
import { bumpDataVersion } from "../services/data-version";
import type { BrowserWindow } from "electron";
import type { DatabaseConnection } from "../database/connection";
import { IPC_CHANNELS } from "../../shared/ipc";
@@ -80,7 +81,7 @@ export function claimNextQueuedRun(connection: DatabaseConnection, excludedProvi
export function startTaskQueueWorker(input: TaskQueueWorkerInput): () => void {
const notifyDataChanged = () => {
if (!input.mainWindow.isDestroyed()) {
input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed" });
input.mainWindow.webContents.send(IPC_CHANNELS.rendererEvent, { type: "snapshot-changed", version: bumpDataVersion() });
}
};
const taskExecution = input.taskExecution ?? new TaskExecutionService(input.connection.db, undefined, undefined, notifyDataChanged);
+11
View File
@@ -0,0 +1,11 @@
let version = 0;
/** Marks a change the window should show; snapshots and change events carry the count so the window never reloads what it already has. */
export function bumpDataVersion(): number {
version += 1;
return version;
}
export function currentDataVersion(): number {
return version;
}
+2 -31
View File
@@ -1,3 +1,4 @@
import { currentDataVersion } from "./data-version";
import { loadViewState } from "./view-state";
import { loadOnboardingDraft } from "./onboarding-progress";
import type { DatabaseSync, StatementSync, SQLInputValue } from "node:sqlite";
@@ -71,32 +72,17 @@ export class SnapshotService {
.map((provider) => ({ ...provider, enabled: toBool(provider.enabled), backgroundEnabled: toBool(provider.backgroundEnabled) }));
const providerAuthProfiles = list<AppSnapshot["providerAuthProfiles"][number]>(this.db, "provider_auth_profiles", "created_at desc")
.map((profile) => ({ ...profile, active: toBool(profile.active) }));
const providerAuthSessions = list<AppSnapshot["providerAuthSessions"][number]>(this.db, "provider_auth_sessions", "created_at desc");
const providerRateLimitObservations = list<AppSnapshot["providerRateLimitObservations"][number]>(
this.db,
"provider_rate_limit_observations",
"observed_at desc",
);
const providerUsageWindows = list<ProviderUsageWindowRow>(this.db, "provider_usage_windows", "window_start desc");
const providerRateLimits = buildProviderRateLimits(providers, providerUsageWindows);
const files = list<AppSnapshot["files"][number]>(this.db, "file_objects", "created_at desc")
.map((file) => ({ ...file, pinned: toBool(file.pinned) }));
const legalHolds = list<AppSnapshot["legalHolds"][number]>(this.db, "legal_holds", "created_at desc")
.map((hold) => ({ ...hold, active: toBool(hold.active) }));
const retentionFreezes = list<AppSnapshot["retentionFreezes"][number]>(this.db, "retention_freezes", "created_at desc")
.map((freeze) => ({ ...freeze, active: toBool(freeze.active) }));
const employeeIdentities = list<AppSnapshot["employeeIdentities"][number]>(this.db, "employee_identities", "created_at desc")
.map((identity) => ({ ...identity, active: toBool(identity.active) }));
const featureFlags = list<AppSnapshot["featureFlags"][number]>(this.db, "feature_flags", "key asc")
.map((flag) => ({ ...flag, enabled: toBool(flag.enabled) }));
const operationalEvents = list<AppSnapshot["operationalEvents"][number]>(this.db, "operational_events", "created_at desc")
.map((event) => ({ ...event, success: toBool(event.success) }));
return {
activeOrgId,
uiPreferences: this.resolveUiPreferences(),
onboardingDraft: loadOnboardingDraft(this.db),
viewState: loadViewState(this.db),
dataVersion: currentDataVersion(),
autonomousPaused: this.resolveAutonomousPaused(),
integrationKeys: new IntegrationKeyStore(this.db).statuses(),
organizations,
@@ -127,7 +113,6 @@ export class SnapshotService {
chatAttachments: forMessages(this.db, "chat_attachments", "message_id desc", loadedMessageIds),
chatMentions: forMessages(this.db, "chat_mentions", "message_id desc, offset_start asc", loadedMessageIds),
chatSlashInvocations: forMessages(this.db, "chat_slash_invocations", "created_at desc", loadedMessageIds),
boardInterventions: list(this.db, "board_interventions", "created_at asc"),
chatPresence: list(this.db, "chat_presence", "last_seen_at desc"),
chatReadStates: list(this.db, "chat_read_state", "last_read_at desc"),
files,
@@ -136,7 +121,6 @@ export class SnapshotService {
scheduledPosts: list(this.db, "scheduled_posts", "scheduled_for asc"),
providers,
providerAuthProfiles,
providerAuthSessions,
providerCatalog: PROVIDER_CATALOG,
// Explicit projection: raw_response_json alone weighs megabytes across
// 100 rows and no renderer surface reads it.
@@ -150,7 +134,6 @@ export class SnapshotService {
`), activeOrgId ?? "").map((row) => camel(row) as unknown as AppSnapshot["providerRequests"][number]),
providerRateLimits,
usageStats: buildUsageStats(this.db, new Date()),
providerRateLimitObservations,
skillDefinitions: list(this.db, "skill_definitions", "created_at asc"),
socialAccounts: list(this.db, "social_accounts", "created_at asc"),
socialAccountGrants: list(this.db, "social_account_grants", "created_at asc"),
@@ -164,19 +147,7 @@ export class SnapshotService {
policyRules: list(this.db, "policy_rules", "created_at asc"),
migrationReports: list(this.db, "migration_reports", "created_at desc"),
complianceExports: list(this.db, "compliance_exports", "created_at desc"),
legalHolds,
retentionFreezes,
employeeIdentities,
serviceIdentities: list(this.db, "service_identities", "created_at desc"),
roleGrants: list(this.db, "role_grants", "created_at desc"),
accessDecisions: recentInOrg<AppSnapshot["accessDecisions"][number]>(this.db, "access_decisions", "created_at desc", 200, activeOrgId),
orgDepartments: list(this.db, "org_departments", "name asc"),
hiringPolicies: list(this.db, "hiring_policies", "department_name asc"),
approvalRoutingRules: list(this.db, "approval_routing_rules", "created_at asc"),
approvalAssignments: list(this.db, "approval_assignments", "created_at desc"),
featureFlags,
sloTargets: list(this.db, "slo_targets", "signal_key asc"),
operationalEvents,
auditEvents: recentInOrg<AppSnapshot["auditEvents"][number]>(this.db, "audit_events", "created_at desc", 200, activeOrgId),
documents: list(this.db, "documents", "updated_at desc"),
knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"),
+4
View File
@@ -0,0 +1,4 @@
/** A snapshot read at an older data version than the one on screen is a late answer; showing it would undo newer data. */
export function supersedes(shownVersion: number, incomingVersion: number): boolean {
return incomingVersion >= shownVersion;
}
+10 -4
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { supersedes } from "./snapshot-order";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { AppSnapshot } from "../../../shared/domain";
import type { DesktopResult } from "../../../shared/result";
@@ -23,13 +24,17 @@ export function useSnapshot(): {
const [snapshot, setSnapshot] = useState<AppSnapshot | null>(null);
const [syncFailure, setSyncFailure] = useState<SyncFailure | null>(null);
const [syncedAt, setSyncedAt] = useState<string | null>(null);
// The newest data version on screen; a change event at or below it needs no reload.
const shownVersion = useRef(-1);
const load = useCallback(async () => {
try {
const result = await window.agentCompany.loadSnapshot();
if (!result.ok) throw new Error(result.error.message);
setSnapshot(result.data);
setSyncFailure(null);
if (!supersedes(shownVersion.current, result.data.dataVersion)) return;
shownVersion.current = result.data.dataVersion;
setSnapshot(result.data);
setSyncedAt(new Date().toISOString());
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -43,7 +48,8 @@ export function useSnapshot(): {
): Promise<T | null> => {
const result = await operation();
if (!result.ok) throw new Error(result.error.message);
if (result.data) {
if (result.data && supersedes(shownVersion.current, (result.data as AppSnapshot).dataVersion)) {
shownVersion.current = (result.data as AppSnapshot).dataVersion;
setSnapshot(result.data as AppSnapshot);
setSyncFailure(null);
setSyncedAt(new Date().toISOString());
@@ -84,7 +90,7 @@ export function useSnapshot(): {
guardedLoad();
});
const unsubscribe = window.agentCompany.subscribe((event) => {
if (event.type === "snapshot-changed") scheduleReload();
if (event.type === "snapshot-changed" && event.version > shownVersion.current) scheduleReload();
});
return () => {
disposed = true;
-15
View File
@@ -24,18 +24,3 @@ export type ChatThreadBoardState = ValueOf<typeof CHAT_THREAD_BOARD_STATE>;
export type BoardInterventionMode = ValueOf<typeof BOARD_INTERVENTION_MODE>;
export type BoardInterventionAction = ValueOf<typeof BOARD_INTERVENTION_ACTION>;
export interface BoardInterventionRecord {
id: string;
orgId: string;
threadId: string;
messageId: string;
mode: BoardInterventionMode;
action: BoardInterventionAction;
details: string;
targetType: "employee" | "agent" | "board" | null;
targetId: string | null;
approvalId: string | null;
createdByType: "employee" | "agent" | "board" | "system";
createdById: string | null;
createdAt: string;
}
-23
View File
@@ -28,26 +28,3 @@ export interface ComplianceExportRecord {
completedAt: string | null;
}
export interface LegalHoldRecord {
id: string;
orgId: string;
entityType: LegalHoldEntityType;
entityId: string;
reason: string;
createdByEmployeeId: string | null;
active: boolean;
createdAt: string;
releasedAt: string | null;
}
export interface RetentionFreezeRecord {
id: string;
orgId: string;
scopeType: RetentionFreezeScopeType;
scopeId: string;
reason: string;
createdByEmployeeId: string | null;
active: boolean;
createdAt: string;
releasedAt: string | null;
}
+2 -1
View File
@@ -113,7 +113,8 @@ export interface ProviderOAuthEvent {
}
export type RendererEvent =
| { type: "snapshot-changed" }
/** `version` is the data version the change produced; a snapshot at that version or later already shows it. */
| { type: "snapshot-changed"; version: number }
| { type: "provider-response-stream"; event: ProviderStreamRendererEvent }
| ProviderOAuthEvent;
+7 -7
View File
@@ -1,14 +1,14 @@
import type { ProviderDataPolicy } from "./provider-policy-domain";
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type BoardInterventionRecord, type ChatThreadBoardState } from "./board-domain";
export { BOARD_INTERVENTION_ACTION, BOARD_INTERVENTION_MODE, CHAT_THREAD_BOARD_STATE, type BoardInterventionAction, type BoardInterventionMode, type ChatThreadBoardState } from "./board-domain";
export type { ChatReplyStatus, ChatSpaceAnchor, ChatSpaceRecord, ChatMessageRecord, ChatMembershipRecord, ChatThreadRecord, ChatAttachmentRecord, ChatMentionRecord, ChatSlashInvocationRecord, ChatPresenceRecord, ChatReadStateRecord } from "./chat-domain";
export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type LegalHoldRecord, type RetentionFreezeRecord, type RetentionFreezeScopeType } from "./compliance-domain";
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type AccessDecisionRecord, type EmployeeIdentityRecord, type IdentitySource, type PrincipalType, type RoleGrantRecord, type RoleKey, type ServiceIdentityRecord } from "./identity-domain";
export { ROUTE_TO_TYPE, type ApprovalAssignmentRecord, type ApprovalRoutingRuleRecord, type HiringPolicyRecord, type OrgDepartmentRecord, type RouteToType } from "./org-governance-domain";
export { FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type FeatureFlagKey, type FeatureFlagRecord, type OperationalEventRecord, type SloSignalKey, type SloTargetRecord } from "./operations-domain";
export type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./provider-auth-domain";
export { COMPLIANCE_EXPORT_STATE, COMPLIANCE_EXPORT_TYPE, type ComplianceExportRecord, type ComplianceExportState, type ComplianceExportType, type ComplianceSubjectType, type LegalHoldEntityType, type RetentionFreezeScopeType } from "./compliance-domain";
export { ACCESS_DECISION, IDENTITY_SOURCE, ROLE_KEY, type AccessDecision, type IdentitySource, type PrincipalType, type RoleKey } from "./identity-domain";
export { ROUTE_TO_TYPE, type HiringPolicyRecord, type RouteToType } from "./org-governance-domain";
export { FEATURE_FLAG_KEY, SLO_SIGNAL_KEY, type FeatureFlagKey, type SloSignalKey } from "./operations-domain";
export type { ProviderAuthProfileRecord } from "./provider-auth-domain";
export { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE, type ProviderDataPolicy, type ProviderExecutionMode } from "./provider-policy-domain";
export { PROVIDER_REQUEST_STATE, type ProviderRequestRecord, type ProviderRequestState, type ProviderStreamRendererEvent } from "./provider-request-domain";
export type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain";
export type { ProviderRateLimitStateRecord } from "./provider-runtime-domain";
export { SEARCH_ENTITY_TYPES, type SearchEntityType, type SearchPage, type SearchResultRecord } from "./search-domain";
export type { BrowserActionPolicyRecord, BrowserActionRecord, BrowserSessionContainerRecord, SocialAccountGrantRecord, SocialAccountRecord, SocialApprovalRouteRecord, SocialAuditRecord } from "./social-domain";
export type { AppSnapshot, UiLanguage, UiPreferences, UiTheme } from "./snapshot-domain";
-18
View File
@@ -14,21 +14,3 @@ export type RoleKey = ValueOf<typeof ROLE_KEY>;
export type AccessDecision = ValueOf<typeof ACCESS_DECISION>;
export type PrincipalType = "employee" | "agent" | "service_identity";
export interface EmployeeIdentityRecord {
id: string; orgId: string; employeeId: string; source: IdentitySource; externalSubject: string;
email: string | null; active: boolean; createdAt: string; lastSeenAt: string | null;
}
export interface ServiceIdentityRecord {
id: string; orgId: string; agentId: string; name: string; scopeJson: string; createdAt: string;
}
export interface RoleGrantRecord {
id: string; orgId: string; principalType: PrincipalType; principalId: string; roleKey: RoleKey;
scopeType: string; scopeId: string; grantedByEmployeeId: string | null; createdAt: string;
}
export interface AccessDecisionRecord {
id: string; orgId: string; principalType: PrincipalType; principalId: string; action: string;
resourceType: string; resourceId: string; decision: AccessDecision; reason: string; createdAt: string;
}
-11
View File
@@ -16,14 +16,3 @@ export const SLO_SIGNAL_KEY = {
export type FeatureFlagKey = ValueOf<typeof FEATURE_FLAG_KEY>;
export type SloSignalKey = ValueOf<typeof SLO_SIGNAL_KEY>;
export interface FeatureFlagRecord {
id: string; orgId: string; key: FeatureFlagKey; label: string; enabled: boolean; createdAt: string; updatedAt: string;
}
export interface SloTargetRecord {
id: string; orgId: string; signalKey: SloSignalKey; label: string; targetBasisPoints: number; windowMinutes: number; createdAt: string; updatedAt: string;
}
export interface OperationalEventRecord {
id: string; orgId: string; signalKey: SloSignalKey; source: string; success: boolean; durationMs: number | null; detailJson: string; createdAt: string;
}
-14
View File
@@ -3,22 +3,8 @@ type ValueOf<T> = T[keyof T];
export const ROUTE_TO_TYPE = { EMPLOYEE: "employee", AGENT: "agent", BOARD: "board" } as const;
export type RouteToType = ValueOf<typeof ROUTE_TO_TYPE>;
export interface OrgDepartmentRecord {
id: string; orgId: string; name: string; leadAgentId: string | null;
headcountLimit: number; monthlyBudgetLimitCents: number; createdAt: string; updatedAt: string;
}
export interface HiringPolicyRecord {
id: string; orgId: string; departmentName: string; maxHeadcount: number;
monthlyBudgetLimitCents: number; requireApprovalAboveCents: number; createdAt: string; updatedAt: string;
}
export interface ApprovalRoutingRuleRecord {
id: string; orgId: string; kind: string; minimumRisk: string;
routeToType: RouteToType; routeToId: string; createdAt: string;
}
export interface ApprovalAssignmentRecord {
id: string; orgId: string; approvalId: string; assigneeType: RouteToType;
assigneeId: string; routingRuleId: string | null; createdAt: string;
}
-16
View File
@@ -16,19 +16,3 @@ export interface ProviderAuthProfileRecord {
createdAt: string;
}
export interface ProviderAuthSessionRecord {
id: string;
orgId: string;
providerId: string;
providerKey: string;
employeeId: string;
state: string;
nonce: string;
codeVerifierSha256: string;
redirectUri: string;
signInUrl: string;
partition: string;
createdAt: string;
expiresAt: string;
completedAt: string | null;
}
-9
View File
@@ -26,15 +26,6 @@ export interface ProviderObservedRateLimitState {
rawStateJson: string;
}
export interface ProviderRateLimitObservationRecord extends ProviderObservedRateLimitState {
id: string;
orgId: string;
providerId: string;
providerKey: string;
model: string | null;
observedAt: string;
}
/** The message of the typed error a run gets when its provider's monthly request allowance is used up. */
export const PROVIDER_MONTHLY_LIMIT_MESSAGE = "Provider monthly request limit has been reached.";
+6 -22
View File
@@ -1,7 +1,6 @@
import type { OnboardingDraft } from "./commands";
import type { UsageStatsRecord } from "./usage-stats";
import type { IntegrationKeyStatus } from "./commands/integration-keys";
import type { BoardInterventionRecord as BoardInterventionRecordModel } from "./board-domain";
import type {
ChatAttachmentRecord,
ChatMembershipRecord,
@@ -15,7 +14,7 @@ import type {
ChatReplyStatus,
ChatSpaceAnchor,
} from "./chat-domain";
import type { ComplianceExportRecord, LegalHoldRecord, RetentionFreezeRecord } from "./compliance-domain";
import type { ComplianceExportRecord } from "./compliance-domain";
import type {
AgentRecord,
ApprovalRecord,
@@ -44,12 +43,10 @@ import type {
TaskRunRecord,
WorkspaceRecord,
} from "./domain";
import type { AccessDecisionRecord, EmployeeIdentityRecord, RoleGrantRecord, ServiceIdentityRecord } from "./identity-domain";
import type { ApprovalAssignmentRecord, ApprovalRoutingRuleRecord, HiringPolicyRecord, OrgDepartmentRecord } from "./org-governance-domain";
import type { FeatureFlagRecord, OperationalEventRecord, SloTargetRecord } from "./operations-domain";
import type { ProviderAuthProfileRecord, ProviderAuthSessionRecord } from "./provider-auth-domain";
import type { HiringPolicyRecord } from "./org-governance-domain";
import type { ProviderAuthProfileRecord } from "./provider-auth-domain";
import type { ProviderRequestRecord } from "./provider-request-domain";
import type { ProviderRateLimitObservationRecord, ProviderRateLimitStateRecord } from "./provider-runtime-domain";
import type { ProviderRateLimitStateRecord } from "./provider-runtime-domain";
import type {
BrowserActionPolicyRecord,
BrowserActionRecord,
@@ -75,6 +72,8 @@ export interface AppSnapshot {
onboardingDraft: OnboardingDraft | null;
/** Where the operator was: page, sections, filters and selected records, by key. */
viewState: Record<string, unknown>;
/** The data version this snapshot was read at. */
dataVersion: number;
/** True while the operator has paused the autonomous run engine. */
autonomousPaused: boolean;
integrationKeys: IntegrationKeyStatus[];
@@ -96,7 +95,6 @@ export interface AppSnapshot {
chatAttachments: ChatAttachmentRecord[];
chatMentions: ChatMentionRecord[];
chatSlashInvocations: ChatSlashInvocationRecord[];
boardInterventions: BoardInterventionRecordModel[];
chatPresence: ChatPresenceRecord[];
chatReadStates: ChatReadStateRecord[];
files: FileObjectRecord[];
@@ -105,13 +103,11 @@ export interface AppSnapshot {
scheduledPosts: ScheduledPostRecord[];
providers: ProviderRecord[];
providerAuthProfiles: ProviderAuthProfileRecord[];
providerAuthSessions: ProviderAuthSessionRecord[];
providerCatalog: ProviderCatalogEntry[];
providerRequests: ProviderRequestRecord[];
providerRateLimits: ProviderRateLimitStateRecord[];
/** Period statistics per org over all records, for charts and billing. */
usageStats: UsageStatsRecord[];
providerRateLimitObservations: ProviderRateLimitObservationRecord[];
// The snapshot is the renderer's working set and rides IPC on every
// refresh. Skill internals (revisions, bindings, per-run runtime
// snapshots with compiled prompts) are main-process concerns and stay out
@@ -127,19 +123,7 @@ export interface AppSnapshot {
policyRules: PolicyRuleRecord[];
migrationReports: MigrationReportRecord[];
complianceExports: ComplianceExportRecord[];
legalHolds: LegalHoldRecord[];
retentionFreezes: RetentionFreezeRecord[];
employeeIdentities: EmployeeIdentityRecord[];
serviceIdentities: ServiceIdentityRecord[];
roleGrants: RoleGrantRecord[];
accessDecisions: AccessDecisionRecord[];
orgDepartments: OrgDepartmentRecord[];
hiringPolicies: HiringPolicyRecord[];
approvalRoutingRules: ApprovalRoutingRuleRecord[];
approvalAssignments: ApprovalAssignmentRecord[];
featureFlags: FeatureFlagRecord[];
sloTargets: SloTargetRecord[];
operationalEvents: OperationalEventRecord[];
auditEvents: AuditEventRecord[];
documents: DocumentRecord[];
knowledgeEntries: KnowledgeEntryRecord[];
+4
View File
@@ -25,6 +25,10 @@ test("a record that breaks one page leaves the rest of the app working", async (
db.prepare("insert into approvals(id, org_id, kind, title, details, risk, requested_by_type, state, created_at) values ('app_bad', ?, 'task_risk', 'Broken request', 'd', 'unknown', 'system', 'pending', ?)")
.run(orgId, new Date().toISOString());
db.close();
// Written behind the app's back, so the test announces it as the app's own changes are.
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER });
});
await page.getByRole("button", { name: "Approvals" }).click();
const failure = page.getByRole("alert").filter({ hasText: "This page could not be shown." });
+1 -1
View File
@@ -54,7 +54,7 @@ test("data that can no longer be updated says so, from when it is, and recovers"
db.close();
};
const announceChange = () => app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed" });
BrowserWindow.getAllWindows()[0]!.webContents.send("agentcompany:renderer-event", { type: "snapshot-changed", version: Number.MAX_SAFE_INTEGER });
});
try {
const page = await app.firstWindow();
+11
View File
@@ -0,0 +1,11 @@
import type { DatabaseSync } from "node:sqlite";
import { camel } from "../../src/main/services/snapshot-service";
/** A table's rows with camel-cased keys, for asserting backend records the app no longer ships to the window. */
export function tableRows(db: DatabaseSync, table: string, orderBy: string, booleans: string[] = []): Array<Record<string, unknown>> {
return (db.prepare(`select * from ${table} order by ${orderBy}`).all() as Array<Record<string, unknown>>).map((row) => {
const next = camel(row as Record<string, string | number | null>);
for (const key of booleans) next[key] = next[key] === 1;
return next;
});
}
+3 -2
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { mkdirSync, writeFileSync } from "node:fs";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
@@ -104,9 +105,9 @@ describe("collaboration integration flow", () => {
.get() as { compiled_prompt: string } | undefined;
if (!snapshotRecord) throw new Error("Skill runtime snapshot was not created.");
expect(state.approvals.find((approval) => approval.id === approvalId)?.state).toBe(APPROVAL_STATE.APPROVED);
expect(state.approvalAssignments.some((assignment) => assignment.approvalId === approvalId)).toBe(true);
expect(tableRows(connection.db, "approval_assignments", "created_at desc").some((assignment) => assignment.approvalId === approvalId)).toBe(true);
expect(snapshotRecord.compiled_prompt).toContain("Release Gate");
expect(state.operationalEvents.some((event) => event.signalKey === "skill_resolve_success")).toBe(true);
expect(tableRows(connection.db, "operational_events", "created_at desc", ["success"]).some((event) => event.signalKey === "skill_resolve_success")).toBe(true);
connection.close();
});
});
+3 -2
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { writeFileSync } from "node:fs";
import { createHash, randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
@@ -567,10 +568,10 @@ describe("chat governance", () => {
const state = snapshot.load();
const thread = state.chatThreads.find((item) => item.id === threadId);
expect(thread).toMatchObject({ boardState: "takeover", controlledByType: "agent", controlledById: agentId });
expect(state.boardInterventions.map((item) => item.action)).toEqual(["freeze", "rollback", "takeover", "force_approval"]);
expect(tableRows(connection.db, "board_interventions", "created_at asc").map((item) => item.action)).toEqual(["freeze", "rollback", "takeover", "force_approval"]);
const forceApproval = state.approvals.find((item) => item.kind === "board_force_approval");
if (!forceApproval) throw new Error("Board force approval was not created.");
const forceIntervention = state.boardInterventions.find((item) => item.action === "force_approval");
const forceIntervention = tableRows(connection.db, "board_interventions", "created_at asc").find((item) => item.action === "force_approval");
if (!forceIntervention) throw new Error("Board force approval intervention was not created.");
expect(forceIntervention.approvalId).toBe(forceApproval.id);
expect(state.chatSlashInvocations.some((item) => item.resultRef === forceIntervention.id)).toBe(true);
+2 -1
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -167,7 +168,7 @@ describe("compliance service", () => {
expect(redactedApproval?.decidedByEmployeeId).toBeNull();
expect(redactedSocialAccount?.authorizedEmployeeId).toBeNull();
expect(state.socialAccountGrants).toHaveLength(0);
expect(state.providerAuthSessions).toHaveLength(0);
expect(tableRows(connection.db, "provider_auth_sessions", "created_at desc")).toHaveLength(0);
expect(state.complianceExports[0]?.exportType).toBe("gdpr_delete");
expect(state.complianceExports[0]?.requestedByEmployeeId).toBeNull();
expect(state.auditEvents.some((event) => event.entityType === "compliance_export" && event.action === "gdpr_deleted")).toBe(true);
+35
View File
@@ -0,0 +1,35 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { bumpDataVersion, currentDataVersion } from "../../src/main/services/data-version";
import { supersedes } from "../../src/renderer/src/app/snapshot-order";
describe("data version", () => {
test("a snapshot read after a change carries that change's version, so its own change event needs no reload", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-data-version-${randomUUID()}.db`));
const snapshot = new SnapshotService(connection.db);
const before = snapshot.load().dataVersion;
const announced = bumpDataVersion();
const after = snapshot.load();
expect(after.dataVersion).toBe(announced);
expect(announced).toBeGreaterThan(before);
// A later change is newer than what is shown, so it is reloaded.
expect(bumpDataVersion()).toBeGreaterThan(after.dataVersion);
expect(currentDataVersion()).toBe(after.dataVersion + 1);
// Records the window never reads are not shipped with every reload.
expect(Object.keys(after)).not.toEqual(expect.arrayContaining(["operationalEvents", "featureFlags", "approvalAssignments", "roleGrants"]));
connection.close();
});
});
describe("snapshot order", () => {
test("a late answer read at an older version never replaces newer data on screen", () => {
expect(supersedes(2, 1)).toBe(false);
expect(supersedes(2, 2)).toBe(true);
expect(supersedes(2, 3)).toBe(true);
expect(supersedes(-1, 0)).toBe(true);
});
});
+9 -8
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -55,13 +56,13 @@ describe("domain services", () => {
expect(state.organizations).toHaveLength(1);
expect(state.employees[0]?.displayName).toBe("Dana Lee");
expect(state.providers.length).toBeGreaterThanOrEqual(6);
expect(state.employeeIdentities).toHaveLength(1);
expect(state.roleGrants.map((grant) => grant.roleKey).sort()).toEqual(["approval_decider", "hiring_manager", "org_admin"]);
expect(state.orgDepartments.map((department) => department.name)).toContain("Engineering");
expect(tableRows(connection.db, "employee_identities", "created_at desc", ["active"])).toHaveLength(1);
expect(tableRows(connection.db, "role_grants", "created_at desc").map((grant) => grant.roleKey).sort()).toEqual(["approval_decider", "hiring_manager", "org_admin"]);
expect(tableRows(connection.db, "org_departments", "name asc").map((department) => department.name)).toContain("Engineering");
expect(state.hiringPolicies.some((policy) => policy.departmentName === "Engineering")).toBe(true);
expect(state.approvalRoutingRules[0]).toMatchObject({ kind: "*", minimumRisk: "low", routeToType: "employee" });
expect(state.featureFlags.map((flag) => flag.key)).toContain("board_intervention");
expect(state.sloTargets.map((target) => target.signalKey)).toContain("provider_request_success");
expect(tableRows(connection.db, "approval_routing_rules", "created_at asc")[0]).toMatchObject({ kind: "*", minimumRisk: "low", routeToType: "employee" });
expect(tableRows(connection.db, "feature_flags", "key asc", ["enabled"]).map((flag) => flag.key)).toContain("board_intervention");
expect(tableRows(connection.db, "slo_targets", "signal_key asc").map((target) => target.signalKey)).toContain("provider_request_success");
expect(state.chatSpaces.map((space) => space.name)).toContain("board");
expect(state.policyRules[0]?.action).toBe("task_create");
expect(state.auditEvents[0]?.action).toBe("created");
@@ -100,7 +101,7 @@ describe("domain services", () => {
const state = snapshot.load();
expect(state.tasks).toHaveLength(1);
expect(state.approvals).toHaveLength(1);
expect(state.approvalAssignments).toHaveLength(1);
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
expect(state.approvals[0]?.state).toBe("pending");
expect(state.approvals[0]).toMatchObject({ sourceEntityType: "task", sourceEntityId: state.tasks[0]?.id });
connection.close();
@@ -140,7 +141,7 @@ describe("domain services", () => {
expect(state.approvals).toHaveLength(1);
expect(state.approvals[0]?.state).toBe("approved");
expect(state.approvals[0]?.decidedByEmployeeId).toBe(employeeId);
expect(state.approvalAssignments).toHaveLength(0);
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(0);
connection.close();
});
+3 -2
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -58,8 +59,8 @@ describe("identity access service", () => {
});
const state = snapshot.load();
expect(state.employeeIdentities.some((identity) => identity.source === "sso" && identity.email === "dana@example.com")).toBe(true);
expect(state.roleGrants.some((grant) => grant.principalId === employeeId && grant.roleKey === ROLE_KEY.APPROVAL_DECIDER)).toBe(true);
expect(tableRows(connection.db, "employee_identities", "created_at desc", ["active"]).some((identity) => identity.source === "sso" && identity.email === "dana@example.com")).toBe(true);
expect(tableRows(connection.db, "role_grants", "created_at desc").some((grant) => grant.principalId === employeeId && grant.roleKey === ROLE_KEY.APPROVAL_DECIDER)).toBe(true);
expect(state.auditEvents.some((event) => event.entityType === "employee_identity" && event.action === "synced")).toBe(true);
connection.close();
});
+3 -5
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -7,7 +8,6 @@ import { OperationsService } from "../../src/main/services/operations-service";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ProviderService } from "../../src/main/services/provider-service";
import { providerAdapters, type ProviderAdapterRegistry } from "../../src/main/services/provider-adapters";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { PROVIDER_DATA_POLICY, PROVIDER_EXECUTION_MODE } from "../../src/shared/domain";
function createConnection(): DatabaseConnection {
@@ -37,20 +37,18 @@ describe("operations service", () => {
test("seeds feature flags and SLO targets, and no drill that scores configuration as fault handling", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
connection.transaction(() => {
createOrganization(organization);
});
const state = snapshot.load();
expect(state.featureFlags.map((flag) => flag.key).sort()).toEqual([
expect(tableRows(connection.db, "feature_flags", "key asc", ["enabled"]).map((flag) => flag.key).sort()).toEqual([
"board_intervention",
"provider_background_execution",
"social_publish_approval",
]);
// Every SLO target is fed by a runtime source; none waits on a signal only a drill would write.
expect(state.sloTargets.map((target) => target.signalKey).sort()).toEqual([
expect(tableRows(connection.db, "slo_targets", "signal_key asc").map((target) => target.signalKey).sort()).toEqual([
"approval_route_coverage", "migration_validation_success", "provider_request_success", "skill_resolve_success",
]);
expect(connection.db.prepare("select name from sqlite_master where type = 'table' and name like 'chaos_drill%'").all()).toEqual([]);
+5 -4
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -72,8 +73,8 @@ describe("org governance service", () => {
});
const state = snapshot.load();
expect(state.approvalAssignments).toHaveLength(1);
expect(state.approvalAssignments[0]).toMatchObject({ approvalId, assigneeType: "employee" });
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
expect(tableRows(connection.db, "approval_assignments", "created_at desc")[0]).toMatchObject({ approvalId, assigneeType: "employee" });
expect(state.auditEvents.some((event) => event.entityType === "approval" && event.action === "routed")).toBe(true);
connection.close();
});
@@ -107,8 +108,8 @@ describe("org governance service", () => {
const state = snapshot.load();
const approval = state.approvals.find((item) => item.kind === "agent_hire");
expect(approval).toMatchObject({ orgId, title: "Review hire: Ada", risk: RISK_LEVEL.MEDIUM });
expect(state.approvalAssignments).toHaveLength(1);
expect(state.approvalAssignments[0]).toMatchObject({ approvalId: approval?.id, assigneeType: "employee" });
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toHaveLength(1);
expect(tableRows(connection.db, "approval_assignments", "created_at desc")[0]).toMatchObject({ approvalId: approval?.id, assigneeType: "employee" });
connection.close();
});
});
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -5,7 +6,6 @@ import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { parseProviderRateLimitHeaders, ProviderRateLimitRegistry } from "../../src/main/services/provider-rate-limit-registry";
import { SnapshotService } from "../../src/main/services/snapshot-service";
function createConnection(): DatabaseConnection {
return new DatabaseConnection(join(tmpdir(), `agentcompany-provider-rate-limits-${randomUUID()}.db`));
@@ -52,7 +52,6 @@ describe("provider rate limit registry", () => {
test("persists observed quota state into snapshots", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const registry = new ProviderRateLimitRegistry(connection.db);
let orgId = "";
let openAiProviderId = "";
@@ -75,7 +74,7 @@ describe("provider rate limit registry", () => {
});
});
expect(snapshot.load().providerRateLimitObservations[0]).toMatchObject({
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
orgId,
providerId: openAiProviderId,
providerKey: "openai",
+4 -4
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto";
@@ -238,7 +239,7 @@ describe("provider service", () => {
responseText: "api_key:gpt-5.4:Draft board memo",
state: "succeeded",
});
expect(state.operationalEvents[0]).toMatchObject({ signalKey: "provider_request_success", source: "provider_gateway", success: true });
expect(tableRows(connection.db, "operational_events", "created_at desc", ["success"])[0]).toMatchObject({ signalKey: "provider_request_success", source: "provider_gateway", success: true });
expect(state.providers.find((provider) => provider.id === openAiProviderId)?.healthState).toBe("healthy");
connection.close();
});
@@ -246,7 +247,6 @@ describe("provider service", () => {
test("records provider response rate limit observations", async () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const snapshot = new SnapshotService(connection.db);
const adapters: ProviderAdapterRegistry = {
...providerAdapters,
openai: {
@@ -281,7 +281,7 @@ describe("provider service", () => {
instructions: null,
});
expect(snapshot.load().providerRateLimitObservations[0]).toMatchObject({
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
orgId,
providerId: openAiProviderId,
providerKey: "openai",
@@ -337,7 +337,7 @@ describe("provider service", () => {
state: "failed",
errorMessage: "rate limited",
});
expect(state.providerRateLimitObservations[0]).toMatchObject({
expect(tableRows(connection.db, "provider_rate_limit_observations", "observed_at desc")[0]).toMatchObject({
orgId,
providerId: openAiProviderId,
providerKey: "openai",
+2 -3
View File
@@ -1,3 +1,4 @@
import { tableRows } from "../helpers/table-rows";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto";
@@ -274,7 +275,6 @@ describe("social service", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);
const social = new SocialService(connection.db);
const snapshot = new SnapshotService(connection.db);
let orgId = "";
let authorizerId = "";
let approvalId = "";
@@ -303,8 +303,7 @@ describe("social service", () => {
.get(postId) as { approval_id: string }).approval_id;
});
const state = snapshot.load();
expect(state.approvalAssignments).toContainEqual(expect.objectContaining({
expect(tableRows(connection.db, "approval_assignments", "created_at desc")).toContainEqual(expect.objectContaining({
approvalId,
assigneeType: ROUTE_TO_TYPE.EMPLOYEE,
assigneeId: authorizerId,