docs(engine): flag triage's evacuation guard — it looks two-thirds converted and is fully literal (#3108)
Completes the sync-listener audit across the three files holding the remaining blocked guards — `executor.ts` (#3104), `scheduler.ts` (#3100), and this one. Triage is the most misleading of the three. ## The shape lies The guard reads as **two resolved arms and one literal**: > `task.column === disposeLanes.hold || task.column === disposeLanes.intake || task.column === "in-progress"` So the obvious next move is to convert the third arm with the same helper. That is wrong twice: **1. The two "resolved" arms are not resolved.** `resolvePlannerLanes` goes through `resolveTaskWorkflowIrSync`, which cannot answer for a **custom** workflow — the sync selection reader returns `undefined` unconditionally, *and* the custom-workflow IR read goes through `store.db`, whose implementation is an unconditional throw (#3103). So `disposeLanes.hold` / `.intake` are `todo` / `triage` on every board. **All three arms are literal in effect.** Converting the third the same way adds a third inert comparison and retires a census entry that is currently telling the truth. **2. The guard's answer is consumed synchronously** — the criterion I had to correct in #3104. Below it, `pauseAborted.add`, `session.dispose()` and `activeSessions.delete` mutate in-memory state in this tick, and other paths read those maps. Contrast `self-healing.ts`'s fan-out, where three of four guards only gated work the listener already `void`s and so *were* convertible via the async resolver (#3094). ## What it costs, and the obvious reading is backwards An evacuation **into** a renamed destination still falls through and disposes correctly — no bug there. The failure is the other direction: on a board whose **hold or intake** lane is renamed, arms 1 and 2 stop matching, so a card **sitting still in its own planning lane** is treated as evacuated and its live triage session is aborted mid-run. I state it that way because "renamed board → guard misses → nothing happens" is the pattern everywhere else in this program, and here it inverts. ## Census **Unchanged at 1**, deliberately. Blocked, and now documented as *fully literal* rather than part-converted — which is the fact a future pass needs in order not to make it worse. ## Measured - Comment-only. - `src/__tests__/triage*` — **25 files / 374 tests pass**. - `tsc --noEmit -p packages/engine` clean; `check-fnxc-future-dates` clean. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -720,6 +720,33 @@ export class TriageProcessor {
|
||||
an unrelated update.
|
||||
*/
|
||||
if (typeof task.column !== "string") return;
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-23:55 (FLAGGED AND LEFT COUNTED — and the shape here is
|
||||
misleading, which is why it needs saying):
|
||||
|
||||
This guard LOOKS two-thirds converted — two resolved arms and one literal — so the obvious next
|
||||
move is to convert the third with the same helper. That would be wrong twice over.
|
||||
|
||||
1. `resolvePlannerLanes` goes through `resolveTaskWorkflowIrSync`, which cannot answer for a
|
||||
CUSTOM workflow: the sync selection reader returns `undefined` unconditionally, AND the
|
||||
custom-workflow IR read goes through `store.db`, whose implementation is an unconditional
|
||||
throw (`sync-workflow-ir-second-blocker.test.ts`). So `disposeLanes.hold`/`.intake` are
|
||||
`todo`/`triage` on every board. All THREE arms are literal in effect; converting the third
|
||||
via the same helper adds a third inert comparison and removes a census entry that is telling
|
||||
the truth.
|
||||
|
||||
2. The guard's answer is consumed SYNCHRONOUSLY — that is the criterion, not "the listener is
|
||||
sync". Below it, `pauseAborted.add`, `session.dispose()` and `activeSessions.delete` all
|
||||
mutate in-memory state in this tick, and other paths read those maps. (Contrast
|
||||
`self-healing.ts`'s fan-out, where three of four guards only gated work the listener already
|
||||
`void`s and were therefore convertible via the async resolver.)
|
||||
|
||||
WHAT IT COSTS TODAY: a card evacuated from planning into a lane the board renamed still matches
|
||||
none of these, so the guard falls through and the session IS disposed — correct. The failure is
|
||||
the other direction: a board whose HOLD or INTAKE lane is renamed no longer matches arms 1 and
|
||||
2, so a card sitting still in its own planning lane is treated as evacuated and its live triage
|
||||
session is aborted mid-run.
|
||||
*/
|
||||
const disposeLanes = resolvePlannerLanes(this.store, task.id);
|
||||
if (task.column === disposeLanes.hold || task.column === disposeLanes.intake || task.column === "in-progress") return;
|
||||
if (this.activeSubagentSessions.has(task.id)) {
|
||||
|
||||
Reference in New Issue
Block a user