feat: improve streaming and startup responsiveness
This commit is contained in:
+64
-48
@@ -82,6 +82,7 @@ import {
|
||||
type DeepSeekHarnessFork
|
||||
} from './agent/deepseek-harness-utility-launcher'
|
||||
import { buildControlledHarnessEnvironment } from './agent/process-environment'
|
||||
import { runStartupPrerequisites } from './startup-prerequisites'
|
||||
|
||||
const shortcut = 'CommandOrControl+Shift+Space'
|
||||
const mainModuleDirectory = dirname(fileURLToPath(import.meta.url))
|
||||
@@ -380,9 +381,11 @@ if (hasSingleInstanceLock) {
|
||||
join(app.getPath('userData'), 'runtime-settings.json'),
|
||||
secureCipher
|
||||
)
|
||||
const initialRuntimeSettings =
|
||||
await settingsStore.getPublicSettings()
|
||||
const initialSettings = await settingsStore.getResolvedSettings()
|
||||
const [initialRuntimeSettings, initialResolvedSettings] =
|
||||
await Promise.all([
|
||||
settingsStore.getPublicSettings(),
|
||||
settingsStore.getResolvedSettings()
|
||||
])
|
||||
globalTlsPolicy = new GlobalTlsPolicy(app)
|
||||
globalTlsPolicy.install()
|
||||
const capabilityService = new CapabilityService(
|
||||
@@ -444,10 +447,6 @@ if (hasSingleInstanceLock) {
|
||||
app.getPath('userData'),
|
||||
'deepseek-harness'
|
||||
)
|
||||
await mkdir(deepSeekHarnessHome, {
|
||||
recursive: true,
|
||||
mode: 0o700
|
||||
})
|
||||
const launchDeepSeekHarness =
|
||||
createDeepSeekHarnessUtilityLauncher({
|
||||
bundledHostPath: bundledRuntimePaths.deepseekHarness,
|
||||
@@ -458,56 +457,29 @@ if (hasSingleInstanceLock) {
|
||||
fork: forkDeepSeekHarness,
|
||||
terminateProcess: terminateHarnessUtilityProcess
|
||||
})
|
||||
knowledgeService = new KnowledgeService({
|
||||
const startupKnowledgeService = new KnowledgeService({
|
||||
databasePath: join(app.getPath('userData'), 'knowledge.sqlite'),
|
||||
managedRoot: join(app.getPath('userData'), 'knowledge'),
|
||||
extractStructured: createModelGraphExtractor(settingsStore),
|
||||
parseDocument: documentParsingService.parse
|
||||
})
|
||||
await knowledgeService.initialize()
|
||||
const knowledgeRuntimeSettings =
|
||||
await settingsStore.getResolvedSettings()
|
||||
void knowledgeService
|
||||
.setEmbeddingProvider(
|
||||
createEmbeddingProvider(knowledgeRuntimeSettings)
|
||||
)
|
||||
.catch(() => undefined)
|
||||
void knowledgeService
|
||||
.setRerankProvider(
|
||||
createRerankProvider(knowledgeRuntimeSettings)
|
||||
)
|
||||
.catch(() => undefined)
|
||||
assistantDatabase = new AssistantDatabase(
|
||||
knowledgeService = startupKnowledgeService
|
||||
const startupAssistantDatabase = new AssistantDatabase(
|
||||
join(app.getPath('userData'), 'assistant.sqlite')
|
||||
)
|
||||
assistantDatabase.initialize(defaultWorkspace)
|
||||
assistantDatabase.ensureChannelProjects(
|
||||
defaultWorkspace,
|
||||
initialRuntimeSettings.defaultModelProfileId
|
||||
)
|
||||
channelSettingsStore.reportRuntimeSelectionRepairs(
|
||||
assistantDatabase.repairConversationRuntimeSelections(
|
||||
initialRuntimeSettings
|
||||
)
|
||||
)
|
||||
assistantDatabase = startupAssistantDatabase
|
||||
const goodbuddyConfigService = new GoodBuddyConfigService(
|
||||
applicationSettingsStore,
|
||||
capabilityService
|
||||
)
|
||||
knowledgeGateway = new KnowledgeMcpGateway(knowledgeService, {
|
||||
magicNotesDatabase: assistantDatabase,
|
||||
configService: goodbuddyConfigService
|
||||
})
|
||||
await knowledgeGateway.start()
|
||||
const subagentService = new SubagentService(
|
||||
createDefaultModelRuntime(defaultWorkspace, initialSettings),
|
||||
assistantDatabase,
|
||||
undefined,
|
||||
createSubagentProfileRuntimes(
|
||||
defaultWorkspace,
|
||||
initialSettings
|
||||
)
|
||||
const startupKnowledgeGateway = new KnowledgeMcpGateway(
|
||||
startupKnowledgeService,
|
||||
{
|
||||
magicNotesDatabase: startupAssistantDatabase,
|
||||
configService: goodbuddyConfigService
|
||||
}
|
||||
)
|
||||
knowledgeGateway = startupKnowledgeGateway
|
||||
const createRuntimeWithCapabilities = async (
|
||||
settings: ResolvedRuntimeSettings,
|
||||
target: SelectedRuntimeTarget
|
||||
@@ -547,7 +519,7 @@ if (hasSingleInstanceLock) {
|
||||
browserCapability?.enabled && browserCapability.supported
|
||||
? browserService
|
||||
: undefined,
|
||||
knowledgeGateway,
|
||||
knowledgeGateway: startupKnowledgeGateway,
|
||||
webSearchEnabled: webSearchCapability?.enabled
|
||||
})
|
||||
}
|
||||
@@ -576,9 +548,53 @@ if (hasSingleInstanceLock) {
|
||||
resolved.target
|
||||
)
|
||||
}
|
||||
runtime = new AgentRuntimeController(
|
||||
await createConfiguredRuntime()
|
||||
const configuredRuntime = await runStartupPrerequisites({
|
||||
prepareDeepSeekHome: async () => {
|
||||
await mkdir(deepSeekHarnessHome, {
|
||||
recursive: true,
|
||||
mode: 0o700
|
||||
})
|
||||
},
|
||||
initializeKnowledgeAndGateway: async () => {
|
||||
await startupKnowledgeService.initialize()
|
||||
await Promise.all([
|
||||
startupKnowledgeService.setEmbeddingProvider(
|
||||
createEmbeddingProvider(initialResolvedSettings)
|
||||
).catch(() => undefined),
|
||||
startupKnowledgeService.setRerankProvider(
|
||||
createRerankProvider(initialResolvedSettings)
|
||||
).catch(() => undefined)
|
||||
])
|
||||
await startupKnowledgeGateway.start()
|
||||
},
|
||||
hydrateConfiguredRuntime: () =>
|
||||
createConfiguredRuntime(initialResolvedSettings),
|
||||
initializeAssistant: () => {
|
||||
startupAssistantDatabase.initialize(defaultWorkspace)
|
||||
startupAssistantDatabase.ensureChannelProjects(
|
||||
defaultWorkspace,
|
||||
initialRuntimeSettings.defaultModelProfileId
|
||||
)
|
||||
channelSettingsStore.reportRuntimeSelectionRepairs(
|
||||
startupAssistantDatabase.repairConversationRuntimeSelections(
|
||||
initialRuntimeSettings
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
const subagentService = new SubagentService(
|
||||
createDefaultModelRuntime(
|
||||
defaultWorkspace,
|
||||
initialResolvedSettings
|
||||
),
|
||||
startupAssistantDatabase,
|
||||
undefined,
|
||||
createSubagentProfileRuntimes(
|
||||
defaultWorkspace,
|
||||
initialResolvedSettings
|
||||
)
|
||||
)
|
||||
runtime = new AgentRuntimeController(configuredRuntime)
|
||||
selectedRuntimeManager = new SelectedRuntimeManager(
|
||||
createSelectedRuntime
|
||||
)
|
||||
|
||||
@@ -120,6 +120,69 @@ describe('KnowledgeDatabase', () => {
|
||||
.toHaveLength(1)
|
||||
})
|
||||
|
||||
it('repairs a mismatched user version without losing current-schema data', async () => {
|
||||
const { database, path } = await createDatabase()
|
||||
const knowledgeBase = database.createKnowledgeBase({
|
||||
name: 'Mismatch repair',
|
||||
storageMode: 'reference'
|
||||
})
|
||||
seedDocument(database, knowledgeBase.id, 'mismatch-repair')
|
||||
database.close()
|
||||
|
||||
const mismatch = new DatabaseSync(path)
|
||||
mismatch.exec('PRAGMA user_version = 10')
|
||||
mismatch.close()
|
||||
|
||||
const repaired = new KnowledgeDatabase(path)
|
||||
openDatabases.push(repaired)
|
||||
repaired.initialize()
|
||||
|
||||
expect(repaired.getKnowledgeBase(knowledgeBase.id)).toMatchObject({
|
||||
name: 'Mismatch repair'
|
||||
})
|
||||
expect(repaired.listDocuments(knowledgeBase.id)).toHaveLength(1)
|
||||
const inspection = new DatabaseSync(path)
|
||||
expect(inspection.prepare('PRAGMA user_version').get()).toEqual({
|
||||
user_version: 11
|
||||
})
|
||||
inspection.close()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['migration version', 'INSERT INTO schema_migrations VALUES (12, ?)', true],
|
||||
['user version', 'PRAGMA user_version = 12', false]
|
||||
])('rejects a future %s without downgrading it', async (
|
||||
_label,
|
||||
statement,
|
||||
hasParameter
|
||||
) => {
|
||||
const { database, path } = await createDatabase()
|
||||
database.close()
|
||||
const future = new DatabaseSync(path)
|
||||
if (hasParameter) {
|
||||
future.prepare(statement).run(new Date().toISOString())
|
||||
} else {
|
||||
future.exec(statement)
|
||||
}
|
||||
future.close()
|
||||
|
||||
const unsupported = new KnowledgeDatabase(path)
|
||||
expect(() => unsupported.initialize()).toThrow(
|
||||
'newer than supported version 11'
|
||||
)
|
||||
|
||||
const inspection = new DatabaseSync(path)
|
||||
expect(inspection.prepare('PRAGMA user_version').get()).toEqual({
|
||||
user_version: hasParameter ? 11 : 12
|
||||
})
|
||||
expect(
|
||||
inspection
|
||||
.prepare('SELECT MAX(version) AS version FROM schema_migrations')
|
||||
.get()
|
||||
).toEqual({ version: hasParameter ? 12 : 11 })
|
||||
inspection.close()
|
||||
})
|
||||
|
||||
it('keeps graph generation off unless explicitly enabled', async () => {
|
||||
const { database } = await createDatabase()
|
||||
const defaultLibrary = database.createKnowledgeBase({
|
||||
|
||||
@@ -4426,6 +4426,31 @@ export class KnowledgeDatabase {
|
||||
}
|
||||
|
||||
private migrate(database: DatabaseSync): void {
|
||||
const migrationTable = database
|
||||
.prepare(
|
||||
`SELECT 1 AS found FROM sqlite_schema
|
||||
WHERE type = 'table' AND name = 'schema_migrations'`
|
||||
)
|
||||
.get()
|
||||
if (migrationTable) {
|
||||
const versions = database
|
||||
.prepare(
|
||||
`SELECT
|
||||
(SELECT COALESCE(MAX(version), 0) FROM schema_migrations)
|
||||
AS migration_version,
|
||||
user_version
|
||||
FROM pragma_user_version`
|
||||
)
|
||||
.get()
|
||||
if (
|
||||
versions &&
|
||||
asNumber(versions, 'migration_version') === DATABASE_VERSION &&
|
||||
asNumber(versions, 'user_version') === DATABASE_VERSION
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
database.exec('BEGIN IMMEDIATE')
|
||||
try {
|
||||
database.exec(`
|
||||
@@ -4443,6 +4468,17 @@ export class KnowledgeDatabase {
|
||||
`Knowledge database version ${currentVersion} is newer than supported version ${DATABASE_VERSION}`
|
||||
)
|
||||
}
|
||||
const userVersionRow = database
|
||||
.prepare('PRAGMA user_version')
|
||||
.get()
|
||||
const userVersion = userVersionRow
|
||||
? asNumber(userVersionRow, 'user_version')
|
||||
: 0
|
||||
if (userVersion > DATABASE_VERSION) {
|
||||
throw new Error(
|
||||
`Knowledge database user version ${userVersion} is newer than supported version ${DATABASE_VERSION}`
|
||||
)
|
||||
}
|
||||
if (currentVersion < 1) {
|
||||
this.migrateToVersion1(database)
|
||||
database
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { runStartupPrerequisites } from './startup-prerequisites'
|
||||
|
||||
function deferred<T = void>(): {
|
||||
promise: Promise<T>
|
||||
resolve: (value: T) => void
|
||||
reject: (reason: unknown) => void
|
||||
} {
|
||||
let resolve!: (value: T) => void
|
||||
let reject!: (reason: unknown) => void
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise
|
||||
reject = rejectPromise
|
||||
})
|
||||
return { promise, resolve, reject }
|
||||
}
|
||||
|
||||
describe('runStartupPrerequisites', () => {
|
||||
it('starts independent work before synchronous initialization and waits for every branch', async () => {
|
||||
const order: string[] = []
|
||||
const deepSeekHome = deferred()
|
||||
const knowledgeAndGateway = deferred()
|
||||
const configuredRuntime = deferred<{ id: string }>()
|
||||
|
||||
const result = runStartupPrerequisites({
|
||||
prepareDeepSeekHome: () => {
|
||||
order.push('deepseek')
|
||||
return deepSeekHome.promise
|
||||
},
|
||||
initializeKnowledgeAndGateway: () => {
|
||||
order.push('knowledge')
|
||||
return knowledgeAndGateway.promise
|
||||
},
|
||||
hydrateConfiguredRuntime: () => {
|
||||
order.push('runtime')
|
||||
return configuredRuntime.promise
|
||||
},
|
||||
initializeAssistant: () => {
|
||||
order.push('assistant')
|
||||
}
|
||||
})
|
||||
const completed = vi.fn()
|
||||
void result.then(completed)
|
||||
|
||||
expect(order).toEqual([
|
||||
'deepseek',
|
||||
'knowledge',
|
||||
'runtime',
|
||||
'assistant'
|
||||
])
|
||||
|
||||
configuredRuntime.resolve({ id: 'configured' })
|
||||
knowledgeAndGateway.resolve()
|
||||
await Promise.resolve()
|
||||
expect(completed).not.toHaveBeenCalled()
|
||||
|
||||
deepSeekHome.resolve()
|
||||
await expect(result).resolves.toEqual({ id: 'configured' })
|
||||
expect(completed).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('settles every started branch before propagating synchronous initialization failure', async () => {
|
||||
const assistantError = new Error('assistant failed')
|
||||
const deepSeekHome = deferred()
|
||||
const knowledgeAndGateway = deferred()
|
||||
const configuredRuntime = deferred<{ id: string }>()
|
||||
|
||||
const result = runStartupPrerequisites({
|
||||
prepareDeepSeekHome: () => deepSeekHome.promise,
|
||||
initializeKnowledgeAndGateway: () =>
|
||||
knowledgeAndGateway.promise,
|
||||
hydrateConfiguredRuntime: () => configuredRuntime.promise,
|
||||
initializeAssistant: () => {
|
||||
throw assistantError
|
||||
}
|
||||
})
|
||||
const rejected = vi.fn()
|
||||
void result.catch(rejected)
|
||||
|
||||
deepSeekHome.resolve()
|
||||
knowledgeAndGateway.resolve()
|
||||
await Promise.resolve()
|
||||
expect(rejected).not.toHaveBeenCalled()
|
||||
|
||||
configuredRuntime.resolve({ id: 'unused' })
|
||||
await expect(result).rejects.toBe(assistantError)
|
||||
expect(rejected).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not publish an async branch failure until the other branches settle', async () => {
|
||||
const runtimeError = new Error('runtime failed')
|
||||
const deepSeekHome = deferred()
|
||||
const knowledgeAndGateway = deferred()
|
||||
|
||||
const result = runStartupPrerequisites({
|
||||
prepareDeepSeekHome: () => deepSeekHome.promise,
|
||||
initializeKnowledgeAndGateway: () =>
|
||||
knowledgeAndGateway.promise,
|
||||
hydrateConfiguredRuntime: () =>
|
||||
Promise.reject(runtimeError),
|
||||
initializeAssistant: () => undefined
|
||||
})
|
||||
const rejected = vi.fn()
|
||||
void result.catch(rejected)
|
||||
|
||||
await Promise.resolve()
|
||||
expect(rejected).not.toHaveBeenCalled()
|
||||
|
||||
deepSeekHome.resolve()
|
||||
knowledgeAndGateway.resolve()
|
||||
await expect(result).rejects.toBe(runtimeError)
|
||||
expect(rejected).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
export type StartupPrerequisiteDependencies<ConfiguredRuntime> = {
|
||||
prepareDeepSeekHome: () => Promise<void>
|
||||
initializeKnowledgeAndGateway: () => Promise<void>
|
||||
hydrateConfiguredRuntime: () => Promise<ConfiguredRuntime>
|
||||
initializeAssistant: () => void
|
||||
}
|
||||
|
||||
function startObserved<T>(operation: () => Promise<T>): Promise<T> {
|
||||
let started: Promise<T>
|
||||
try {
|
||||
started = Promise.resolve(operation())
|
||||
} catch (error) {
|
||||
started = Promise.reject(error)
|
||||
}
|
||||
void started.catch(() => undefined)
|
||||
return started
|
||||
}
|
||||
|
||||
export async function runStartupPrerequisites<ConfiguredRuntime>(
|
||||
dependencies: StartupPrerequisiteDependencies<ConfiguredRuntime>
|
||||
): Promise<ConfiguredRuntime> {
|
||||
const deepSeekHomeReady = startObserved(
|
||||
dependencies.prepareDeepSeekHome
|
||||
)
|
||||
const knowledgeAndGatewayReady = startObserved(
|
||||
dependencies.initializeKnowledgeAndGateway
|
||||
)
|
||||
const configuredRuntimeReady = startObserved(
|
||||
dependencies.hydrateConfiguredRuntime
|
||||
)
|
||||
|
||||
let assistantInitializationFailed = false
|
||||
let assistantInitializationError: unknown
|
||||
try {
|
||||
dependencies.initializeAssistant()
|
||||
} catch (error) {
|
||||
assistantInitializationFailed = true
|
||||
assistantInitializationError = error
|
||||
}
|
||||
|
||||
const [deepSeekHome, knowledgeAndGateway, configuredRuntime] =
|
||||
await Promise.allSettled([
|
||||
deepSeekHomeReady,
|
||||
knowledgeAndGatewayReady,
|
||||
configuredRuntimeReady
|
||||
] as const)
|
||||
|
||||
if (assistantInitializationFailed) {
|
||||
throw assistantInitializationError
|
||||
}
|
||||
if (deepSeekHome.status === 'rejected') {
|
||||
throw deepSeekHome.reason
|
||||
}
|
||||
if (knowledgeAndGateway.status === 'rejected') {
|
||||
throw knowledgeAndGateway.reason
|
||||
}
|
||||
if (configuredRuntime.status === 'rejected') {
|
||||
throw configuredRuntime.reason
|
||||
}
|
||||
return configuredRuntime.value
|
||||
}
|
||||
Reference in New Issue
Block a user