Files
fusion/packages/core/src/task-lane-cache.ts
gsxdsm ebe514c3e4 FN-8677: propagate task update lanes before conversion
Propagate cache-warmed workflow lanes through task updates so synchronous engine consumers support renamed boards.

- Add task lane cache and attach resolved lanes to task:updated metadata.
- Update scheduler, triage, and notification consumers to use carried lanes with bridge-safe fallbacks.
- Cover lane propagation and renamed-lane event behavior with core and engine tests.

Files changed:
 .changeset/fn-8677-manual-merge-hold-lanes.md      |   7 ++
 .changeset/task-updated-carries-lanes.md           |   7 ++
 ...orkflow-ir-readers-always-return-the-default.md |  22 +++++
 .../sync-workflow-ir-second-blocker.test.ts        |  43 +++-----
 .../core/src/__tests__/task-lane-cache.test.ts     |  30 ++++++
 .../task-updated-lanes-emit-surfaces.test.ts       |  92 ++++++++++++++++++
 .../__tests__/task-updated-lanes-payload.test.ts   |  42 ++++++++
 packages/core/src/index.ts                         |   1 +
 packages/core/src/store.ts                         |  36 ++++++-
 packages/core/src/task-lane-cache.ts               |  63 ++++++++++++
 .../core/src/task-store/archive-lifecycle-2.ts     |   3 +
 packages/core/src/task-store/moves.ts              |   1 +
 packages/core/src/task-store/task-artifacts-ops.ts |   1 +
 packages/core/src/task-store/task-update.ts        |   1 +
 packages/core/src/task-store/update-task-deps.ts   |   4 +-
 .../core/src/task-store/workflow-definitions.ts    |  71 +++++---------
 .../__tests__/scheduler-task-updated-lanes.test.ts | 108 +++++++++++++++++++++
 .../task-updated-lanes-bridge-compat.test.ts       |  94 ++++++++++++++++++
 ...task-updated-lanes-engine-emit-surfaces.test.ts | 101 +++++++++++++++++++
 .../src/__tests__/triage-pause-abort.test.ts       |  22 +++++
 .../src/__tests__/triage-planning-wake.test.ts     |  25 +++++
 .../notification-renamed-lifecycle-columns.test.ts |  84 +++++++++++++++-
 .../__tests__/task-wedge-notification.test.ts      |  19 ++++
 .../src/notification/notification-service.ts       |  56 ++++-------
 packages/engine/src/scheduler.ts                   |  62 +++---------
 packages/engine/src/triage.ts                      | 105 ++++++--------------
 scripts/lib/inert-sync-lane-baseline.json          |   5 +-
 27 files changed, 858 insertions(+), 247 deletions(-)

Fusion-Task-Id: FN-8677

Fusion-Task-Lineage: d8fef9db-0f88-4dfd-9813-be25e10e3588

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 01:15:10 -07:00

64 lines
2.1 KiB
TypeScript

import type { TaskMoveLanes } from "./workflow-lifecycle-traits.js";
export interface TaskLaneCacheOptions {
ttlMs?: number;
maxSize?: number;
now?: () => number;
}
interface TaskLaneCacheEntry {
lanes: TaskMoveLanes | undefined;
workflowId?: string;
at: number;
}
/*
FNXC:WorkflowEvents 2026-08-01-06:11:
PostgreSQL deployments can rewrite a task workflow selection from another node, so an in-process lane
answer must expire as well as being invalidated by local selection writes. Returning an old answer
would make a synchronous listener confidently choose the wrong lane; a cache miss is deliberately
"unknown" and lets that listener retain its existing fallback.
The clock is injectable so TTL coverage advances a deterministic fake clock instead of sleeping. The
cache never resolves an IR: update-event decoration must remain a synchronous Map lookup on hot paths.
*/
export class TaskLaneCache {
private readonly entries = new Map<string, TaskLaneCacheEntry>();
private readonly ttlMs: number;
private readonly maxSize: number;
private readonly now: () => number;
constructor({ ttlMs = 30_000, maxSize = 1_000, now = Date.now }: TaskLaneCacheOptions = {}) {
this.ttlMs = ttlMs;
this.maxSize = maxSize;
this.now = now;
}
set(taskId: string, lanes: TaskMoveLanes | undefined, options: { workflowId?: string } = {}): void {
this.entries.delete(taskId);
this.entries.set(taskId, { lanes, workflowId: options.workflowId, at: this.now() });
while (this.entries.size > this.maxSize) this.entries.delete(this.entries.keys().next().value!);
}
get(taskId: string): TaskMoveLanes | undefined {
const entry = this.entries.get(taskId);
if (!entry) return undefined;
if (this.now() - entry.at >= this.ttlMs) {
this.entries.delete(taskId);
return undefined;
}
// Refresh insertion order without changing the recorded cache time.
this.entries.delete(taskId);
this.entries.set(taskId, entry);
return entry.lanes;
}
invalidate(taskId: string): void {
this.entries.delete(taskId);
}
clear(): void {
this.entries.clear();
}
}