fix: eagerly warm extension host stores from engine TaskStores (#3340)

## Summary

Warm the extension-host task stores **up front** at dashboard startup
instead of letting the first `fn_task_*` call lazily boot a second
PostgreSQL pool per project.

## What changed

`packages/cli/src/commands/dashboard.ts`:

- After the dashboard boots, iterate every registered project (from
`centralCoreForEngine.listProjects()`) and call
`setHostTaskStore(p.path, engine.getTaskStore())` for each non-cwd
project that already has a running `ProjectEngine`.
- Reuses each engine's **existing** `TaskStore` directly — no new
backend connection, no schema advisory-lock contention, no extra
connection-pool exhaustion.
- `cwd` is skipped because its store is already injected at startup.
- Per-project failures are non-fatal (warn) and a failed project listing
logs a single warn — dashboard startup never blocks on this.
- `.changeset/extension-host-store-warmup.md` (patch, fix).

## Why

Left on its own, the first extension tool call (`fn_task_update`,
`fn_task_archive`, `fn_agent_show`, …) for a non-cwd project falls
through to `createTaskStoreForBackend`, which boots a **second**
PostgreSQL connection pool on demand. On busy hosts that lazy boot can
time out, or the call stalls behind pool/startup contention — the
classic "first `fn_task_*` call is slow or errors" experience.
Pre-populating from the already-running engines removes that lazy
worst-case path entirely.

## Verification

- `pnpm verify:fast` — PASS (13 steps, 115s): CLI `tsup` build green,
scoped typecheck/build green, boot smoke green (`fn --help` + real
`serve` with `GET /api/health` 200).
- Cherry-picked cleanly onto current `origin/main` (`5532019fd`); branch
is up-to-date with `origin/main` at PR time.

## Files

- `packages/cli/src/commands/dashboard.ts` (+30)
- `.changeset/extension-host-store-warmup.md` (new)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved dashboard startup reliability by reusing existing project
task connections.
* Prevented extension task tools from creating duplicate connection
pools.
* Added non-blocking warnings when individual project initialization or
discovery fails.
* Dashboard startup now reports how many project task stores were
successfully prepared.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
ischindl
2026-08-07 09:25:39 +02:00
committed by GitHub
parent de38ead4c9
commit 4739f8aa67
2 changed files with 37 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Warm extension-host task stores up front so fn_task_* tools never hit a lazy second-pool boot.
category: fix
dev: Populates setHostTaskStore for every registered project from the already-running ProjectEngine TaskStores at dashboard startup, so extension API tools (fn_task_update, fn_task_archive, fn_agent_show) find a cached store and never fall through to createTaskStoreForBackend, which could time out creating a second connection pool. cwd is skipped because its store is already injected. Non-fatal warnings on any per-project failure.

View File

@@ -2272,6 +2272,36 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
}
}
// FNXC:ExtensionHostStoreWarmup 2026-07-18-19:20:
// Pre-populate setHostTaskStore for all registered projects from already-
// running ProjectEngine TaskStores, so extension API tools (fn_task_archive,
// fn_task_update, etc.) find a cached store and never fall through to
// createTaskStoreForBackend (which times out creating a second pool).
// Reuses each engine's existing TaskStore directly — no new PG boot needed.
void (async () => {
try {
const projects = await centralCoreForEngine.listProjects();
// Skip cwd — its store is already injected at line 928.
const nonCwd = projects.filter((p) => p.path !== cwd);
for (const p of nonCwd) {
try {
const engine = engineManager.getEngine(p.id);
if (!engine) continue;
setHostTaskStore(p.path, engine.getTaskStore());
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logSink.warn(`Failed to warm extension store for ${p.name} (${p.path}): ${msg}`, "extension");
}
}
if (nonCwd.length > 0) {
logSink.log(`Warmed extension host stores for ${nonCwd.length} project(s)`, "extension");
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logSink.warn(`Failed to list projects for store warmup: ${msg}`, "extension");
}
})();
disposeCallbacks.push(async () => {
if (hybridExecutor) {
await hybridExecutor.shutdown();