feat(FN-3448): normalize AgentsView button utility classes

The merge normalizes button utility classes in AgentsView, swapping 12 lines of CSS class references for their standardized counterparts for consistency and maintainability.

Fusion-Task-Id: FN-3448
This commit is contained in:
Fusion
2026-05-05 05:28:16 -07:00
committed by gsxdsm
parent 07f8fad144
commit 2dd38a6182
8 changed files with 431 additions and 12 deletions

View File

@@ -48,6 +48,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
| [Storage](./storage.md) | Storage architecture, migration, archive system, and SQLite schema | | [Storage](./storage.md) | Storage architecture, migration, archive system, and SQLite schema |
| [Dev Server Module Audit](./dev-server-modules.md) | Analysis of parallel dashboard dev-server module families, production wiring, and consolidation guidance | | [Dev Server Module Audit](./dev-server-modules.md) | Analysis of parallel dashboard dev-server module families, production wiring, and consolidation guidance |
| [Beads and Dolt Evaluation for Fusion Node Sync](./beads-dolt-sync-evaluation.md) | Evaluation of Beads and Dolt for node sync, with a recommendation for Fusion-native sync design | | [Beads and Dolt Evaluation for Fusion Node Sync](./beads-dolt-sync-evaluation.md) | Evaluation of Beads and Dolt for node sync, with a recommendation for Fusion-native sync design |
| [Shared Mesh Replication Protocol](./shared-mesh-protocol.md) | Canonical multi-leader replication/write-coordination contract (versioning, quorum, leases/fencing, queue/replay, reconciliation, and degraded-read semantics) |
| [Contributing](./contributing.md) | Local development setup, testing, release flow, and contributor conventions | | [Contributing](./contributing.md) | Local development setup, testing, release flow, and contributor conventions |
| [Docker](./docker.md) | Container builds, deployment, and persistence configuration | | [Docker](./docker.md) | Container builds, deployment, and persistence configuration |
| [Code Signing](./CODE_SIGNING.md) | macOS and Windows code signing configuration for release binaries | | [Code Signing](./CODE_SIGNING.md) | macOS and Windows code signing configuration for release binaries |

View File

@@ -425,6 +425,9 @@ Implemented in `agent-heartbeat.ts`:
### Node/mesh runtime services ### Node/mesh runtime services
- `NodeHealthMonitor` (`node-health-monitor.ts`) — remote node liveness/metrics checks - `NodeHealthMonitor` (`node-health-monitor.ts`) — remote node liveness/metrics checks
- `PeerExchangeService` (`peer-exchange-service.ts`) — peer sync orchestration - `PeerExchangeService` (`peer-exchange-service.ts`) — peer sync orchestration
- Canonical replication/write-coordination contract: [`docs/shared-mesh-protocol.md`](./shared-mesh-protocol.md)
- Defines protocol versioning, write classes, quorum/ack semantics, lease epochs/fencing, offline queue/replay, reconciliation outcomes, restart recovery hooks, and degraded-read staleness metadata.
- Existing `/api/mesh/sync` and settings-sync payloads remain the active exchange primitives while follow-on runtime tasks implement full v1 coordinator/quorum behavior.
- Process lifecycle ownership: - Process lifecycle ownership:
- `fn serve` / `fn dashboard` start a single process-level `PeerExchangeService` and stop it during shutdown. - `fn serve` / `fn dashboard` start a single process-level `PeerExchangeService` and stop it during shutdown.
- `CentralCore.startDiscovery()` is invoked from CLI startup only after HTTP bind completes so discovery advertises the actual listening port. - `CentralCore.startDiscovery()` is invoked from CLI startup only after HTTP bind completes so discovery advertises the actual listening port.

View File

