From fa006524c274b747ab0081b4bd8f95eeecb70a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 22 Apr 2026 16:58:36 -0400 Subject: [PATCH] refactor(runtime): centralize lifecycle constants --- src/main/constants.ts | 18 ++++++++++++++++ src/main/database-base.ts | 3 +-- src/main/database.ts | 2 +- src/main/database/migrations/v50-v50.ts | 3 +-- src/main/document-lifecycle-processor.ts | 11 ++-------- tests/unit/runtime-constants.test.ts | 26 ++++++++++++++++++++++++ 6 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 src/main/constants.ts create mode 100644 tests/unit/runtime-constants.test.ts diff --git a/src/main/constants.ts b/src/main/constants.ts new file mode 100644 index 00000000..ea65bcf5 --- /dev/null +++ b/src/main/constants.ts @@ -0,0 +1,18 @@ +/** + * Task and workspace locks are considered stale after 15 minutes. The window + * covers heartbeat cadence, worst-case model/tool round trips, and shutdown + * recovery margin while still releasing abandoned work within one operator + * session. + */ +export const STALE_LOCK_TIMEOUT_MS = 15 * 60 * 1000; + +export const DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS = { + /** Five attempts cover transient filesystem/indexing failures without hiding a broken hook. */ + maxAttempts: 5, + /** Document hooks are local metadata side effects; 60s separates a dead handler from a slow one. */ + staleRunningMs: 60_000, + /** Keeps a burst of document hooks efficient while bounding one sweep's synchronous work. */ + batchSize: 32, + /** Recovers crash leftovers quickly without turning idle profiles into polling loops. */ + sweepIntervalMs: 15_000, +} as const; diff --git a/src/main/database-base.ts b/src/main/database-base.ts index 1d24d60e..a747b41e 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -32,12 +32,11 @@ import { MODEL_CAPABILITY_SEED } from "./model-capability-seed"; import { initializeDatabase } from "./database/schema"; import { SecretVault } from "./vault"; import type { AgentStatus, ConnectorId, ProfileSnapshot, RunStatus } from "@shared/types"; +import { STALE_LOCK_TIMEOUT_MS } from "./constants"; function nowIso() { return new Date().toISOString(); } -export const STALE_LOCK_TIMEOUT_MS = 15 * 60 * 1000; - export class AppDatabaseBase { readonly profileDir: string; readonly dbPath: string; diff --git a/src/main/database.ts b/src/main/database.ts index 8a589277..1c448717 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1,7 +1,7 @@ import type { SQLInputValue } from "node:sqlite"; import { AppDatabaseBase } from "./database-base"; -export { STALE_LOCK_TIMEOUT_MS } from "./database-base"; +export { STALE_LOCK_TIMEOUT_MS } from "./constants"; /** * AppDatabase — concrete persistence surface wired to the twenty-six diff --git a/src/main/database/migrations/v50-v50.ts b/src/main/database/migrations/v50-v50.ts index bc75a491..910f246b 100644 --- a/src/main/database/migrations/v50-v50.ts +++ b/src/main/database/migrations/v50-v50.ts @@ -1,4 +1,5 @@ import log from "electron-log/main.js"; +import { STALE_LOCK_TIMEOUT_MS } from "../../constants"; import type { Migration, MigrationDeps } from "./types"; import { columnExists, nowIso, tableExists } from "./types"; @@ -42,8 +43,6 @@ import { columnExists, nowIso, tableExists } from "./types"; * already orphaned pre-migration. */ -const STALE_LOCK_TIMEOUT_MS = 15 * 60 * 1000; - export function createMigrationsV50V50(deps: MigrationDeps): Migration[] { const { db } = deps; return [ diff --git a/src/main/document-lifecycle-processor.ts b/src/main/document-lifecycle-processor.ts index 7da5fb1a..76dce85d 100644 --- a/src/main/document-lifecycle-processor.ts +++ b/src/main/document-lifecycle-processor.ts @@ -24,6 +24,7 @@ */ import type { AppDatabase } from "./database"; +import { DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS } from "./constants"; import type { DocumentLifecycleEventRow } from "./database/document-lifecycle-events"; export interface LifecycleHookContext { @@ -59,19 +60,11 @@ export interface LifecycleProcessorOptions { sweepIntervalMs: number; } -const DEFAULT_MAX_ATTEMPTS = 5; -const DEFAULT_STALE_RUNNING_MS = 60_000; -const DEFAULT_BATCH_SIZE = 32; -const DEFAULT_SWEEP_INTERVAL_MS = 15_000; - export function makeLifecycleProcessorOptions( overrides: Partial & Pick, ): LifecycleProcessorOptions { return { - maxAttempts: DEFAULT_MAX_ATTEMPTS, - staleRunningMs: DEFAULT_STALE_RUNNING_MS, - batchSize: DEFAULT_BATCH_SIZE, - sweepIntervalMs: DEFAULT_SWEEP_INTERVAL_MS, + ...DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS, ...overrides, }; } diff --git a/tests/unit/runtime-constants.test.ts b/tests/unit/runtime-constants.test.ts new file mode 100644 index 00000000..ded85a95 --- /dev/null +++ b/tests/unit/runtime-constants.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import type { AppDatabase } from "../../src/main/database"; +import { + DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS, + STALE_LOCK_TIMEOUT_MS, +} from "../../src/main/constants"; +import { makeLifecycleProcessorOptions } from "../../src/main/document-lifecycle-processor"; + +describe("runtime constants", () => { + it("uses one stale lock timeout for task lease recovery", () => { + expect(STALE_LOCK_TIMEOUT_MS).toBe(15 * 60 * 1000); + }); + + it("uses shared document lifecycle processor defaults", () => { + const logger = { info: () => undefined, warn: () => undefined, error: () => undefined }; + const options = makeLifecycleProcessorOptions({ + db: {} as AppDatabase, + logger, + }); + + expect(options.maxAttempts).toBe(DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS.maxAttempts); + expect(options.staleRunningMs).toBe(DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS.staleRunningMs); + expect(options.batchSize).toBe(DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS.batchSize); + expect(options.sweepIntervalMs).toBe(DOCUMENT_LIFECYCLE_PROCESSOR_DEFAULTS.sweepIntervalMs); + }); +});