Files
AgentCompany/src/main/bootstrap-application.ts
T
ZacharyZhang-NY 3353e46067 Backfill existing workspace files into work_products on first boot after observationPath fix
The 1f2f977 hotfix repairs the projector for new runs, but files that
already exist on disk before the fix lands stay invisible to the
DocumentCenter — a baseline+diff projector reports "changed" only
when a file's mtime advances past the run-start baseline, and a
quiescent file written before the fix never advances.

This commit adds a bootstrap-time one-shot rescan:

- New module `bootstrap/backfill-workspace-artifacts.ts` walks the
  primary/oldest workspace of every company exactly once (gated by
  `artifact_rescan_v1_done` in the settings table), applies the same
  registrability filter as `artifact-projector.ts` (skip .git/,
  node_modules/, .DS_Store, .lock/.log/.map; ignore package-lock.json
  / pnpm-lock.yaml / yarn.lock; cap at 5 000 files per workspace),
  and upserts a `work_products` row via `findWorkProductByExternalId`
  → `createWorkProduct` for any file the projector never registered.
- bootstrap-application.ts wires the call after recoverScheduledPosts
  (DB ready, services running, before the first heartbeat tick), so
  the rescan completes in the same boot tick that the user opens
  the DocumentCenter.

Idempotency: subsequent boots short-circuit on the settings flag.
The post-fix projector handles all future file activity inside
each run, so the backfill never needs to run twice. Per-workspace
file cap prevents a pathological tree from locking the bootstrap
path; overflow files are still picked up by the next run that
touches them. No fallback, no mock — files that fail stat are
silently skipped (same policy as the projector).

typecheck green, 543/543 unit tests, 10/10 eval. The user's 19
existing markdown deliverables under ~/Documents/Github/test/
should appear in the DocumentCenter on the next `pnpm dev`.
2026-04-20 16:25:48 -04:00

194 lines
6.9 KiB
TypeScript

