docs(FN-4824): land cross-node assignment-wake contract
Fusion-Task-Id: FN-4824 Fusion-Task-Lineage: 65f53a33-0939-4b6f-b62b-4a255ad14d9e
This commit is contained in:
committed by
gsxdsm
parent
d724008af8
commit
39a87a8925
107
docs/design/fn-4824-cross-node-assignment-wake-contract.md
Normal file
107
docs/design/fn-4824-cross-node-assignment-wake-contract.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user