89d6d76d604d99ca2d30f08ab867cfa35a8769ca
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2e39763930 |
test(engine): prove agent-link hygiene on a renamed board — a leaked agent slot, not a stale link (#2514)
Stacked on #2510. Test-only. Closes the `task-agent-sync` ledger entry. ## The defect this reproduces, in the code's own words `task-agent-sync.ts`'s conversion note: > a move into a renamed terminal column matched nothing and this handler returned early — so the agent kept a `taskId` pointing at a finished card and stayed `running`, **with no error and no failing test**. "No error and no failing test" is the whole problem — and the cost is not a stale link. **The scheduler counts `running` agents against its cap**, so on a renamed board every completed task permanently consumes an agent slot until a human notices. A board would just get slower and slower. ## Everything in the path is real Real PostgreSQL `TaskStore`, real `AgentStore`, the real `attachAgentLinkSync` subscribed to the store's real `task:moved` event (the same call `in-process-runtime` makes), and a real `moveTask` to trigger it. Assertions read the **agent row** back out of the store — never "the handler was called". ## Mutation-verified Forcing the legacy literal sets (the pre-conversion behavior) fails **exactly the two renamed cases**, leaving the default-vocabulary floor and both negatives green. So this reproduces the original defect rather than merely covering the file. ## Negative half An ordinary mid-lifecycle move (`wip → review`) must **not** release the agent — given the same time to run as the positive case. "Clear the link whenever the card moves" would drop the binding the moment work started, a louder failure than the leak it fixes. ## Two anti-flake, anti-vacuity details - **Async delivery.** `task:moved` is a plain EventEmitter and the handler is async, so the assertions **poll the persisted row** to a bounded deadline and fail with the row's actual contents. A fixed sleep would flake in both directions. - **The fixture asserts itself.** The link and `running` state are verified *before* the move, so an agent that was never linked cannot make this pass for the wrong reason — the failure mode I hit twice already in this program. ## Verification - four live-E2E suites green together: **39/39** - engine `tsc --noEmit` clean - `pnpm test:gate` green (307 + 10 + 71) 🤖 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 agent-link cleanup when tasks reach workflow completion. * Ensured completed task links are released even when workflow columns have been renamed. * Preserved active agent links when tasks move through non-terminal workflow stages. * Improved reporting of link cleanup outcomes and handling of synchronization errors. * **Tests** * Added live PostgreSQL end-to-end coverage for completion, in-progress moves, and renamed-column scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
02b0f4f860 |
Phase B slice B2: U5 small movers — 12 literal sites converted, plus a negative result on hold-release (#2471)
Stacked on #2470 (Phase B slice B1). Base is `feature/workflow-vocabulary-conversion` — do not merge before it. ## What this is Phase B slice B2 — the U5 small movers. **12 literal sites converted, plus one negative result.** The plan estimated 36 sites. A survey found 12 genuinely-convertible ones, and separately found that the plan's headline hold-release scenario **was already fixed**. Both are reported below rather than padded into a bigger-looking diff. ## The negative result (commit 1) The plan named hold-release.ts as a target on the scenario *"release readiness must hold and release identically for a RENAMED hold column."* I wrote that test first, to prove it broken. **It is not broken.** All five assertions passed against unmodified `hold-release.ts`. U6/KTD-5 had already converted the module — `isHeldTask`, `resolveReleaseTarget`, and `dependencySatisfied` each resolve the task's IR. **`hold-release.ts` has no production change in this PR.** The tests are kept as a regression floor: the invariant rests on three independent trait resolutions any of which could be "simplified" back to a literal, and nothing else covered a renamed vocabulary end-to-end through the sweep. **I verified the tests can actually fail.** Mutating `isHeldTask` back to `task.column === "todo"` kills all five. Without that check, a green run against unmodified code is indistinguishable from a test asserting something trivially true. Two drafting notes kept in the file: the renamed ids deliberately avoid colliding with any legacy literal, and the first draft's two dependency tests used a `capacity` hold — which never consults dependencies at all, so one passed **vacuously**. Both now use a `dependency` hold. ## The 12 conversions Each was red-green: the renamed-workflow test written first and **observed failing**, then made to pass. | Site | Was | Now | |---|---|---| | `task-agent-sync` CLEAR_COLUMNS | `{done,archived,todo,triage}` | resolved complete+archived+hold+intake | | `task-agent-sync` isParkedTaskColumn | `{todo,triage}` | `parkedColumns` param (hold+intake) | | `task-agent-sync` handler branch | `to === "todo" \|\| "triage"` | resolved parked set | | `mesh-lease` parked guard | `task.column !== "todo"` | resolved rebound column | | `mesh-lease` rebound move | `moveTask(id,"todo")` | resolved rebound column | | `mesh-lease` audit decisionPath | `=== "todo" ? … : …` | same resolved column | | `mesh-lease` audit newColumn | `… : "todo"` | same resolved column | | `merger-ai` already-finalized | `=== "done" \|\| "archived"` | resolved complete+archived | | `merger-ai` ×4 rebounds | `moveTask(id,"todo")` | shared `resolveFinalizeReboundColumn` | Rebound targets all use KTD-10 `resolveReboundTarget` (hold → intake → first column), the helper `self-healing.ts:714` already uses — reused, not invented. ## Three findings worth reading **1. The mesh-lease bug was in the AUDIT, not the move.** The guard and the audit were *independent* `=== "todo"` comparisons, so `newColumn` asserted the card landed in `todo` regardless of what the move did. For a workflow with no `todo` column that produced a lease-recovery trail naming a nonexistent column — and run-audit is the only post-hoc record of a lease recovery. Now resolved once and threaded to both, so they are structurally incapable of disagreeing. **2. The merger-ai failure mode was not what I predicted.** I expected the already-finalized guard to fail open and re-merge a finished card. The red run showed it actually throws `Cannot merge FN-1: task is in 'shipped', must be in 'in-review'` — a hard error blaming the column, on a task whose real state is "already done". The thing preventing the re-merge is *itself* a literal in core's `getTaskMergeBlocker`, outside this slice. Two bugs coinciding, not a design. **3. A fourth site had to move that wasn't on the list.** `evaluateParkedAgentTaskLink` calls `isParkedTaskColumn` internally. Converting only the handler would have left the preservation branch on legacy ids after the caller resolved a renamed workflow — trading a stale-link bug for a **worse** dropped-link bug (a live agent's link cleared mid-run). ## Deliberately NOT converted Both keep their literals with the reason recorded at the site under a greppable `DELIBERATE-LITERAL` tag: - **`hold-release.ts:326` `legacyDependencySatisfied`** — the FN-5719 dual-accept half. Converting makes both halves compute the same answer, deleting the compatibility signal *and* its divergence detector while looking like a cleanup. - **`replan-target.ts` final fallback** — its value is precisely that it is *not* trait-resolved; resolving it against the workflow is the stranded-card bug it was written to fix. ⚠️ **The U12 literal ratchet does not exist in the tree yet.** The brief assumed an allowlist to add entries to; there is none. `grep -rn DELIBERATE-LITERAL packages/*/src` enumerates the sites it must admit. ## What I could NOT verify - **One of the four merger-ai rebound sites is untested.** The `landWorkspaceTask` rebound is verified by inspection and the shared resolver's unit tests only — `landWorkspaceTask` is only ever *mocked* (project-engine.test.ts), never executed. Covering it needs a multi-repo git fixture and a full land run. The **other three are genuinely exercised** by pre-existing merger-ai.test.ts (lines 676/716/777/895 assert `moveTask("FN-1","todo",…)` through a real git repo) and pass unchanged — real wiring proof for those. - **3 of the 9 task-agent-sync tests passed before the conversion too**, vacuously — the literal handler early-returned and cleared nothing. They assert nothing about the old code; they are guardrails against the conversion over-clearing. - **No renamed workflow was run against a live engine.** All evidence is unit-level. ## Call sites outside this slice — NOT converted, byte-identical They keep the legacy defaults: `scheduler.ts:1273`, `agent-heartbeat.ts:1169/3642`, `self-healing.ts:11600/11665` (all `evaluateParkedAgentTaskLink`), and `merger.ts:6585` (the sibling terminal guard). Each is its own Phase C/D surface. ## Behavior changes (not a pure refactor) For a **renamed** workflow: agent links now actually get cleared on terminal moves (they never were); lease rebounds land in the resolved hold column; finalize-blocked rebounds land in the resolved hold column and their operator-facing task-log lines name the real column; already-finalized cards short-circuit cleanly instead of throwing. For **builtin:coding** and any unresolvable workflow: byte-identical. Every new parameter defaults to the legacy set, and both merger-ai resolvers fail *soft* to legacy ids in opposite directions — the terminal guard keeps `done`/`archived` (losing it sends a finished card into the merge path), the rebound keeps `todo` (abandoning it strands the card in the merge lane with no owner). ## Verification - Merge gate **green**: 299 + 10 + 71 tests - Slice suites **green**: 100 tests across 8 files (new + all pre-existing neighbours) - Existing merger suites **green**: 82 tests across 5 files, unchanged - `tsc --noEmit` clean, `pnpm lint` clean No changeset: `@fusion/engine` is private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
fe536b2af8 |
FN-6954: reconcile stale parked task assignments
Reconcile agent/task drift when durable agents remain linked to queued tasks without live execution proof. - clear stale Agent.taskId links for parked todo/triage tasks while preserving task leases and queue state - report stale parked assignments as active/no-live-run in Reports Health Check before reconciliation completes - add scheduler and self-healing coverage for queued lease drift, overlap starvation, and audit events - document the reconciliation behavior and add a published package patch changeset Files changed: .changeset/fn-6954-agent-task-state-drift.md | 5 + docs/architecture.md | 2 + .../src/__tests__/heartbeat-executor.test.ts | 68 ++++++++++++ .../__tests__/scheduler-overlap-starvation.test.ts | 68 +++++++++++- .../self-healing-agent-link-drift.test.ts | 97 ++++++++++++++++- .../engine/src/__tests__/task-agent-sync.test.ts | 33 +++++- packages/engine/src/agent-heartbeat.ts | 121 +++++++++++++++++++-- packages/engine/src/run-audit.ts | 5 + packages/engine/src/runtimes/in-process-runtime.ts | 1 + packages/engine/src/scheduler.ts | 24 +++- packages/engine/src/self-healing.ts | 102 ++++++++++++++--- packages/engine/src/task-agent-sync.ts | 65 ++++++++++- 12 files changed, 555 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-6954 Fusion-Task-Lineage: 24b8a2eb-5a33-4539-ab64-ae2bbfc2d195 |
||
|
|
d3fa2e1a8d |
feat(FN-4296): complete Step 1 — add task moved agent link sync module
Fusion-Task-Id: FN-4296 Fusion-Task-Lineage: 9d061861-547a-4905-8fdd-4b66dd7bff97 |