@@ -34,6 +34,7 @@ Per-project task data remains in each repos `.fusion/fusion.db`.
Peer/mesh coordination spans core + engine, with startup ownership in CLI process entrypoints: Peer/mesh coordination spans core + engine, with startup ownership in CLI process entrypoints:
- `NodeDiscovery` and `NodeConnection` in `@fusion/core` handle discovery and remote node connectivity/auth primitives. - `NodeDiscovery` and `NodeConnection` in `@fusion/core` handle discovery and remote node connectivity/auth primitives.
- `PeerExchangeService` in `@fusion/engine` coordinates node-to-node sync/exchange workflows. - `PeerExchangeService` in `@fusion/engine` coordinates node-to-node sync/exchange workflows.
- Canonical replication semantics live in [`docs/shared-mesh-protocol.md`](./shared-mesh-protocol.md). That protocol separates strongly coordinated shared state from append-only streams, queued replay classes, and node-local runtime state.
- `runServe()` and `runDashboard()` (CLI) own process-level mesh service lifecycle: - `runServe()` and `runDashboard()` (CLI) own process-level mesh service lifecycle:
- start one process-wide `PeerExchangeService` instance - start one process-wide `PeerExchangeService` instance
- call `CentralCore.startDiscovery()` only after the HTTP server is listening and the real bound port is known - call `CentralCore.startDiscovery()` only after the HTTP server is listening and the real bound port is known

View File

