feat(FN-2931): merge fusion/fn-2931

- **New node routing policy engine** (`packages/engine/src/node-routing-policy.ts`, `scheduler.ts`): introduces configurable routing rules for assigning tasks to specific nodes, with a new test suite covering policy matching, node priority, and capacity constraints
- **Agent run route decoupling** (`register-agent-runtime-routes.ts`): routes now respond before heartbeat side effects complete, improving latency and reducing 503s under load; tests updated to handle async state transitions
- **Scheduler node routing** (`scheduler-node-routing.test.ts`): 131-line test expansion covering task-to-node assignment with routing policies
- **Dashboard UI polish** (AgentsView, QuickChatFAB, SettingsModal): CSS refinements and component adjustments from FN-2903
- **Reviewer and triage refinements** (`reviewer.ts`, `triage.ts`): logic improvements for async execution paths
- **Remote access adapter** (`provider-adapters.ts`): extended from FN-2950
- **Settings memory routes** (`register-settings-memory-routes.ts`): from FN-2925
- **Docs** (`architecture.md`, `settings-reference.md`): updated for new routing policy and settings features

Commits merged:
- test(FN-2931): complete Step 2 — update async state route assertions
- feat(FN-2931): complete Step 1 — respond before heartbeat side effects
- feat(FN-2925): merge fusion/fn-2925
- feat(FN-2903): merge fusion/fn-2903
- feat(FN-2950): merge fusion/fn-2950

Files changed:
docs/architecture.md                               |  19 +--
 docs/settings-reference.md                         |   2 +-
 packages/dashboard/app/components/AgentsView.css   |  20 +++-
 packages/dashboard/app/components/AgentsView.tsx   |  11 +-
 packages/dashboard/app/components/QuickChatFAB.css |  32 ++++-
 .../dashboard/app/components/SettingsModal.css     |  21 ++++
 .../dashboard/app/components/SettingsModal.tsx     |  58 ++++++++-
 .../app/components/__tests__/AgentsView.test.tsx   |  37 ++++++
 .../app/components/__tests__/QuickChatFAB.test.tsx |  18 +--
 .../components/__tests__/SettingsModal.test.tsx    |   2 +-
 packages/dashboard/package.json                    |   2 +
 .../src/__tests__/routes-agent-runs.test.ts        |  13 +-
 .../src/routes/register-agent-runtime-routes.ts    |  49 ++++----
 .../src/routes/register-settings-memory-routes.ts  |   8 +-
 .../src/__tests__/node-routing-policy.test.ts      |  78 ++++++++++++
 packages/engine/src/__tests__/reviewer.test.ts     |  19 ++-
 .../src/__tests__/scheduler-node-routing.test.ts   | 131 ++++++++++++++++++++-
 packages/engine/src/__tests__/triage.test.ts       |  16 +--
 packages/engine/src/node-routing-policy.ts         |  45 +++++++
 .../engine/src/remote-access/provider-adapters.ts  |  14 ++-
 packages/engine/src/reviewer.ts                    |  33 ++++--
 packages/engine/src/scheduler.ts                   |  44 ++++++-
 packages/engine/src/triage.ts                      |  48 ++++----
 pnpm-lock.yaml                                     | 113 ++++++++++++++++++
 24 files changed, 721 insertions(+), 112 deletions(-)

Fusion-Task-Id: FN-2931
This commit is contained in:
Fusion
2026-04-29 12:55:00 -07:00
committed by gsxdsm
parent 87d9c1c380
commit 72ae0b46bf
2 changed files with 38 additions and 26 deletions

View File

@@ -416,29 +416,38 @@ export function registerAgentRuntimeRoutes(ctx: ApiRoutesContext, deps: AgentRun
? heartbeatMonitor
: null;
if (nextState === "paused" && projectHeartbeatMonitor) {
const activeRun = await agentStore.getActiveHeartbeatRun(agentId);
if (activeRun) {
await projectHeartbeatMonitor.stopRun(agentId);
}
}
const updatedAgent = await agentStore.updateAgentState(agentId, nextState);
if (nextState === "active" && projectHeartbeatMonitor) {
await projectHeartbeatMonitor.executeHeartbeat({
agentId,
source: "on_demand",
triggerDetail: "Triggered from state resume",
contextSnapshot: {
wakeReason: "on_demand",
triggerDetail: "Triggered from state resume",
triggerSource: "state-resume",
},
});
}
res.json(updatedAgent);
void (async () => {
try {
if (nextState === "paused" && projectHeartbeatMonitor) {
const activeRun = await agentStore.getActiveHeartbeatRun(agentId);
if (activeRun) {
await projectHeartbeatMonitor.stopRun(agentId);
const agentAfterStop = await agentStore.getAgent(agentId);
if (agentAfterStop && agentAfterStop.state !== "paused") {
await agentStore.updateAgentState(agentId, "paused");
}
}
}
if (nextState === "active" && projectHeartbeatMonitor) {
await projectHeartbeatMonitor.executeHeartbeat({
agentId,
source: "on_demand",
triggerDetail: "Triggered from state resume",
contextSnapshot: {
wakeReason: "on_demand",
triggerDetail: "Triggered from state resume",
triggerSource: "state-resume",
},
});
}
} catch (err) {
console.error(`[agent-state] async heartbeat work failed for ${agentId}:`, err);
}
})();
} catch (err: unknown) {
if (err instanceof ApiError) {
throw err;