feat(storage): move desktop SQLite ownership into a storage utility process
Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
This commit is contained in:
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { parseEnv } from 'node:util'
|
||||
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
|
||||
import { extractExperiences } from '../src/main/assistant/supervision-experiences.ts'
|
||||
import { extractExperiences } from '../src/main/assistant/experience-extraction-service.ts'
|
||||
|
||||
const [prepared, envPath, directory, maxCallsArg] = process.argv.slice(2)
|
||||
const config = parseEnv(readFileSync(envPath, 'utf8'))
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/* global process, console, setTimeout, clearTimeout */
|
||||
import { spawn, spawnSync } from 'node:child_process'
|
||||
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const script = fileURLToPath(import.meta.url)
|
||||
const root = resolve(dirname(script), '..')
|
||||
const base = join(root, 'temp', 'goodbuddy-final-tests-final')
|
||||
const stateFile = join(base, 'status.json')
|
||||
const lockFile = join(base, 'runner.lock')
|
||||
const npm = join(root, 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
||||
const [mode = 'status', selection = 'all', ...filters] = process.argv.slice(2)
|
||||
const alive = (pid) => {
|
||||
try { process.kill(pid, 0); return true } catch { return false }
|
||||
}
|
||||
const readState = () => JSON.parse(readFileSync(stateFile, 'utf8'))
|
||||
const killTree = (pid) => {
|
||||
if (!pid || !alive(pid)) return
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, timeout: 15000 })
|
||||
} else {
|
||||
try { process.kill(-pid, 'SIGKILL') } catch { /* Already exited. */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'start') {
|
||||
if (!['all', 'test', 'typecheck', 'lint'].includes(selection)) throw new Error('Unknown validation selection')
|
||||
if (!statSync(join(root, 'temp')).isDirectory()) throw new Error('Repository temp directory is required')
|
||||
mkdirSync(base, { recursive: true })
|
||||
if (existsSync(lockFile)) {
|
||||
const pid = Number(readFileSync(lockFile, 'utf8'))
|
||||
if (alive(pid)) throw new Error(`Validation runner already active: ${pid}`)
|
||||
rmSync(lockFile)
|
||||
}
|
||||
const lock = openSync(lockFile, 'wx')
|
||||
writeFileSync(lock, String(process.pid))
|
||||
closeSync(lock)
|
||||
const log = openSync(join(base, 'runner.log'), 'a')
|
||||
try {
|
||||
const child = spawn(process.execPath, [script, 'worker', selection, ...filters], {
|
||||
cwd: root, detached: true, windowsHide: true, stdio: ['ignore', log, log]
|
||||
})
|
||||
await new Promise((resolveSpawn, reject) => {
|
||||
child.once('spawn', resolveSpawn)
|
||||
child.once('error', reject)
|
||||
})
|
||||
writeFileSync(lockFile, String(child.pid))
|
||||
child.unref()
|
||||
console.log(JSON.stringify({ runnerPid: child.pid, selection, logs: base }))
|
||||
} catch (error) {
|
||||
rmSync(lockFile, { force: true })
|
||||
throw error
|
||||
} finally { closeSync(log) }
|
||||
} else if (mode === 'status') {
|
||||
const state = readState()
|
||||
console.log(JSON.stringify({ ...state, runnerAlive: alive(state.pid) }, null, 2))
|
||||
} else if (mode === 'stop') {
|
||||
const state = readState()
|
||||
if (state.status === 'running') {
|
||||
killTree(state.childPid)
|
||||
killTree(state.pid)
|
||||
state.status = 'stopped'
|
||||
state.endedAt = new Date().toISOString()
|
||||
writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||
rmSync(lockFile, { force: true })
|
||||
}
|
||||
} else if (mode === 'worker') {
|
||||
const run = join(base, new Date().toISOString().replace(/[:.]/g, '-'))
|
||||
const tmp = join(run, 'tmp')
|
||||
mkdirSync(tmp, { recursive: true })
|
||||
if (!statSync(tmp).isDirectory()) throw new Error('Child temp directory was not created')
|
||||
const state = { pid: process.pid, status: 'running', startedAt: new Date().toISOString(), run, results: [] }
|
||||
const save = () => writeFileSync(stateFile, JSON.stringify(state, null, 2))
|
||||
let child
|
||||
let interrupted = false
|
||||
const interrupt = (reason) => {
|
||||
interrupted = true
|
||||
state.reason = reason
|
||||
killTree(child?.pid)
|
||||
}
|
||||
process.on('SIGINT', () => interrupt('SIGINT'))
|
||||
process.on('SIGTERM', () => interrupt('SIGTERM'))
|
||||
process.on('exit', () => killTree(child?.pid))
|
||||
const deadline = setTimeout(() => interrupt('30-minute overall deadline exceeded'), 30 * 60 * 1000)
|
||||
save()
|
||||
try {
|
||||
for (const job of selection === 'all' ? ['test', 'typecheck', 'lint'] : [selection]) {
|
||||
if (interrupted) break
|
||||
const started = Date.now()
|
||||
const report = join(run, 'tests.json')
|
||||
const args = job === 'test'
|
||||
? ['test', '--', ...filters, '--reporter=default', '--reporter=json', `--outputFile.json=${report}`]
|
||||
: ['run', job]
|
||||
const logPath = join(run, `${job}.log`)
|
||||
const log = openSync(logPath, 'w')
|
||||
try {
|
||||
child = spawn(process.execPath, [npm, ...args], {
|
||||
cwd: root, windowsHide: true, detached: process.platform !== 'win32',
|
||||
env: { ...process.env, TEMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMP: join(tmpdir(), 'goodbuddy-final-tests-final'), TMPDIR: join(tmpdir(), 'goodbuddy-final-tests-final'), NO_COLOR: '1' },
|
||||
stdio: ['ignore', log, log]
|
||||
})
|
||||
state.active = job
|
||||
state.childPid = child.pid
|
||||
save()
|
||||
const result = await new Promise((resolveExit, reject) => {
|
||||
child.once('error', reject)
|
||||
child.once('exit', (code, signal) => resolveExit({ code, signal }))
|
||||
})
|
||||
const entry = { job, ...result, seconds: (Date.now() - started) / 1000, log: logPath }
|
||||
if (job === 'test' && existsSync(report)) {
|
||||
const data = JSON.parse(readFileSync(report, 'utf8'))
|
||||
entry.totals = Object.fromEntries(Object.entries(data).filter(([key]) => key.startsWith('num') || key === 'success'))
|
||||
entry.failedFiles = data.testResults.filter((file) => file.status === 'failed').map((file) => ({
|
||||
file: file.name.replace(root, '').replaceAll('\\', '/'),
|
||||
tests: file.assertionResults.filter((test) => test.status === 'failed').map((test) => test.fullName)
|
||||
}))
|
||||
}
|
||||
state.results.push(entry)
|
||||
} finally {
|
||||
killTree(child?.pid)
|
||||
child = undefined
|
||||
closeSync(log)
|
||||
delete state.childPid
|
||||
delete state.active
|
||||
save()
|
||||
}
|
||||
}
|
||||
state.status = interrupted ? 'interrupted' : 'completed'
|
||||
} catch (error) {
|
||||
state.status = 'error'
|
||||
state.reason = error.message
|
||||
} finally {
|
||||
clearTimeout(deadline)
|
||||
killTree(child?.pid)
|
||||
try { rmSync(tmp, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 }) }
|
||||
catch (error) { state.cleanupError = error.code }
|
||||
state.endedAt = new Date().toISOString()
|
||||
save()
|
||||
rmSync(lockFile, { force: true })
|
||||
}
|
||||
} else {
|
||||
throw new Error('Usage: node scripts/final-regression.mjs start [all|test|typecheck|lint] [test filters...] | status | stop')
|
||||
}
|
||||
@@ -86,7 +86,7 @@ try {
|
||||
'CANDIDATES:', JSON.stringify(request.candidates), 'Return only JSON.'].join('\n\n')),
|
||||
{ supervisionRunId, heartbeatRunId: run.id })
|
||||
})
|
||||
const plan = heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
|
||||
const plan = await heartbeat.create({ name: '项目回顾', scope: { kind: 'projects', projectIds: [project.id] }, timezone: 'UTC',
|
||||
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
|
||||
let tick = 0
|
||||
const due = async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { parseEnv } from 'node:util'
|
||||
import { AssistantDatabase } from '../src/main/assistant/assistant-database.ts'
|
||||
import { assignStories } from '../src/main/assistant/supervision-stories.ts'
|
||||
import { assignStories } from '../src/main/assistant/story-assignment-service.ts'
|
||||
|
||||
const [prepared, envPath, directory, mode, maxCallsArg] = process.argv.slice(2)
|
||||
const config = parseEnv(readFileSync(envPath, 'utf8'))
|
||||
|
||||
@@ -12,6 +12,8 @@ import { SupervisionModelPool } from '../src/main/assistant/supervision-model-po
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
|
||||
import { supervisionReviewSettingsSchema } from '../src/shared/supervision-review-contracts'
|
||||
import { ReadonlyQueryReader } from '../src/main/readonly-query-reader'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [mode, parent, envPath] = process.argv.slice(2)
|
||||
@@ -25,7 +27,8 @@ async function main() {
|
||||
bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
|
||||
const db = new AssistantDatabase(path)
|
||||
db.initialize(directory)
|
||||
db.enableReadonlyWorker(worker)
|
||||
const reader = new ReadonlyQueryReader('assistant', path, worker)
|
||||
const storage = asyncSupervisionStorage(db, undefined, reader).supervision
|
||||
const sql = (db as unknown as { requireDatabase(): DatabaseSync }).requireDatabase()
|
||||
const pool = new SupervisionModelPool()
|
||||
const report: Record<string, unknown> = { mode, directory, externalCalls: 0 }
|
||||
@@ -49,7 +52,7 @@ async function main() {
|
||||
const counts = () => ['supervision_entities', 'supervision_events', 'supervision_event_entities', 'supervision_sources'].map(table =>
|
||||
[table, Number(sql.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()!.n)])
|
||||
const before = counts()
|
||||
await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
|
||||
await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
|
||||
const legacy = sql.prepare(`SELECT e.id FROM supervision_entities e WHERE e.confirmation_state != 'revoked' AND EXISTS (
|
||||
SELECT 1 FROM supervision_event_entities ee JOIN supervision_events ev ON ev.id = ee.event_id
|
||||
WHERE ee.entity_id = e.id AND ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?)
|
||||
@@ -61,14 +64,14 @@ async function main() {
|
||||
const legacyMs = performance.now() - legacyStart
|
||||
const start = performance.now()
|
||||
for (const project of projects) {
|
||||
const candidates = await db.supervisionCandidates(request, { projectId: project.id, crossProject: false })
|
||||
const candidates = await storage.supervisionCandidates(request, { projectId: project.id, crossProject: false })
|
||||
assert.equal(candidates.length, 100)
|
||||
assert(candidates.every(item => item.label.startsWith(project.id + ':')))
|
||||
assert.deepEqual(candidates.map(item => item.id), expected.get(project.id))
|
||||
}
|
||||
const isolatedMs = performance.now() - start
|
||||
const crossStart = performance.now()
|
||||
const candidates = await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
|
||||
const candidates = await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
|
||||
assert.equal(candidates.length, 100)
|
||||
rounds.push({ legacyMs, isolatedMs, crossProjectMs: performance.now() - crossStart })
|
||||
}
|
||||
@@ -106,7 +109,7 @@ async function main() {
|
||||
calls++
|
||||
return fetch(input, init)
|
||||
} })
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
|
||||
const service = createProductionSupervisorService(storage, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
|
||||
supervisionReview: supervisionReviewSettingsSchema.parse({ crossProject: false, experienceMinEvents: 200 }) }), async () => runtime, pool)
|
||||
const result = await service.run(request)
|
||||
assert.equal(result.status, 'completed')
|
||||
@@ -133,6 +136,7 @@ async function main() {
|
||||
} finally {
|
||||
report.externalCalls = calls
|
||||
report.elapsedMs = performance.now() - started
|
||||
await reader.close()
|
||||
db.close()
|
||||
pool.dispose()
|
||||
report.databaseBytes = (await stat(path)).size
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [directory, envPath, mode] = process.argv.slice(2)
|
||||
@@ -68,7 +69,7 @@ async function main() {
|
||||
const candidates = db.listSupervisionCandidates(state.request)
|
||||
writeFileSync(join(directory, 'candidates.private.json'), JSON.stringify(candidates))
|
||||
metrics.originalCandidates = candidates.length
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1, supervisorOrganizeTimeoutSeconds: 120 }), async () => runtime, pool)
|
||||
const originalStore = db.supervisionReviewStore.bind(db)
|
||||
const save = store.save.bind(store)
|
||||
store.save = (...args) => { save(...args); service.pause(incident.run.id) }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import type { AgentRuntime } from '../src/main/agent/runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [sourcePath, destinationPath, runId] = process.argv.slice(2)
|
||||
@@ -37,7 +38,7 @@ async function main() {
|
||||
const leaves = store.batches(runId, 20)
|
||||
assert.equal(leaves.length, 6)
|
||||
assert.equal(store.progress(runId).remainingSources, 0)
|
||||
const service = createProductionSupervisorService(db, async () => ({ supervisorModelConcurrency: 1 }),
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ supervisorModelConcurrency: 1 }),
|
||||
async () => ({ runtimeId: 'offline-replay', capability: 'chat', run } as AgentRuntime), pool)
|
||||
const result = await service.resume(runId)
|
||||
assert.equal(result.status, 'completed')
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AssistantDatabase } from '../src/main/assistant/assistant-database'
|
||||
import { createProductionSupervisorService } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
|
||||
|
||||
async function main() {
|
||||
const [sourceArg, outputArg, envArg, mode] = process.argv.slice(2)
|
||||
@@ -79,7 +80,7 @@ async function main() {
|
||||
title: 'Private selected conversation', updatedAt: Date.parse(scope.lastAt) },
|
||||
messages: messages.map(message => ({ id: message.id, content: message.content, role: message.role,
|
||||
createdAt: Date.parse(message.created_at), state: 'complete' })) }])
|
||||
const service = createProductionSupervisorService(db, async () => ({ heartbeatEnabled: true,
|
||||
const service = createProductionSupervisorService(asyncSupervisionStorage(db).supervision, async () => ({ heartbeatEnabled: true,
|
||||
supervisorModelConcurrency: 2, supervisorOrganizeTimeoutSeconds: 180,
|
||||
supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 20, executionSeconds: 600 }
|
||||
}), async () => runtime, pool)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/* global require, process, __dirname, console, setTimeout, clearTimeout */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
// node scripts/supervision-storage-live.cjs <current-runtime-settings.json>
|
||||
const { build } = require('esbuild')
|
||||
const { spawn } = require('node:child_process')
|
||||
const { mkdir, mkdtemp, rm } = require('node:fs/promises')
|
||||
const { join, resolve } = require('node:path')
|
||||
async function main() {
|
||||
const repo = resolve(__dirname, '..')
|
||||
if (!process.argv[2]) throw new Error('Runtime settings path required')
|
||||
const parent = join(repo, 'temp', 'supervision-storage-live')
|
||||
await mkdir(parent, { recursive: true })
|
||||
const root = await mkdtemp(join(parent, 'probe-'))
|
||||
try {
|
||||
for (const [entry, output, format] of [
|
||||
['scripts/supervision-storage-live.ts', 'main.mjs', 'esm'],
|
||||
['src/main/desktop-storage-entry.ts', 'desktop-storage-entry.mjs', 'esm'],
|
||||
['src/main/readonly-query-worker.ts', 'readonly-query-worker.cjs', 'cjs'],
|
||||
['src/main/assistant-storage-worker.ts', 'assistant-storage-worker.cjs', 'cjs']
|
||||
]) await build({ entryPoints: [join(repo, entry)], outfile: join(root, output), bundle: true,
|
||||
platform: 'node', format, packages: 'external', logLevel: 'silent' })
|
||||
const env = { ...process.env, GB_SUPERVISION_STORAGE_ROOT: root, GB_SUPERVISION_SETTINGS: resolve(process.argv[2]), TEMP: root, TMP: root, TMPDIR: root }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
const child = spawn(require('electron'), [join(root, 'main.mjs')], { env, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
child.stdout.on('data', data => process.stdout.write(data))
|
||||
// The probe prints only bounded metrics; native stderr is not a credential-safe report.
|
||||
child.stderr.resume()
|
||||
const timer = setTimeout(() => child.kill(), 300_000)
|
||||
try { process.exitCode = (await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })) ?? 1 }
|
||||
finally { clearTimeout(timer) }
|
||||
} finally { await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }
|
||||
}
|
||||
main().catch(() => { console.error('Supervision storage probe failed before completion'); process.exitCode = 1 })
|
||||
@@ -0,0 +1,134 @@
|
||||
// Run through supervision-storage-live.cjs. Only synthetic evidence is sent to the model.
|
||||
import assert from 'node:assert/strict'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { readFile, writeFile, rm } from 'node:fs/promises'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { app, safeStorage } from 'electron'
|
||||
import { RuntimeSettingsStore } from '../src/main/runtime-settings-store'
|
||||
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
|
||||
import { DesktopStorageClient } from '../src/main/desktop-storage-client'
|
||||
import { createSupervisionDomainPorts } from '../src/main/assistant/supervision-domain-ports'
|
||||
import { createProductionSupervisorService, createProductionSuggestionPhraser } from '../src/main/assistant/supervision-production'
|
||||
import { SupervisionModelPool } from '../src/main/assistant/supervision-model-pool'
|
||||
import { HeartbeatService } from '../src/main/assistant/heartbeat-service'
|
||||
import { deriveSuggestions } from '../src/main/assistant/supervision-suggester'
|
||||
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
|
||||
|
||||
const root = process.env.GB_SUPERVISION_STORAGE_ROOT!
|
||||
const settingsPath = process.env.GB_SUPERVISION_SETTINGS!
|
||||
const calls: Array<{ status?: number; inputBytes: number; ms?: number }> = []
|
||||
const report: Record<string, unknown> = { syntheticOnly: true, maximumHttpCalls: 4, calls }
|
||||
async function run() {
|
||||
report.stage = 'preflight'
|
||||
assert(root && settingsPath)
|
||||
const profileDirectory = join(root, 'electron')
|
||||
// Electron must see the copied encryption state before its ready event.
|
||||
mkdirSync(profileDirectory)
|
||||
app.setPath('userData', profileDirectory)
|
||||
if (process.platform === 'win32') {
|
||||
const state = JSON.parse(readFileSync(join(dirname(settingsPath), 'Local State'), 'utf8'))
|
||||
writeFileSync(join(profileDirectory, 'Local State'), JSON.stringify({ os_crypt: state.os_crypt }))
|
||||
}
|
||||
await app.whenReady()
|
||||
const original = await readFile(settingsPath)
|
||||
const snapshot = join(root, 'settings.snapshot.json')
|
||||
await writeFile(snapshot, original)
|
||||
const settings = await new RuntimeSettingsStore(snapshot, {
|
||||
isAvailable: () => safeStorage.isEncryptionAvailable(),
|
||||
encrypt: () => { throw new Error('Probe does not write credentials') },
|
||||
decrypt: value => safeStorage.decryptString(value)
|
||||
}, {}).getResolvedSettings().finally(() => rm(snapshot, { force: true }))
|
||||
const application = JSON.parse(await readFile(join(dirname(settingsPath), 'application-settings.json'), 'utf8'))
|
||||
const selectedId = application.supervisorModelProfileId ?? settings.defaultModelProfileId
|
||||
const selected = settings.modelProfiles.find(profile => profile.id === selectedId)
|
||||
report.selection = { profileCount: settings.modelProfiles.length, explicitSupervisor: Boolean(application.supervisorModelProfileId),
|
||||
found: Boolean(selected), protocol: selected?.protocol, authentication: selected?.authentication, credentialAvailable: Boolean(selected?.apiKey) }
|
||||
assert(selected && selected.protocol !== 'openai-images-generations', 'No configured supervisor text model')
|
||||
assert(selected.authentication === 'none' || selected.apiKey, 'No configured credential')
|
||||
report.model = selected.modelName
|
||||
report.protocol = selected.protocol
|
||||
const storage = new DesktopStorageClient({ assistantPath: join(root, 'assistant.sqlite'), knowledgePath: join(root, 'knowledge.sqlite'),
|
||||
userDataPath: root, defaultRootPath: root, entryPath: join(root, 'desktop-storage-entry.mjs'),
|
||||
readerWorkerPath: join(root, 'readonly-query-worker.cjs'), upgradeWorkerPath: join(root, 'assistant-storage-worker.cjs') })
|
||||
const pool = new SupervisionModelPool()
|
||||
const runtime = () => new ModelAgentRuntime({ protocol: selected.protocol, authentication: selected.authentication,
|
||||
apiKey: selected.apiKey, baseUrl: selected.baseUrl, model: selected.modelName,
|
||||
requestHeaders: selected.requestHeaders, requestBody: selected.requestBody,
|
||||
maximumOutputTokens: 3000, defaultWorkspace: root,
|
||||
toolProvider: { listTools: async () => [], getApproval: () => { throw new Error('Probe tools disabled') },
|
||||
callTool: async () => { throw new Error('Probe tools disabled') }, releaseConversation: async () => {}, dispose: async () => {} },
|
||||
fetcher: async (input, init) => {
|
||||
assert(calls.length < 4, 'Probe HTTP capacity reached')
|
||||
const body = JSON.parse(String(init?.body))
|
||||
assert.equal(body.tools?.length ?? 0, 0)
|
||||
const call: (typeof calls)[number] = { inputBytes: Buffer.byteLength(String(init?.body)) }
|
||||
calls.push(call)
|
||||
console.log(JSON.stringify({ event: 'model-request', number: calls.length }))
|
||||
const start = performance.now()
|
||||
const response = await fetch(input, { ...init, signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(60_000)]) })
|
||||
call.status = response.status
|
||||
call.ms = Math.round(performance.now() - start)
|
||||
return response
|
||||
} })
|
||||
try {
|
||||
report.stage = 'storage-ready'
|
||||
await storage.ready
|
||||
const ports = createSupervisionDomainPorts(storage)
|
||||
const project = (await storage.call('assistant', 'listProjects', []))[0]!
|
||||
const now = Date.now()
|
||||
const messages = [
|
||||
'Synthetic project Aurora maintains a weather sensor calibration module. Decision: calibrate against a reference sensor before each field deployment. This replaced the earlier plan to calibrate only after deployment.',
|
||||
'Milestone: the pre-deployment reference calibration detected a two-degree offset, which we corrected before deployment. The field measurements then matched the reference. Lesson: verify calibration before deployment to prevent biased field data.',
|
||||
'Open question for the calibration module: should routine recalibration happen monthly or quarterly? No interval has been decided.'
|
||||
].map((content, index) => ({ id: randomUUID(), role: 'user' as const, content, state: 'complete' as const, createdAt: now + index }))
|
||||
await storage.call('assistant', 'saveLocalConversations', [[{
|
||||
header: { id: randomUUID(), projectId: project.id, title: 'Synthetic Aurora calibration', updatedAt: now + 2 }, messages
|
||||
}]])
|
||||
const request: SupervisionRunRequest = { trigger: 'heartbeat', scope: { kind: 'global' },
|
||||
timeRange: { from: new Date(now - 1000).toISOString(), to: new Date(now + 1000).toISOString() } }
|
||||
const getSettings = async () => ({ supervisorModelProfileId: selectedId, supervisorOrganizeTimeoutSeconds: 60,
|
||||
supervisorModelConcurrency: 1, supervisionReview: { pageSize: 10, batchCharacters: 8000, batchMessages: 10,
|
||||
executionSeconds: 300, experienceMinEvents: 2, storyThreadEvents: 20 } })
|
||||
const supervisor = createProductionSupervisorService(ports.supervision, getSettings, async () => runtime(), pool)
|
||||
const phrase = createProductionSuggestionPhraser(ports.supervision, getSettings, async () => runtime(), pool)
|
||||
const heartbeat = new HeartbeatService(ports.heartbeat, {
|
||||
review: async ({ run }) => {
|
||||
const result = await supervisor.run(request, run.id)
|
||||
assert.equal(result.status, 'completed')
|
||||
assert.equal(result.coverage?.complete, true)
|
||||
assert.equal(result.coverage?.stories?.status, 'completed')
|
||||
report.coverage = result.coverage
|
||||
return { status: result.status, runId: result.runId }
|
||||
},
|
||||
suggest: async ({ run, supervisionRunId }) => deriveSuggestions(ports.suggestions, phrase, { supervisionRunId, heartbeatRunId: run.id })
|
||||
})
|
||||
const config = await heartbeat.create({ name: 'Synthetic probe', scope: request.scope, timezone: 'UTC',
|
||||
recurrence: { type: 'daily', localTime: '09:00' }, enabled: true, lookbackHours: 24, retentionDays: 30 })
|
||||
const first = await heartbeat.runNow({ id: config.id, idempotencyKey: 'first' })
|
||||
report.stage = 'verify-publication'
|
||||
const activity = (await storage.call('assistant', 'listSupervisionActivity', [10, 0, config.id]))[0]!
|
||||
assert.equal(activity.status, 'completed')
|
||||
assert.equal(activity.suggestionStatus, 'completed')
|
||||
const stories = await ports.stories.list(request.scope)
|
||||
const experiences = await ports.experiences.list()
|
||||
const suggestions = await ports.suggestions.countForHeartbeat(first.id)
|
||||
assert(stories.length > 0 && experiences.length > 0 && suggestions > 0)
|
||||
const before = calls.length
|
||||
const quiet = await supervisor.run(request)
|
||||
assert.equal(quiet.status, 'no_change')
|
||||
assert.equal(calls.length, before)
|
||||
report.counts = { stories: stories.length, experiences: experiences.length, suggestions }
|
||||
report.noChangeCalls = calls.length - before
|
||||
report.usage = (await storage.call('assistant', 'getTokenUsageSummary', [])).systemTotals
|
||||
report.settingsUnchanged = (await readFile(settingsPath)).equals(original)
|
||||
assert.equal(report.settingsUnchanged, true)
|
||||
assert.equal(storage.pendingCount, 0)
|
||||
report.passed = true
|
||||
} finally { pool.dispose(); await storage.close() }
|
||||
}
|
||||
void run().catch(error => {
|
||||
report.passed = false
|
||||
report.errorType = error instanceof Error ? error.name : 'Error'
|
||||
// Do not print provider errors, headers, credentials, or settings.
|
||||
}).finally(() => { console.log(JSON.stringify(report)); app.exit(report.passed ? 0 : 1) })
|
||||
Reference in New Issue
Block a user