## Summary
Fixes a race where a stuck-kill during Plan Review could leave an
approved task stranded in `todo` as **unplanned**, so the scheduler
refused release to `in-progress` for several minutes (observed on
FN-1312: ~6m39s idle after Plan Review APPROVE).
### What went wrong
1. Finalize clears `status` early, then runs Plan Review.
2. Stuck-kill disposes the main triage session near the 30m processing
threshold.
3. Stale eviction only treated the main session as live, so the card
left `processing` while Plan Review / finalize was still running.
4. A second planner claimed `status: "planning"` and rewrote
`PROMPT.md`.
5. The first finalize moved `triage → todo` without clearing planning
statuses.
6. Hold-release saw planning/`needs-replan` and blocked: *“card is
unplanned and cannot enter processing column in-progress”*.
### Fix
In `packages/engine/src/triage.ts`:
- Track **finalizing** handoffs and **live Plan Review subagents** as
live planning work.
- Keep them in `getProcessingTaskIds`, refuse concurrent `specifyTask`,
and skip stale-processing eviction while they are live.
- **Defer** stuck-abort requeue during finalize (bump kill count only;
do not force `needs-replan`).
- Recover approved drafts with `status: null` (post early-clear), not
only `status: "planning"`.
- Re-assert `status: null` after the successful release move to todo.
### Tests
Regression coverage in `triage.test.ts` for eviction retention,
processing-id inclusion, null-status recovery, needs-replan
non-recovery, and deferred stuck-abort during finalize.
## Test plan
- [x] `pnpm --filter @fusion/engine exec vitest run
src/__tests__/triage.test.ts -t "evictStaleProcessing|specified triage
recovery|stuck|recoverApproved|markStuckAborted|defers stuck-abort"`
(related cases green)
- [ ] CI gate on this PR
- [ ] Optional: reproduce stuck-kill mid–Plan Review and confirm todo
releases without a second full replan
## Notes
Secondary report (stuck-kill immediately after all implementation steps
complete, then 16s auto-recovery) is **out of scope** here; existing
recovery already continued by skipping completed steps.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved recovery for approved tasks when Plan Review has completed,
including correct handling of `null`-status drafts.
* Avoided recovering unplanned seed drafts and tasks that should enter a
replanning flow.
* Refined stuck-abort requeue/cleanup to respect in-progress Plan Review
and finalize handoffs.
* Prevented stale-processing eviction from removing tasks while Plan
Review/subagent finalization is still active.
* Blocked new triage discovery and task specification when finalize/Plan
Review work is ongoing, preventing handoff disruption.
* Preserved durable task state during handoff completion
(approval/failure/replan outcomes).
* **Tests**
* Added expanded triage recovery and stuck-abort/stale-eviction
regression coverage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Prevent concurrent Fusion sessions and processes from consuming the same rotating refresh token. Share one refresh lock across legacy and subscription Anthropic credential aliases.
## Summary
- Heartbeat customTools inventory includes `fn_research_promote_finding`
(64 tools).
- Schema-applier bookkeeping for migration `0023` research feature
provenance.
- Seed upgrade fixtures include `mission_features.slice_id` for the
unique index.
## Evidence
- Full greens earlier this session: **29662309385** (#2323),
**29663526777** (#2325), **29664692957** (#2327).
- Tip red after FN-8297: heartbeat 63→64 + schema upgrade seed.
## Test plan
- [x] heartbeat expected-tools + schema upgrade tests green locally
- [ ] Full Suite all shards green on tip after merge
## Summary
- Default `createAgentTask` in dashboard `@fusion/engine` mock so
planning/subtask create routes return 201 (FN-8277).
- Mock `findRecentTasksBySourceParentTaskId` on github/planning route
stores.
- Quarantine `merge-reuse-task-worktree.slow.test.ts` (engine-slow load
flake, run 29663725381).
## Evidence
- Prior full green: Full Suite run **29663526777** on #2325.
- Tip red class: routes-github/planning 500 + engine-slow lease
residual.
## Test plan
- [x] routes subtask create-tasks / shared branch groups tests green
locally
- [ ] Full Suite all 4 shards + engine-slow green on main tip after
merge
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved task and subtask creation test coverage to correctly handle
parent-scoped duplicate checks.
* Updated test behavior to return reliable task creation results.
* **Tests**
* Quarantined a flaky integration test from the slow test suite to
improve test run reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Planning breakdowns now preserve their creating task as durable lineage
and reuse only siblings from that same parent. Identical wording under a
different parent creates a distinct child instead of silently linking
the wrong lineage.
The dashboard planning path now uses the same duplicate-safe creation
contract as agent tools, leaves reused canonical tasks untouched, and
exposes API-created parent links in task details.
Related: FN-8277
## Validation
- Core duplicate guard: 12 tests passed
- Engine task creation: 32 tests passed
- Dashboard planning routes: 4 focused tests passed
- Dashboard task detail provenance: 2 focused tests passed
- Core, engine, dashboard, and CLI typechecks passed
- Lint and strict changeset validation passed
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Preserved parent-task lineage for subtasks created through planning
breakdowns and API workflows.
- Improved duplicate detection so identical tasks from different parent
tasks can coexist safely.
- Added parent-task links to API-created task provenance details.
- Reused existing duplicates only within the same parent-task context.
- **Bug Fixes**
- Prevented duplicate handling from incorrectly archiving or skipping
tasks belonging to other parents.
- Improved dependency handling when creating planned subtasks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Moving an active task back to Todo could update the board before its
agent and subprocesses had stopped, leaving a Todo card that was still
processing. User-initiated in-progress-to-Todo moves now wait for every
executor cancellation surface before the new column is persisted or
returned to the dashboard. Cancellation is fail-closed and bounded: a
wedged shutdown leaves the task in Progress, releases its lock for
recovery, and fences late cleanup from replacement execution
generations. Engine-driven recovery moves and other transitions retain
their existing behavior.
## Validation
- Confirmed with PostgreSQL-backed symptom tests that the durable row
stays in Progress while cancellation is pending and that a timeout
releases the task lock without publishing Todo.
- Verified multi-executor ownership and replacement-generation fencing
across focused core and engine tests: 18 tests passed.
- Core build, engine typecheck, targeted lint, and strict changeset
validation passed.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
## Summary
- FN-8277 parent-scoped uniqueness: mock
`findRecentTasksBySourceParentTaskId` in heartbeat/triage/split suites.
- FN-8326: index `reportRoadmapDedup` in Settings search.
- Quarantine re-flaked `dev-server-process` under full-suite API load
(run 29661202279).
## Test plan
- [x] Targeted createTask / search-index tests green locally
- [ ] Full Suite all 4 shards green on main after merge
## Summary
Repeated review and executor sessions could replay a follow-up creation
step and produce another live task whenever the wording changed. In the
incident behind this fix, 21 creation calls for three intended
follow-ups left 18 duplicate tasks.
Agent-created tasks now retain their parent and agent provenance across
step sessions, heartbeats, and the published CLI surface. Same-parent
paraphrases converge on the existing task through a serialized pre-check
and a database-backed intent claim, while distinct sibling actions
remain separate. Candidate lookup is parent-indexed, uniqueness failures
abort creation, and canonical reuse no longer emits misleading creation
audit events or workflow claims.
Related: FN-8277
## Validation
- Core duplicate guard and intake: 24 tests passed
- Engine task creation and heartbeat: 154 tests passed
- CLI extension: 66 tests passed, 95 skipped
- Core, engine, and CLI typechecks passed
- Core, engine, and CLI builds passed before rebase; the rebase was
conflict-free
- Scoped lint, strict changeset validation, and diff checks passed
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Prevented retried agent steps from creating duplicate follow-up tasks.
* Improved parent-scoped deduplication for paraphrased follow-ups while
preserving distinct sibling actions.
* Preserved parent-task and agent context for created follow-ups.
* Concurrent follow-up requests are now serialized/deduplicated so
duplicates link to the existing task instead of showing as newly
created.
* Updated agent follow-up/heartbeat activity so reused follow-ups no
longer appear in run results as fresh creations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
- Heartbeat customTools inventory includes FN-8295 ideation tools (63
total).
- Non-en i18n parity for FN-8286 `reviewArtifacts` Command Center +
settings keys.
- Quarantine `TaskDetailModal.tab-persistence.test.tsx` (CI load flake;
green focused thrice).
## Test plan
- [x] heartbeat expected-tools case green
- [x] i18n-gate-coverage + parity green
- [ ] Full Suite all 4 shards green on main after merge
## Summary
- Align heartbeat `customTools` expectations with FN-8294 mission
hierarchy tools (43→58).
- Refresh `COORDINATION_EXEMPT_TOOLS` snapshot for `fn_mission_list` /
`fn_mission_show`.
- Backfill `commandCenter.portability.*` for non-en locales and map
`reportMode` / `reportModeByAction` / `embeddedPostgresMaxConnections`
into settings default-description inventory with i18n help text.
- Realign FN-8064 skip-narration unit test with store-owned proactive
chat (no tool-side `appendAgentLog`).
- Quarantine load-sensitive `async-quality-store.pg.test.ts` (5s timeout
+ leftover psql under full-suite shard load; run 29657633544).
## Test plan
- [x] `pnpm --filter @fusion/engine exec vitest run`
gating-classifications + executor-prompt + heartbeat expected-tools case
- [x] `pnpm --filter @fusion/i18n exec vitest run` i18n-gate-coverage +
parity
- [x] `pnpm --filter @fusion/dashboard exec vitest run`
settings-default-descriptions
- [ ] Full Suite all 4 shards green on main after merge
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Settings**
* Added clearer, localized help text for report modes and per-action
overrides, including inheritance behavior.
* Added advanced embedded database connection-limit settings and
validation guidance.
* **Localization**
* Expanded translations for report settings, database tuning, and
organization configuration import/export workflows across supported
languages.
* **Tests & Maintenance**
* Updated test coverage and expectations for expanded tools and
reporting behavior.
* Quarantined a flaky database-related test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
After #2289, Full Suite shard 4 still failed on the **OMP** twin of the
Grok process-lifecycle stress test (`import("../index.js")` × 15 under
shard transform load → 5s timeout).
Apply the same fix class as grok-runtime:
- Symbol.for exit reaper on `process-manager`
- Stress test reimports that module
- 15s timeout for cold transform
## Test plan
- [x] Local OMP process-lifecycle green
- [ ] PR gate
- [ ] Post-merge Full Suite
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Improved cleanup of OMP ACP processes when the application exits.
- Prevented duplicate exit handlers and excess listener growth during
runtime reloads.
- Preserved reliable process lifecycle behavior under repeated module
loading.
- **Tests**
- Added lifecycle coverage for repeated process-manager reloads.
- Optimized the stress test to complete more efficiently while retaining
cleanup assertions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Follow-up to #2275 review feedback: `getTask` mocks for DUPLICATE
recovery tests now return a default current-task fixture for
non-canonical IDs instead of `undefined`.
## Context
CodeRabbit on #2275 noted that returning `undefined` for every id except
the canonical task could break recovery if the subject task is
re-fetched mid-path.
## Test plan
- [x] `triage-split-into-subtasks-delete.test.ts`
- [x] `triage-finalize-duplicate-lineage.test.ts`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Tests**
* Improved duplicate-resolution test coverage by preserving task data
during recovery and re-fetch scenarios.
* Added validation for non-canonical task handling to prevent errors
when duplicate tasks are resolved or split.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Restacks onto latest main after #2285 and clears the remaining Full
Suite red classes from run
[29633869887](https://github.com/Runfusion/Fusion/actions/runs/29633869887)
(post-#2285):
- **Shard 1:** `agent-skills-flow` vitest TDZ — hoist `mockFiles` via
`vi.hoisted` (same class as skill-resolver in #2285)
- **Shard 2/3:** incomplete mocks after product drift
- `isFullScreenSheetViewport` / `isShortViewport` on viewport mocks
(without overriding dynamic mobile helpers)
- `fetchCodebaseMetrics` on Command Center `api/legacy` mocks
- `fetchSettings` on `agent-modals-mobile` api mock
- **Shard 3:** PlanningMode `ui-interactions` race — sync-settle
`fetchGlobalSettings` (FN-8245 pattern from planning-flow)
- **Shard 3:** settings search drift guard — inventory
`SettingsFieldRow` `htmlFor` keys (`mobileNavPrimaryItems`)
- **Shard 2:** FloatingWindow shared-stack product bug — only reclaim
z-index on hidden→visible (not every mount effect), so last-mounted
utility stays on top
- **Shard 4:** grok process-lifecycle timeout under shard load — prove
bound with 5 reimports instead of 15
## Test plan
- [x] `agent-skills-flow.test.ts` green
- [x] `process-lifecycle.test.ts` green
- [x] FileBrowserModal, FloatingWindowStack.cross-type,
agent-modals-mobile, settings-search-index, SystemControlsArea,
PlanningModeModal.ui-interactions + planning-flow (210 tests) green
- [ ] PR merge gate (Lint/Typecheck/Build/Gate)
- [ ] Post-merge Full Suite on `main` green
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved floating-window stacking so reopened or interacted windows
appear in the correct order.
* Restored consistent layering between floating windows and expanded
dock modals.
* **Tests**
* Updated automated coverage for viewport behavior, codebase metrics,
settings search indexing, and process lifecycle scenarios.
* Improved test reliability and consistency across responsive layouts
and modal interactions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
Main Full Suite was red again after release/desktop workflow drift,
engine mock gaps, mission landed-SHA gating, and compound-engineering PG
admin auth on GHA (`USER=runner`).
## Fixes
| Area | Failure | Fix |
|------|---------|-----|
| desktop `release-workflow` | expected old `find artifacts -type f` |
assert pruned collect + `release-files/*` |
| `step-session-executor` | missing
`resolveExecutorFallbackThinkingLevel` | mock export |
| tool-availability tests | empty tools (cascade from above) | fixed by
mock |
| `skill-resolver` | TDZ on `mockFiles` during import | `vi.hoisted`
filesystem state |
| `merge-error-recovery` | enqueue no-op when not started | set
`started=true` |
| mission behavioral posture | `blocked` (no landed SHA / git probe) |
`mergeDetails.commitSha` + staleness stub |
| GraphTaskNode | missing `useOptionalToast` | mock both toast exports |
| CE `pipeline-store.pg` | psql as `runner` | admin via
`FUSION_PG_TEST_URL_BASE` |
## Test plan
- [x] step-session-executor, skill-resolver, merge-error-recovery,
mission-validator-behavioral-posture (203)
- [x] release-workflow (10)
- [x] `pnpm --filter @fusion/engine test:core` (294)
- [ ] Full Suite (non-blocking) after merge
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Improved desktop release artifact collection by pruning nested
`runtime` and `migrations` directories and consistently staging release
uploads via a dedicated `release-files` mapping.
- **Tests**
- Enhanced engine merge error-recovery coverage and mission validator
behavioral posture setup.
- Improved test reliability by synchronizing mocked filesystem state,
executor fallbacks, and toast hook variants.
- Updated Postgres test harness/admin commands to use a configurable
base URL; refined related Windows changeset description.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Summary
In a centrally-installed, multi-project Fusion server (one process
serving several repos, `process.cwd()` = the install dir, not any repo),
every task under `mergeStrategy: "pull-request"` fails at the auto-merge
stage with:
```
Could not determine repository. Specify owner/repo in params or run from a git repository with a GitHub remote.
```
PR creation from the dashboard and status polling work; only the
engine's automatic PR path fails. This is the **non-workspace sibling of
#1924** (FN-7610 routed workspace-mode tasks to direct merge but does
not cover regular multi-project tasks) and the completion of
#1797/FN-7133 (which fixed only the `getPrMergeStatus` arguments).
## Root cause
`GitHubClient.resolveRepo()` (`packages/dashboard/src/github.ts`) falls
back to a cwd-less `getCurrentRepo()` — i.e. `git remote get-url origin`
in `process.cwd()` — whenever a PR method is called without explicit
`owner`/`repo`. The engine merge path already resolves the correct repo
from the per-project cwd (`prRepo = getCurrentRepo(cwd)`, FN-7133) but
only threaded it into `getPrMergeStatus`. Every other GitHub call
omitted it:
- `processPullRequestMergeTask`: `findPrForBranch` / `createPr` /
`mergePr` on both the per-task and shared-branch-group paths
- `createGroupPrCallback` (group-PR promotion): `findPrForBranch` /
`createPr`
- `createPrNodeGithubOps` (`pr-create`/`pr-merge` workflow nodes):
cwd-less `getCurrentRepo()` persisted `entity.repo` as `""` (poisoning
the downstream `splitRepoSlug` consumers), and the git
push/`createPr`/`mergePr` ran against `process.cwd()`
- the engine's review-response run (`buildRespondCallback`):
`respondOps.getCwd` collapses to `process.cwd()` because no CLI
composition site wires `getTaskWorktree`, so its git ops and response
agent ran outside the project repo
In a central install the fallback throws; worse, if `process.cwd()`
happens to be inside some *other* git repo, it silently targets the
**wrong repository**.
## What changed
- `fix(pr-merge): thread repo identity into PR auto-merge GitHub calls`
— widens the CLI-local `GitHubOperations` interface (optional
`owner`/`repo`, already accepted by `GitHubClient`'s
`FindPrParams`/`CreatePrParams`/`MergePrParams`) and passes `prRepo` at
all six call sites in `processPullRequestMergeTask`.
- `fix(pr-merge): resolve group-PR repo from project cwd in
createGroupPrCallback` — resolves via `getCurrentRepo(cwd)` from the
callback input (same T4 pattern as `syncGroupPrCallback`) with a loud
failure instead of a silent wrong-repo fallback.
- `fix(pr-merge): resolve PR-node repo from task worktree instead of
process cwd` — `resolvePrSource` resolves from `task.worktree`, git ops
run in `getTaskWorktree(...) ?? task.worktree ?? process.cwd()`, and
`createPr`/`mergePr` pass `owner`/`repo` parsed from `entity.repo`.
- `fix(pr-merge): resolve review-response run cwd from the task
worktree` — the engine owns the store, so `buildRespondCallback` prefers
the task's recorded `worktree` for the response run's git ops + agent,
keeping `ops.getCwd` as the single-project fallback (defensive against
structural `PrNodeStore`s without `getTask`).
- Changeset (`@runfusion/fusion` patch, structured body) included.
Deliberately **not** done: a constructor-scoped default repo on
`GitHubClient` — one client instance is shared across all projects in a
central install (`serve.ts`/`daemon.ts`/`dashboard.ts`), so per-call
`owner`/`repo` is the only correct scope.
## Testing
- New regression tests simulate the central-install topology
(`getCurrentRepo` mocked as `(cwd?) => cwd ? repo : null`, exactly the
failing environment) and drive the merge flow end-to-end on the per-task
path, the shared-branch-group path, `createGroupPrCallback`, and all
three `createPrNodeGithubOps` ops, asserting every GitHub call carries
explicit `owner`/`repo` (45 tests in
`packages/cli/src/commands/__tests__/task-lifecycle.test.ts`, all
green).
- `packages/engine/src/__tests__/pr-respond-cwd-resolution.test.ts`
covers the respond-run cwd: worktree preferred, `ops.getCwd` fallback
when the task has no worktree, when the lookup fails, and when a
structural store has no `getTask`.
- Existing exact-argument assertions were extended to the new call
contract (no assertions weakened or removed).
- `pnpm lint`, `pnpm typecheck`, and `pnpm build` green locally; `pnpm
test:gate`'s engine-core suite green (294/294) — its PostgreSQL-backend
lane needs local PG credentials this environment lacks, so that lane
defers to CI. `pnpm verify:fast` (scoped typecheck/build + CLI build +
boot smoke) also passes.
## Repro
1. Install the CLI centrally; run the server from a dir that is not a
git repo, serving ≥1 project with a GitHub `origin` and `mergeStrategy:
"pull-request"`.
2. Run a task to completion and let it reach the merge stage.
3. Before this fix: the auto-merger throws `Could not determine
repository …` (tasks with a persisted PR poll fine but never merge).
Merging the same task from the Pull Requests tab succeeds, because the
dashboard route resolves the repo explicitly (`parseBadgeUrl(...) ??
getCurrentRepo(rootDir)`).
Full analysis: https://github.com/Tchori-Labs/Fusion/issues/4
---
Developed with Claude (co-authored on all commits).
https://claude.ai/code/session_01ChEa8SHFYNAzjCdFbwFMfh
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Resolved pull request auto-merge failures in centrally installed,
multi-project deployments.
- Ensured explicit repository context (`owner/repo`) is used for pull
request lookup, creation, and merging throughout the merge workflow.
- Improved pull request response handling to prefer the task worktree
for working-directory resolution, with safe error behavior when task
details are unavailable.
- **Tests**
- Expanded coverage for multi-repository merge workflows and
worktree-based repository/cwd resolution in PR response handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
Make triage duplicate-finalization tests provide the activity recorder used by the implementation.
- Add an awaited recordActivity mock to the shared TaskStore fixture.
- Select delete resolution in the reviewer-outage retry scenario so it reaches deleteTask.
Files changed:
packages/engine/src/__tests__/triage.test.ts | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
Fusion-Task-Id: FN-8242
Fusion-Task-Lineage: 8f9fd4b6-8071-47f3-8091-747884762ec9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
## Summary
An audit of the SQLite→PostgreSQL store migration found data-store paths
still reaching the removed SQLite stub in backend (PG) mode. In backend
mode `store.db`/`getDatabase()` throw the removed-SQLite error, so each
of these either threw on every run or — worse — had the throw swallowed
into a silent wrong result. This PR routes all of them through the
`AsyncDataLayer` (and removes one dead primitive).
## The 6 live bugs fixed
| Fix | Was |
|-----|-----|
| `executor.ts` authoritative assigned-agent fallback now inherits the
TaskStore `asyncLayer` | silently returned `null` → model drift to the
pi built-in (the exact thing its comment guards) |
| `pruneAgentLogFilesAsync` replaces the sync self-healing prune call |
threw `SQLite Database is not available` every maintenance sweep →
agent-log pruning never ran |
| `cleanupOrphanedMaterializedSteps` deletes PG `workflow_steps` rows on
a failed create | swallowed the throw → leaked rows |
| `deleteTaskBackendImpl` now runs the async mission feature/task-link
unlink | PG hard delete left orphaned mission links |
| `getWorkflowSettingsProjectId` returns `rootDir` in backend mode
without touching the stub | swallowed throw for unscoped backend stores
|
| `fn plugin` unregistered-project fallback bootstraps a `CentralCore`
`AsyncDataLayer` | layerless `PluginStore` threw in PG |
## The 4 latent traps, fixed properly
- **`cleanupArchivedTasks`** — real async port (enumerate archived
soft-deleted rows, guarantee cold snapshot, hard-delete project row +
purge selection rows + rm dir).
- **`deleteWorkflowStep`** — real async port (delete `workflow_steps`
via the layer with `.returning()` to preserve the not-found contract).
- **`applyTaskPatch`** — **removed** (zero-caller SQLite column-patch
primitive with no backend analogue; impl + facade + import deleted).
- **`AgentStore.importLegacyFileRuns`** — clean backend no-op (no legacy
SQLite run-files exist in a PG deployment; its only `init()` caller
early-returns in backend mode).
## Symptom Verification
New PG regression suite
`packages/core/src/__tests__/postgres/store-sqlite-residue-fixes.pg.test.ts`
reproduces the original failures against real embedded Postgres and
asserts they're gone:
- orphaned `workflow_steps` are actually deleted (no swallowed throw)
- `pruneAgentLogFilesAsync` resolves and prunes inactive-task log files
- hard delete unlinks the mission feature from the task
- `deleteWorkflowStep` removes the row / reports not-found
- `cleanupArchivedTasks` hard-deletes the project row while retaining
the cold snapshot
## Verification
- `@fusion/core`, `@fusion/engine`, `@runfusion/fusion` typecheck clean
- ~50 existing + 5 new PG tests pass; lint clean; changeset validates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Prevented PostgreSQL backend maintenance from hitting removed legacy
SQLite code paths, avoiding datastore failures and residue cleanup
issues.
* Fixed workflow-step deletion and “not found” behavior in backend mode.
* Ensured backend hard-deletes correctly unlink related mission
feature/task links and clean orphaned materialized steps.
* Prevented legacy file-run imports from incorrectly reporting success
in backend mode.
* **New Features**
* Added async agent-log pruning for inactive tasks and updated
maintenance to use it.
* **Tests**
* Added PostgreSQL regression coverage for residue fixes and
archive/workflow cleanup.
* **Refactor**
* Removed an unused task patch operation and updated task-store cleanup
methods to be async where needed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Problem
Permanent (durable) agent heartbeats went silent while the rest of the
engine kept running. Investigation of the live DB showed **every**
permanent agent's `heartbeatTimerRepair` metadata carrying
`nonAdvancingEscalated: true` with **3–63 consecutive** zombie re-arms —
the audit re-arming a timer every 60s for hours while emitting
`heartbeat-rearm-nonadvancing-escalated` warnings that never recovered
anything.
## Root cause
The heartbeat trigger audit classified a "zombie" (dead) timer **solely
from a stale `lastHeartbeatAt`**. But that column advances *only* on a
successful `"ok"` delivery (`agent-store.ts` `recordHeartbeat`). It
stays frozen whenever a heartbeat is intentionally skipped or no-op'd:
- agent over budget / over budget threshold
- `globalPause` / `enginePaused`
- `skipHeartbeatWhenIdle` on an idle agent
- idle "org" agents whose runs complete as `no_assignment_identity_run`
In all of these the interval keeps firing perfectly — the timer is
alive, delivery is just (correctly) skipped. Keying zombie detection off
`lastHeartbeatAt` misread those healthy timers as dead, re-armed them
every 60s, and escalated forever. Re-arming a live timer is a no-op, so
the loop could never recover — it only produced churn and phantom
warnings.
## Fix (the invariant)
Key zombie detection off **whether the interval physically fired**, not
whether delivery advanced.
- New `lastTimerFireAtMs` map, stamped at the top of `onTimerTick`
**before any gate** — a fired-but-skipped tick still counts as proof of
liveness.
- In the audit: a present + stale timer that fired within its stale
window is **left untouched** (no re-arm, no escalation, non-advancing
counter reset). Only a timer with **no recent fire** (a genuinely dead
interval) falls through to the existing re-arm/escalation path.
- Map cleaned up in `unregisterAgent()` / `stop()`.
This preserves the FN-7645 zombie repair (a timer that stops firing goes
stale in lockstep on both clocks and is still re-armed) and the FN-7939
watchdog, while eliminating the phantom churn for live-but-skipping
timers.
Why not "force a heartbeat" or "park the agent": forcing delivery would
bypass budget/pause governance, and parking a healthy idle agent would
be wrong. The correct action for a live-but-skipping timer is to leave
it alone — its next real tick delivers once the skip condition clears.
## Tests
- Rewrote the old `skipHeartbeatWhenIdle` test that codified the buggy
escalation → now asserts a **live** idle-skipping timer is left
untouched (no zombie re-arm, no escalation).
- Added a budget/no-assignment surface: a live timer that dispatches but
leaves `lastHeartbeatAt` frozen must not be misclassified.
`heartbeat-scheduler.test.ts` 120/120; broader heartbeat + concurrency
suites 349/349; `@fusion/engine` typecheck 0 errors.
## Review
Self-reviewed at medium effort. Two acknowledged, bounded trade-offs
(kept intentionally): a genuinely-dead-but-recently-fired timer's repair
latency is bounded at ~2× interval (same as the original FN-7645
latency), and the escalation warning is suppressed for live timers (it
only ever fired because of the churn this removes; per-tick error logs +
a new "left live-but-skipping timer" log retain visibility). One trivial
cleanup applied (single `Date.now()` sample).
## Notes
- Engine is a private package → no changeset.
- Complementary to a separate in-flight fix for the
agentStore/scheduler-not-constructed bug (why heartbeats stopped
*entirely*); this PR ensures that once the scheduler runs again, the
audit stops the phantom churn/escalation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved heartbeat timer monitoring to distinguish healthy timers from
genuinely stopped timers.
* Prevented unnecessary timer re-registration and warning escalation
when heartbeats are intentionally skipped due to idle, paused,
budget-limited, or unassigned states.
* Improved recovery when a replacement timer stops firing, ensuring it
is detected and repaired reliably.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>