Files
fusion/docs/design/fn-4824-cross-node-assignment-wake-contract.md
Fusion (runfusion.ai) 39a87a8925 docs(FN-4824): land cross-node assignment-wake contract
Fusion-Task-Id: FN-4824
Fusion-Task-Lineage: 65f53a33-0939-4b6f-b62b-4a255ad14d9e
2026-05-16 21:53:18 -07:00

108 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FN-4824 — Cross-node assignment-wake propagation contract
## 1) Scope & Relationship to FN-4819
This contract closes the FN-4819 §3 operational-handoff gap for assignment-driven wakes across nodes by defining push, fallback, and missed-wake recovery behavior for owner and peer runtimes. It does not change FN-4819 §2 distributed checkout mutex work (FN-4822), and it does not cover FN-4819 §5 non-goals: scheduler failover, live-process migration, multi-master writes, or automatic node promotion.
## 2) Definitions
- **owner node**: node that currently owns execution responsibility for an agent/task assignment in practice (the node that must wake the assigned agent).
- **peer node**: another node in the mesh that can observe/forward assignment signals but is not the wake target for that assignment.
- **assignment event**: cross-node event representing an assignment write.
- **wake**: a heartbeat trigger equivalent in intent to local `agent:assigned` handling.
- **push path**: delivery via remote event stream (`/api/events/stream`) consumed by `RemoteNodeClient.streamEvents`.
- **fallback path**: bounded pull/poll while push transport is degraded but still reachable.
- **missed wake**: assignment occurred but no wake fired before stream failure window ended.
- **eventual wake bound**: maximum allowed time from restoration/recovery condition to wake emission.
### Event payload contract
```ts
export interface TaskAssignedEventPayload {
taskId: string;
agentId: string;
fromNodeId: string;
toNodeId?: string;
leaseEpoch?: number;
assignedAt: string; // ISO-8601
}
```
## 3) Push Path Contract (healthy transport)
1. The originating node **MUST** emit `task:assigned` on the same event surface already used for `task:created`, `task:moved`, and `task:updated` (`/api/events/stream`).
2. Event name is **`task:assigned`**. Payload shape is `TaskAssignedEventPayload`.
3. The receiving remote runtime **MUST** forward this event through `RemoteNodeRuntime.forwardRemoteEvent` by re-emitting `task:assigned` for owner-side listeners.
4. Owner-side wake listener **MUST** treat forwarded `task:assigned` as semantically equivalent to local `agent:assigned` wake intent.
5. Under healthy transport, p95 assignment-write → wake latency target is **<= 2 seconds**.
## 4) Fallback Path Contract (transport degraded but reachable)
1. While SSE is disconnected and reconnecting inside `runEventStreamLoop` backoff window (`reconnectBaseDelayMs`..`maxReconnectDelayMs`, bounded by `maxReconnectAttempts`), assignment wake delivery **MUST NOT** rely solely on SSE.
2. Runtime **MUST** invoke a fallback pull seam during disconnected windows: `RemoteNodeClient.pollPendingAssignments({ since })`.
3. Poll response **MUST** include assignment rows newer than a cursor (`assignedAt`/cursor semantics).
4. Owner-side listener **MUST** trigger wake when poll returns assignment newer than the last observed `(taskId, agentId, assignedAt)` tuple.
5. Fallback bound: wake **MUST** occur within **<= 2 × reconnectBaseDelayMs** (default **<= 10 s**) even if zero SSE events succeed in that interval.
6. Idempotency requirement: replay of already-observed assignment (`same taskId + same assignedAt`) **MUST NOT** emit a duplicate wake.
## 5) Missed-Wake Recovery Contract (transport unavailable, then restored)
1. If stream errors past `maxReconnectAttempts` and runtime transitions to `errored`, assignment writes during outage are considered potential missed wakes.
2. On next successful transport restoration/health re-entry, owner-side scheduler/runtime **MUST** run one-shot reconciliation:
- enumerate locally-owned agents,
- detect `assignedTaskId` changes vs last observed snapshot,
- emit exactly one wake per recovered assignment.
3. Eventual wake bound after restoration is **<= next heartbeat tick + reconciliation pass**, hard ceiling **<= 60 s** under default heartbeat settings.
4. Reconciliation **MUST** emit audit event `wake:cross-node-reconcile` with `{ ownerNodeId, peerNodeId, agentIds, recoveredAssignments }`.
## 6) Telemetry & Audit Requirements
For every cross-node wake trigger, telemetry **MUST** include source discriminator:
- `source="cross-node-push"`
- `source="cross-node-poll"`
- `source="cross-node-reconcile"`
Required diagnostics:
- `[wake-trigger-diagnostics]` log entry includes source discriminator and `taskId`.
Required run-audit event names:
- `wake:cross-node-push`
- `wake:cross-node-poll`
- `wake:cross-node-reconcile`
## 7) Non-Goals
- No distributed mutex/schema definition changes (FN-4822).
- No changes to local single-node `agent:assigned` semantics.
- No scheduler failover or live-process state migration.
- No settings consensus or multi-master writes.
- No cross-node settings sync behavior changes (FN-4796).
## 8) Testability Surface
The following scenarios are required and must be executable with in-process fakes and fake timers:
1. **Healthy push**: injected `task:assigned` over stream causes one wake on owner runtime with `source: "cross-node-push"` and correct `{taskId, agentId}`.
2. **Degraded poll fallback**: stream disconnect path still wakes within <= 10 s simulated via poll fallback; no duplicate wake on later stream replay.
3. **Missed-wake reconciliation**: runtime reaches `errored`, assignment happens during outage, restoration + reconcile emits one `cross-node-reconcile` wake and audit event.
4. **No-wake invariants**: duplicate broadcast with same `(taskId, agentId, assignedAt)` does not re-wake; newer `assignedAt` does.
5. **Telemetry shape**: wake diagnostics include source discriminator and `taskId`.
## 9) References
- `docs/design/fn-4819-distributed-multi-node-coordination-gap.md:173-324` (§3 unavailable-node handoff policy and testability framing)
- `packages/engine/src/agent-heartbeat.ts:3665-3755` (`watchAssignments`, local `agent:assigned` wake hook)
- `packages/engine/src/agent-heartbeat.ts:2430-2456` (`task_assigned` reason and `[wake-trigger-diagnostics]` surface)
- `packages/engine/src/runtimes/remote-node-runtime.ts:153-230` (event stream loop, reconnect/backoff lifecycle)
- `packages/engine/src/runtimes/remote-node-runtime.ts:239-271` (`forwardRemoteEvent` switch)
- `packages/engine/src/runtimes/remote-node-client.ts:98-120` (`streamEvents` source)
- `packages/engine/src/runtimes/in-process-runtime.ts:1180-1220` (in-process event-forwarding baseline)
- `packages/dashboard/src/sse.ts:384-390,773-775` (SSE catalog currently forwarding `task:created|moved|updated`)
- `packages/dashboard/src/server.ts: /api/events, /api/events/keepalive, /api/events/disconnect` (event stream routes)
- `packages/core/src/agent-store.ts: assignTask + `agent:assigned` emission seam
- `packages/engine/src/node-health-monitor.ts` (health restoration signal seam)
- `packages/engine/src/mesh-lease-manager.ts` (`isLeaseRecoverable`/`recoverAbandonedLease` seam referenced by FN-4819 §3.3)