@@ -0,0 +1,164 @@
# Shared Mesh Replication Protocol (v1)
[← Docs index](./README.md)
This document is the canonical contract for Fusion multi-leader mesh replication.
## 1. Goals and non-goals
### Goals
- Preserve one shared durable project state across multiple nodes.
- Keep task and planning state strongly coordinated by default.
- Allow local progress during peer outages via durable queues.
- Support deterministic replay/reconciliation after recovery.
- Expose read staleness so clients can decide whether to trust last-known global state.
### Non-goals (for v1)
- Full runtime scheduler failover.
- Full live-process state migration.
- Immediate global consistency for every data class.
## 2. Terms
- **Node**: A Fusion runtime instance participating in mesh sync.
- **Coordinator**: Node currently responsible for committing a write intent.
- **Intent**: Durable write proposal before global ack quorum completes.
- **Envelope**: Wire record carrying replication metadata + payload.
- **Epoch**: Monotonic lease/fencing generation for coordinator authority.
- **Fence token**: `epoch + coordinatorNodeId + sequence` token that invalidates stale coordinators.
- **Queue entry**: Durable locally-accepted write waiting for replay.
## 3. Versioning
- Protocol id: `fusion.shared-mesh`
- Initial version: `1.0`
- All envelopes must include `{ protocol, version }`.
- Minor versions (`1.x`) are backward-compatible additive.
- Major versions (`2.0+`) may change semantics and require explicit compatibility checks.
## 4. Data-class coordination matrix
| Data class | Mode | Notes |
|---|---|---|
| Tasks (core fields, deps, steps, column transitions) | Strongly coordinated | Quorum-acked intent/commit path; replayable with fencing |
| Task metadata (priority, model overrides, docs metadata refs) | Strongly coordinated | Same write path as tasks |
| Missions/milestones/slices/features | Strongly coordinated | Ordered writes preserve hierarchy invariants |
| Agent definitions/configuration | Strongly coordinated | Durable config replicated; runtime process handles excluded |
| Agent runtime state (heartbeat ticks, local process internals, worktree paths) | Node-local only | Exposed as local telemetry, not global truth |
| Project settings | Strongly coordinated | Existing settings payloads remain canonical payload shape |
| Auth material / provider credentials | Queued-for-later (secured transport only) | Explicit auth channel; never merged as ordinary settings data |
| Execution runs / live activity streams | Node-local + queued summary | Live events local; durable run outcomes appended later |
| Audit / event streams (`activityLog`, `runAuditEvents`) | Append-only replicated | Immutable event replication with origin metadata |
| Filesystem blobs (`.fusion/tasks/*` prompts/logs/attachments) | Queued-for-later | Metadata in replicated records, blob transfer out-of-band |
## 5. Write classes
- **`strong`**: Requires coordinator fence + quorum ack before `committed`.
- **`append-only`**: Event-style immutable replication; dedupe by event id.
- **`queued`**: Accept locally when peers unavailable; replay later.
- **`local`**: Never replicated globally.
## 6. Replication envelope
Every replicated record uses:
- `protocol`, `version`
- `recordId`, `entityType`, `entityId`
- `originNodeId`, `originSeq`
- `writeClass`
- `leaseEpoch`, `fenceToken`
- `intentId` and `state` (`intent` | `committed` | `rejected` | `queued` | `reconciled`)
- `createdAt`, `committedAt?`
- `payload`
- `precondition?` (base revision / expected epoch)
`PeerSyncRequest` / `PeerSyncResponse` remain mesh exchange carriers. v1 envelopes are payloads exchanged through current mesh sync infrastructure and follow-on sync endpoints.
## 7. Quorum and acknowledgements
For `strong` writes:
1. Coordinator accepts intent locally.
2. Coordinator requests acknowledgements from peers in current membership view.
3. Commit requires `quorum = floor(eligibleVoters / 2) + 1` including coordinator.
4. If quorum fails before timeout, intent becomes `queued` with retry metadata.
`append-only` writes can be accepted locally and replicated asynchronously, but must preserve origin ordering `(originNodeId, originSeq)`.
## 8. Lease epochs and fencing
- Coordinator authority is leased with a monotonic `leaseEpoch`.
- Any write with stale epoch/fence must be rejected (`fenced`).
- Restarted nodes must reacquire lease and increment epoch before coordinating strong writes.
- Replay workers must carry original fence metadata; reconciler can reject stale queued entries after epoch advancement.
## 9. Offline queueing and replay
When a strong/queued write cannot reach quorum:
- Persist queue entry durably with:
- `intentId`, `entityType`, `entityId`, `writeClass`
- `originNodeId`, `originSeq`, `leaseEpoch`, `fenceToken`
- retry counters, first/last attempt timestamps, next attempt time
- Local node may expose optimistic local result as `queued` only (not globally committed).
Replay ordering:
1. Sort by `(leaseEpoch asc, originSeq asc, createdAt asc, intentId asc)`.
2. Re-validate preconditions and fence tokens.
3. Commit, reject, or reconcile with deterministic outcome.
## 10. Reconciliation
Reconciliation outcomes are explicit:
- `applied` — replayed successfully.
- `noop_already_applied` — idempotent duplicate.
- `superseded` — newer committed revision already exists.
- `conflict_requires_merge` — semantic conflict; requires policy/agent/manual resolution.
- `rejected_fenced` — stale epoch/fence.
Conflict policy must never silently downgrade strong writes to local-only updates.
## 11. Restart recovery hooks
On node startup:
1. Load durable queue.
2. Rebuild last known lease epoch / origin sequence.
3. Mark in-flight intents without terminal state as `queued` recovery candidates.
4. Start replay loop only after mesh membership snapshot and lease status are known.
## 12. Degraded reads and staleness
Read responses for shared entities include staleness metadata:
- `source`: `local-committed` | `local-queued` | `replica`
- `lastGlobalCommitAt`
- `replicationLagMs`
- `queueDepth`
- `isStale`
In degraded mode, clients may read last-known global state plus queued-local overlays, but must be able to distinguish them.
## 13. End-to-end v1 write path
1. **Intent creation**: Node creates write intent + envelope.
2. **Coordinator selection**: Node routes to current coordinator lease holder for the entity scope.
3. **Commit/ack**:
- strong: quorum commit
- append-only: local append + async replication
4. **Fallback**: if unreachable/quorum-fail, persist queue entry (`queued`).
5. **Replay**: on recovery, replay durable queue in canonical order with fencing checks.
6. **Reconciliation**: produce explicit outcome and update entity revision state.
## 14. Contract for FN-3449 through FN-3456
Follow-on tasks must implement against this contract and not redefine it:
- **FN-3449**: distributed ids/origin sequence allocation + monotonic ordering.
- **FN-3450**: coordinator selection and lease management runtime.
- **FN-3451**: strong-write commit path + quorum ack handling.
- **FN-3452**: durable offline queue persistence and replay engine.
- **FN-3453**: reconciliation executor + conflict outcome handling.
- **FN-3454**: restart recovery bootstrap and in-flight intent recovery.
- **FN-3455**: degraded-read APIs exposing staleness metadata.
- **FN-3456**: partition behavior policy, observability, and operator controls.
## 15. Security boundary
- Mesh transport authentication (node API keys / trust) is mandatory for replication traffic.
- Auth credential replication is explicit and separately controlled from ordinary settings replication.
- Sensitive payloads must be redacted from non-secure logs and diagnostics.

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
SHARED_MESH_PROTOCOL_ID,
SHARED_MESH_PROTOCOL_VERSION,
classifyReadStaleness,
createFenceToken,
getCoordinationModeForEntity,
getDefaultWriteClassForEntity,
getQuorumRequirement,
isMeshIntentState,
isMeshWriteClass,
isProtocolRef,
isQuorumSatisfied,
} from "../mesh-replication-protocol.js";
describe("mesh-replication-protocol", () => {
it("exposes protocol identity", () => {
expect(SHARED_MESH_PROTOCOL_ID).toBe("fusion.shared-mesh");
expect(SHARED_MESH_PROTOCOL_VERSION).toBe("1.0");
});
it("classifies entity coordination modes", () => {
expect(getCoordinationModeForEntity("task")).toBe("strongly-coordinated");
expect(getCoordinationModeForEntity("audit-event")).toBe("append-only-replicated");
expect(getCoordinationModeForEntity("filesystem-blob")).toBe("queued-for-later");
expect(getCoordinationModeForEntity("agent-runtime")).toBe("node-local-only");
});
it("maps entity coordination mode to default write class", () => {
expect(getDefaultWriteClassForEntity("task")).toBe("strong");
expect(getDefaultWriteClassForEntity("audit-event")).toBe("append-only");
expect(getDefaultWriteClassForEntity("filesystem-blob")).toBe("queued");
expect(getDefaultWriteClassForEntity("agent-runtime")).toBe("local");
});
it("computes quorum requirements and satisfies majority rule", () => {
expect(getQuorumRequirement(1)).toEqual({ eligibleVoters: 1, requiredAcks: 1 });
expect(getQuorumRequirement(2)).toEqual({ eligibleVoters: 2, requiredAcks: 2 });
expect(getQuorumRequirement(3)).toEqual({ eligibleVoters: 3, requiredAcks: 2 });
expect(isQuorumSatisfied(5, 2)).toBe(false);
expect(isQuorumSatisfied(5, 3)).toBe(true);
});
it("validates write class and intent state discriminators", () => {
expect(isMeshWriteClass("strong")).toBe(true);
expect(isMeshWriteClass("invalid")).toBe(false);
expect(isMeshIntentState("committed")).toBe(true);
expect(isMeshIntentState("waiting")).toBe(false);
});
it("creates deterministic fence tokens", () => {
expect(createFenceToken(7, "node_a", 99)).toBe("7:node_a:99");
});
it("validates protocol refs", () => {
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "1.0" })).toBe(true);
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "2.0" })).toBe(false);
});
it("classifies read staleness from queue depth and lag", () => {
const fresh = classifyReadStaleness({ queueDepth: 0, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:10.000Z" });
expect(fresh.isStale).toBe(false);
expect(fresh.source).toBe("local-committed");
const queued = classifyReadStaleness({ queueDepth: 2, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:09.000Z" });
expect(queued.isStale).toBe(true);
expect(queued.source).toBe("local-queued");
expect(queued.replicationLagMs).toBe(1000);
});
});

View File

@@ -1,6 +1,7 @@
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey } from "./types.js"; export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey } from "./types.js";
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js"; export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
export { AGENT_VALID_TRANSITIONS } from "./types.js"; export { AGENT_VALID_TRANSITIONS } from "./types.js";
export * from "./mesh-replication-protocol.js";
export { export {
BUILTIN_AGENT_PROMPTS, BUILTIN_AGENT_PROMPTS,
resolveAgentPrompt, resolveAgentPrompt,

View File

@@ -0,0 +1,179 @@
import type { NodeMeshState, PeerSyncRequest, PeerSyncResponse, SettingsSyncPayload } from "./types.js";
export const SHARED_MESH_PROTOCOL_ID = "fusion.shared-mesh" as const;
export const SHARED_MESH_PROTOCOL_VERSION = "1.0" as const;
export const MESH_WRITE_CLASSES = ["strong", "append-only", "queued", "local"] as const;
export type MeshWriteClass = (typeof MESH_WRITE_CLASSES)[number];
export const MESH_INTENT_STATES = ["intent", "committed", "rejected", "queued", "reconciled"] as const;
export type MeshIntentState = (typeof MESH_INTENT_STATES)[number];
export const MESH_RECONCILIATION_OUTCOMES = [
"applied",
"noop_already_applied",
"superseded",
"conflict_requires_merge",
"rejected_fenced",
] as const;
export type MeshReconciliationOutcome = (typeof MESH_RECONCILIATION_OUTCOMES)[number];
export type SharedMeshEntityType =
| "task"
| "task-metadata"
| "mission"
| "agent-config"
| "agent-runtime"
| "project-settings"
| "auth-material"
| "execution-run"
| "audit-event"
| "filesystem-blob";
export type SharedMeshCoordinationMode = "strongly-coordinated" | "append-only-replicated" | "queued-for-later" | "node-local-only";
export interface SharedMeshProtocolRef {
protocol: typeof SHARED_MESH_PROTOCOL_ID;
version: typeof SHARED_MESH_PROTOCOL_VERSION;
}
export interface SharedMeshLeaseRef {
leaseEpoch: number;
fenceToken: string;
coordinatorNodeId: string;
}
export interface SharedMeshWritePrecondition {
expectedBaseRevision?: string;
expectedLeaseEpoch?: number;
}
export interface SharedMeshReplicationEnvelope<TPayload = unknown> extends SharedMeshProtocolRef, SharedMeshLeaseRef {
recordId: string;
intentId: string;
entityType: SharedMeshEntityType;
entityId: string;
originNodeId: string;
originSeq: number;
writeClass: MeshWriteClass;
state: MeshIntentState;
createdAt: string;
committedAt?: string;
precondition?: SharedMeshWritePrecondition;
payload: TPayload;
}
export interface SharedMeshQueueEntryMeta {
firstAttemptAt: string;
lastAttemptAt?: string;
nextAttemptAt?: string;
retryCount: number;
}
export interface SharedMeshReconciliationResult {
intentId: string;
outcome: MeshReconciliationOutcome;
detail?: string;
}
export interface SharedMeshReadStaleness {
source: "local-committed" | "local-queued" | "replica";
lastGlobalCommitAt?: string;
replicationLagMs?: number;
queueDepth: number;
isStale: boolean;
}
export interface SharedMeshQuorumRequirement {
eligibleVoters: number;
requiredAcks: number;
}
export type SharedMeshSyncRequestEnvelope<TPayload = unknown> = PeerSyncRequest & {
replication?: SharedMeshReplicationEnvelope<TPayload>[];
};
export type SharedMeshSyncResponseEnvelope<TPayload = unknown> = PeerSyncResponse & {
replication?: SharedMeshReplicationEnvelope<TPayload>[];
};
export interface SharedMeshSettingsRecord {
settings: SettingsSyncPayload;
}
export interface SharedMeshSnapshot {
mesh: NodeMeshState;
staleness: SharedMeshReadStaleness;
}
const COORDINATION_BY_ENTITY: Record<SharedMeshEntityType, SharedMeshCoordinationMode> = {
task: "strongly-coordinated",
"task-metadata": "strongly-coordinated",
mission: "strongly-coordinated",
"agent-config": "strongly-coordinated",
"agent-runtime": "node-local-only",
"project-settings": "strongly-coordinated",
"auth-material": "queued-for-later",
"execution-run": "queued-for-later",
"audit-event": "append-only-replicated",
"filesystem-blob": "queued-for-later",
};
export function isMeshWriteClass(value: string): value is MeshWriteClass {
return MESH_WRITE_CLASSES.includes(value as MeshWriteClass);
}
export function isMeshIntentState(value: string): value is MeshIntentState {
return MESH_INTENT_STATES.includes(value as MeshIntentState);
}
export function getCoordinationModeForEntity(entityType: SharedMeshEntityType): SharedMeshCoordinationMode {
return COORDINATION_BY_ENTITY[entityType];
}
export function getDefaultWriteClassForEntity(entityType: SharedMeshEntityType): MeshWriteClass {
const mode = getCoordinationModeForEntity(entityType);
if (mode === "append-only-replicated") return "append-only";
if (mode === "queued-for-later") return "queued";
if (mode === "node-local-only") return "local";
return "strong";
}
export function getQuorumRequirement(eligibleVoters: number): SharedMeshQuorumRequirement {
const normalized = Math.max(1, Math.floor(eligibleVoters));
return {
eligibleVoters: normalized,
requiredAcks: Math.floor(normalized / 2) + 1,
};
}
export function isQuorumSatisfied(eligibleVoters: number, ackCount: number): boolean {
return ackCount >= getQuorumRequirement(eligibleVoters).requiredAcks;
}
export function createFenceToken(leaseEpoch: number, coordinatorNodeId: string, originSeq: number): string {
return `${leaseEpoch}:${coordinatorNodeId}:${originSeq}`;
}
export function isProtocolRef(value: { protocol?: string; version?: string } | null | undefined): value is SharedMeshProtocolRef {
return value?.protocol === SHARED_MESH_PROTOCOL_ID && value.version === SHARED_MESH_PROTOCOL_VERSION;
}
export function classifyReadStaleness(params: {
queueDepth: number;
lastGlobalCommitAt?: string;
observedAt?: string;
}): SharedMeshReadStaleness {
const observedAt = params.observedAt ? Date.parse(params.observedAt) : Date.now();
const lastGlobal = params.lastGlobalCommitAt ? Date.parse(params.lastGlobalCommitAt) : undefined;
const lag = lastGlobal !== undefined && Number.isFinite(lastGlobal) ? Math.max(0, observedAt - lastGlobal) : undefined;
const queueDepth = Math.max(0, params.queueDepth);
return {
source: queueDepth > 0 ? "local-queued" : "local-committed",
lastGlobalCommitAt: params.lastGlobalCommitAt,
replicationLagMs: lag,
queueDepth,
isStale: queueDepth > 0 || (lag ?? 0) > 0,
};
}

View File

@@ -1251,7 +1251,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
/> />
<span className="text-secondary">min</span> <span className="text-secondary">min</span>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleCustomHeartbeatSave(agent)} onClick={() => void handleCustomHeartbeatSave(agent)}
disabled={isUpdatingHeartbeat} disabled={isUpdatingHeartbeat}
title="Save custom interval" title="Save custom interval"
@@ -1259,7 +1259,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
Save Save
</button> </button>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => { onClick={() => {
setCustomHeartbeatAgentId(null); setCustomHeartbeatAgentId(null);
setCustomHeartbeatMinutes((prev) => { setCustomHeartbeatMinutes((prev) => {
@@ -1327,7 +1327,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<div className="agent-card-actions"> <div className="agent-card-actions">
{agent.state === "idle" && ( {agent.state === "idle" && (
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")} onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Activate" title="Activate"
@@ -1338,7 +1338,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
{agent.state === "active" && ( {agent.state === "active" && (
<> <>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleRunHeartbeat(agent.id, agent.name)} onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Run Now" title="Run Now"
@@ -1347,7 +1347,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<Activity size={14} /> Run Now <Activity size={14} /> Run Now
</button> </button>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")} onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Pause" title="Pause"
@@ -1358,7 +1358,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)} )}
{agent.state === "paused" && ( {agent.state === "paused" && (
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")} onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Resume" title="Resume"
@@ -1369,7 +1369,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
{agent.state === "running" && ( {agent.state === "running" && (
<> <>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })} onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
title="View live run details" title="View live run details"
aria-label={`View live run details for ${agent.name}`} aria-label={`View live run details for ${agent.name}`}
@@ -1377,7 +1377,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
<Activity size={14} /> Running <Activity size={14} /> Running
</button> </button>
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "paused")} onClick={() => void handleStateChange(agent.id, "paused")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Pause" title="Pause"
@@ -1388,7 +1388,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)} )}
{agent.state === "error" && ( {agent.state === "error" && (
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")} onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Retry" title="Retry"
@@ -1398,7 +1398,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
)} )}
{agent.state === "terminated" && ( {agent.state === "terminated" && (
<button <button
className="btn btn--sm" className="btn btn-sm"
onClick={() => void handleStateChange(agent.id, "active")} onClick={() => void handleStateChange(agent.id, "active")}
disabled={transitioningAgentIds.has(agent.id)} disabled={transitioningAgentIds.has(agent.id)}
title="Start" title="Start"
@@ -1407,7 +1407,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</button> </button>
)} )}
<button <button
className="btn btn--sm agent-card-details-btn" className="btn btn-sm agent-card-details-btn"
onClick={() => openAgentDetail(agent.id)} onClick={() => openAgentDetail(agent.id)}
title={`View details for ${agent.name}`} title={`View details for ${agent.name}`}
aria-label={`View details for ${agent.name}`} aria-label={`View details for ${agent.name}`}
@@ -1416,7 +1416,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
</button> </button>
{(agent.state === "idle" || agent.state === "terminated" || agent.state === "paused") && ( {(agent.state === "idle" || agent.state === "terminated" || agent.state === "paused") && (
<button <button
className="btn btn--sm btn--danger" className="btn btn-sm btn-danger"
onClick={() => void handleDelete(agent.id, agent.name)} onClick={() => void handleDelete(agent.id, agent.name)}
title="Delete" title="Delete"
> >