import { app } from "electron";
import type { AgentApiServer } from "./api-server";
import type { AppDatabase } from "./database";
import type { BrowserManager } from "./browser-manager";
import { prepareHostEnvironment } from "./host-environment-service";
import type { createServiceGraph } from "./service-graph";
import type { createIpcSupport } from "./ipc-support";
import type { createThreadIntegration } from "./thread-integration";
import type { createCheckpointEngine } from "./checkpoint-engine";
import {
DocumentLifecycleProcessor,
makeLifecycleProcessorOptions,
} from "./document-lifecycle-processor";
import { registerLifecycleHandlers } from "./document-lifecycle-handlers";
import { prepareLogger } from "./bootstrap/prepare-logger";
import { prepareDatabase } from "./bootstrap/prepare-database";
import { prepareServices } from "./bootstrap/prepare-services";
import { wireApiServer, type CreateApiServerFn } from "./bootstrap/wire-api-server";
import { recoverScheduledPosts } from "./bootstrap/recover-scheduled-posts";
import { backfillWorkspaceArtifacts } from "./bootstrap/backfill-workspace-artifacts";
import { runStartupHealthChecks } from "./bootstrap/run-startup-health-checks";
import { emitCrashRecoveryNotification, recoverShutdownInterruptedTasks } from "./bootstrap/recover-interrupted-tasks";
interface BootstrapApplicationDependencies {
hasSingleInstanceLock: boolean;
isDev: boolean;
devServerOrigin: string | null;
profileDir: string;
appDatabaseFactory: (profileDir: string) => AppDatabase;
initSigningKey: (profileDir: string) => void;
windowStatePath: string;
preloadPath: string;
rendererDist: string;
iconPath: string;
devServerUrl?: string;
workerPath: string;
createApiServer: CreateApiServerFn;
createRunToken: (agentId: string, companyId: string, runId: string) => string;
seedDefaultAutomationRules: (companyId: string) => void;
isQuittingRef: { current: boolean };
onRuntimeReady?: (state: {
db: AppDatabase;
apiServer: AgentApiServer;
serviceGraph: ReturnType<typeof createServiceGraph>;
ipcSupport: ReturnType<typeof createIpcSupport>;
threadIntegration: ReturnType<typeof createThreadIntegration>;
checkpointEngine: ReturnType<typeof createCheckpointEngine>;
lifecycleProcessor: DocumentLifecycleProcessor;
}) => void;
}
export async function bootstrapApplication(deps: BootstrapApplicationDependencies) {
await app.whenReady();
const log = prepareLogger();
await prepareHostEnvironment({ logger: log });
const db = prepareDatabase({
profileDir: deps.profileDir,
appDatabaseFactory: deps.appDatabaseFactory,
initSigningKey: deps.initSigningKey,
});
const apiServerRef = { current: null as AgentApiServer | null };
const browserManagerRef = { current: null as BrowserManager | null };
const services = prepareServices({
db,
apiServerRef,
browserManagerRef,
isQuittingRef: deps.isQuittingRef,
preloadPath: deps.preloadPath,
rendererDist: deps.rendererDist,
iconPath: deps.iconPath,
isDev: deps.isDev,
devServerUrl: deps.devServerUrl,
devServerOrigin: deps.devServerOrigin,
windowStatePath: deps.windowStatePath,
workerPath: deps.workerPath,
logger: log,
createRunToken: deps.createRunToken,
seedDefaultAutomationRules: deps.seedDefaultAutomationRules,
});
const { serviceGraph, ipcSupport, threadIntegration, notificationService, checkpointEngine, notify, emitEvent, publishDomainChanged } = services;
const wakeAgentIfPossible = (agentId: string | null | undefined, companyId: string, trigger: string) => {
const orchestration = serviceGraph.getOrchestrationService();
if (!orchestration) throw new Error("Orchestration service is not initialized.");
return orchestration.wakeAgentIfPossible(agentId, companyId, trigger);
};
const lifecycleProcessor = new DocumentLifecycleProcessor(
makeLifecycleProcessorOptions({ db, logger: log }),
);
registerLifecycleHandlers(lifecycleProcessor, {
dispatchAutomation: (trigger, ctx) => {
const automation = serviceGraph.getAutomationService();
if (!automation) throw new Error("Automation service is not initialized.");
automation.dispatchAutomation(trigger, ctx);
},
wakeAgentIfPossible,
});
const apiServer = wireApiServer({
db,
serviceGraph,
threadIntegration,
checkpointEngine,
lifecycleProcessor,
emitEvent,
notify,
publishDomainChanged,
isDev: deps.isDev,
devServerUrl: deps.devServerUrl,
createApiServer: deps.createApiServer,
});
apiServerRef.current = apiServer;
await apiServer.start();
deps.onRuntimeReady?.({
db,
apiServer,
serviceGraph,
ipcSupport,
threadIntegration,
checkpointEngine,
lifecycleProcessor,
});
lifecycleProcessor.start();
app.on("before-quit", () => lifecycleProcessor.stop());
if (apiServer && serviceGraph.getBrowserActionService()) {
apiServer.setBrowserActionHandler(async (actionId, companyId) => {
const browserActions = serviceGraph.getBrowserActionService();
if (!browserActions) throw new Error("Browser action service is not initialized");
await browserActions.dispatchBrowserAction(actionId, companyId);
});
}
const scheduledPostTimers = recoverScheduledPosts({
db,
logger: log,
isQuittingRef: deps.isQuittingRef,
getBrowserActionService: () => serviceGraph.getBrowserActionService(),
});
// One-time backfill: surface deliverables that cognitive runs wrote
// to disk while the projector was suppressed by the pre-observationPath
// bug (regression of T1 workspace fix, repaired in 2c12c1a). Gated by
// a settings flag so subsequent boots skip the workspace walk; the
// post-fix projector handles new file activity inside each run.
backfillWorkspaceArtifacts({ db, logger: log });
// Register cleanup for bootstrap-owned resources during quit.
// Runs BEFORE service-graph's onBeforeQuit (which is registered later).
app.on("before-quit", () => {
for (const timer of scheduledPostTimers) clearTimeout(timer);
scheduledPostTimers.clear();
notificationService.destroy();
});
// Show the window immediately so the user sees the app launch without delay.
await serviceGraph.getDesktopShellService()?.ensureWindow();
serviceGraph.getAppLifecycleService()?.register(deps.hasSingleInstanceLock);
// Run connector health checks AFTER the window is visible to unblock startup.
await runStartupHealthChecks(db, serviceGraph.getConnectorHealthService(), log);
serviceGraph.getOrchestrationService()?.startAllSchedulers();
emitCrashRecoveryNotification({
db,
logger: log,
emitEvent,
notify,
publishDomainChanged,
wakeAgentIfPossible,
});
serviceGraph.getOrchestrationService()?.resumeQueuedRuns();
recoverShutdownInterruptedTasks({
db,
logger: log,
emitEvent,
notify,
publishDomainChanged,
wakeAgentIfPossible,
});
return { db, apiServer, serviceGraph, ipcSupport, threadIntegration };
}