Summary
The plan template writes its runtime state — the SQLite database under data/
and a per-plan MDX mirror under plans/ — inside the project root, which is also
the Vite watch root. Neither @agent-native/core's Vite plugin nor the template's
vite.config.ts excludes those paths from the watcher, so an app write registers
as a source change and the dev server commands a full page reload. The reload
drives the next write, and the cycle sustains itself.
Measured on a plan-template app: 24 full reloads in 15 seconds, settling into
a reload every 2–3 seconds indefinitely, with the plan editor open and idle.
Environment
@agent-native/core 0.133.1 (where this was measured)
- Template:
plan, standalone shape
- Node 22.22.2, pnpm 11.18.0, macOS
The relevant code paths are unchanged on main @ 215ed11 (0.136.2) — see
Evidence below — so this should still reproduce on current main.
Reproduction
npx agent-native create my-plan-app --template plan
cd my-plan-app
pnpm install
pnpm dev
Open a plan in the browser and leave it idle. The page reloads every few seconds.
To see the loop name its own trigger, add this plugin as the first entry in
plugins in vite.config.ts:
const reloadSpy = () => ({
name: "reload-spy",
configureServer(server: any) {
const send = server.ws.send.bind(server.ws);
server.ws.send = (...args: any[]) => {
if (args[0]?.type === "full-reload") {
console.log(`[reload-spy] full-reload ${JSON.stringify(args[0])}`);
}
return send(...args);
};
server.watcher.on("change", (p: string) =>
console.log(`[reload-spy] watcher change: ${p}`),
);
},
});
The dev-server log then shows watcher change events for data/app.db-wal and
plans/<slug>/plan.mdx, each followed by a full-reload.
Evidence
Runtime state is resolved relative to the project root:
packages/core/src/server/better-auth-instance.ts:1553 —
getDatabaseUrl("file:./data/app.db"). SQLite in WAL mode also churns
data/app.db-wal and data/app.db-shm on every write.
templates/plan/server/lib/local-plan-files.ts:87 —
return path.resolve(process.cwd(), "plans")
templates/plan/server/lib/local-plan-files.ts:407,411 — fs.mkdir(folder) then
fs.writeFile(path.join(folder, "plan.mdx"), …), reached from every plan-writing
action (update-visual-plan, create-*, patch-visual-plan-source,
restore-plan-version, …).
Nothing excludes those paths from the watcher:
templates/plan/vite.config.ts — the whole file is 24 lines and sets no server
key at all, so server.watch.ignored is Vite's default (which does not cover
data/ or plans/).
packages/core/src/vite/ — no watch.ignored is contributed by the
agentNative() plugin.
The template already treats these as generated, which is what makes the omission
look like an oversight rather than a decision —
templates/plan/.gitignore carries /plans/* under the comment
# Local plan data generated by tests/dev runs.
A Tailwind-shaped detail worth noting: the plan template ships @tailwindcss/vite
(templates/plan/package.json:79), whose hotUpdate hook treats the .mdx write
as a source change because MDX is inside its content scan. Removing Tailwind from
the app made the loop stop in our testing, which is why the report singles it out —
but the underlying cause is the unexcluded watch paths, not Tailwind.
Suggested fix
Set watch exclusions for app-written state at the framework level, so every
scaffold inherits them rather than each app rediscovering this. Either:
- have the
agentNative() Vite plugin contribute server.watch.ignored entries
for the directories core itself owns (data/, .generated/), and have the
plan template add its own (plans/); or
- ship the exclusions in the template's
vite.config.ts.
Option 1 seems preferable — data/ is core's own path, so an app author has no
reason to know it needs excluding.
Workaround
Adding this to the app's vite.config.ts stopped the loop completely (0 reloads
and 0 watcher events with a plan open and scrolled end to end):
server: {
watch: {
ignored: ["**/data/**", "**/plans/**"],
},
},
Offer
Happy to submit a PR implementing either option (my lean is option 1) if the maintainers are open to it — glad to contribute the fix back.
Summary
The
plantemplate writes its runtime state — the SQLite database underdata/and a per-plan MDX mirror under
plans/— inside the project root, which is alsothe Vite watch root. Neither
@agent-native/core's Vite plugin nor the template'svite.config.tsexcludes those paths from the watcher, so an app write registersas a source change and the dev server commands a full page reload. The reload
drives the next write, and the cycle sustains itself.
Measured on a
plan-template app: 24 full reloads in 15 seconds, settling intoa reload every 2–3 seconds indefinitely, with the plan editor open and idle.
Environment
@agent-native/core0.133.1 (where this was measured)plan, standalone shapeThe relevant code paths are unchanged on
main@ 215ed11 (0.136.2) — seeEvidence below — so this should still reproduce on current
main.Reproduction
npx agent-native create my-plan-app --template plan cd my-plan-app pnpm install pnpm devOpen a plan in the browser and leave it idle. The page reloads every few seconds.
To see the loop name its own trigger, add this plugin as the first entry in
pluginsinvite.config.ts:The dev-server log then shows watcher
changeevents fordata/app.db-walandplans/<slug>/plan.mdx, each followed by afull-reload.Evidence
Runtime state is resolved relative to the project root:
packages/core/src/server/better-auth-instance.ts:1553—getDatabaseUrl("file:./data/app.db"). SQLite in WAL mode also churnsdata/app.db-walanddata/app.db-shmon every write.templates/plan/server/lib/local-plan-files.ts:87—return path.resolve(process.cwd(), "plans")templates/plan/server/lib/local-plan-files.ts:407,411—fs.mkdir(folder)thenfs.writeFile(path.join(folder, "plan.mdx"), …), reached from every plan-writingaction (
update-visual-plan,create-*,patch-visual-plan-source,restore-plan-version, …).Nothing excludes those paths from the watcher:
templates/plan/vite.config.ts— the whole file is 24 lines and sets noserverkey at all, so
server.watch.ignoredis Vite's default (which does not coverdata/orplans/).packages/core/src/vite/— nowatch.ignoredis contributed by theagentNative()plugin.The template already treats these as generated, which is what makes the omission
look like an oversight rather than a decision —
templates/plan/.gitignorecarries/plans/*under the comment# Local plan data generated by tests/dev runs.A Tailwind-shaped detail worth noting: the plan template ships
@tailwindcss/vite(
templates/plan/package.json:79), whosehotUpdatehook treats the.mdxwriteas a source change because MDX is inside its content scan. Removing Tailwind from
the app made the loop stop in our testing, which is why the report singles it out —
but the underlying cause is the unexcluded watch paths, not Tailwind.
Suggested fix
Set watch exclusions for app-written state at the framework level, so every
scaffold inherits them rather than each app rediscovering this. Either:
agentNative()Vite plugin contributeserver.watch.ignoredentriesfor the directories core itself owns (
data/,.generated/), and have theplantemplate add its own (plans/); orvite.config.ts.Option 1 seems preferable —
data/is core's own path, so an app author has noreason to know it needs excluding.
Workaround
Adding this to the app's
vite.config.tsstopped the loop completely (0 reloadsand 0 watcher events with a plan open and scrolled end to end):
Offer
Happy to submit a PR implementing either option (my lean is option 1) if the maintainers are open to it — glad to contribute the fix back.