agent-relay 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.trajectories/active/traj_3yx9dy148mge.json +42 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.json +49 -0
- package/.trajectories/completed/2026-01/traj_1g7yx6qtg4ai.md +31 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.json +49 -0
- package/.trajectories/completed/2026-01/traj_4qwd4zmhfwp4.md +31 -0
- package/.trajectories/completed/2026-01/traj_6unwwmgyj5sq.json +109 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.json +49 -0
- package/.trajectories/completed/2026-01/traj_a0tqx8biw9c4.md +31 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.json +66 -0
- package/.trajectories/completed/2026-01/traj_ax8uungxz2qh.md +36 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.json +49 -0
- package/.trajectories/completed/2026-01/traj_c9izbh2snpzf.md +31 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.json +65 -0
- package/.trajectories/completed/2026-01/traj_cpn70dw066nt.md +37 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.json +36 -0
- package/.trajectories/completed/2026-01/traj_erglv2f8t9eh.md +21 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.json +101 -0
- package/.trajectories/completed/2026-01/traj_he75f24d1xfm.md +52 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.json +61 -0
- package/.trajectories/completed/2026-01/traj_lgtodco7dp1n.md +36 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.json +73 -0
- package/.trajectories/completed/2026-01/traj_oszg9flv74pk.md +41 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.json +77 -0
- package/.trajectories/completed/2026-01/traj_pulomd3y8cvj.md +42 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.json +109 -0
- package/.trajectories/completed/2026-01/traj_rsavt0jipi3c.md +56 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.json +113 -0
- package/.trajectories/completed/2026-01/traj_x721m1j9rzup.md +57 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.json +61 -0
- package/.trajectories/completed/2026-01/traj_xjqvmep5ed3h.md +36 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.json +49 -0
- package/.trajectories/completed/2026-01/traj_y7n6hfbf7dmg.md +31 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.json +49 -0
- package/.trajectories/completed/2026-01/traj_yvfkwnkdiso2.md +31 -0
- package/.trajectories/index.json +140 -1
- package/TRAIL_GIT_AUTH_FIX.md +113 -0
- package/deploy/workspace/codex.config.toml +1 -1
- package/deploy/workspace/entrypoint.sh +20 -79
- package/deploy/workspace/gh-relay +156 -0
- package/deploy/workspace/git-credential-relay +5 -1
- package/dist/bridge/multi-project-client.js +13 -10
- package/dist/bridge/spawner.d.ts +2 -0
- package/dist/bridge/spawner.js +19 -1
- package/dist/bridge/types.d.ts +2 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +115 -69
- package/dist/cloud/api/admin.js +16 -3
- package/dist/cloud/api/codex-auth-helper.js +28 -8
- package/dist/cloud/api/consensus.d.ts +13 -0
- package/dist/cloud/api/consensus.js +259 -0
- package/dist/cloud/api/daemons.js +205 -1
- package/dist/cloud/api/git.js +37 -7
- package/dist/cloud/api/onboarding.js +4 -1
- package/dist/cloud/api/provider-env.d.ts +5 -0
- package/dist/cloud/api/provider-env.js +27 -0
- package/dist/cloud/api/providers.js +2 -0
- package/dist/cloud/api/test-helpers.js +130 -0
- package/dist/cloud/api/workspaces.js +38 -3
- package/dist/cloud/db/bulk-ingest.d.ts +88 -0
- package/dist/cloud/db/bulk-ingest.js +268 -0
- package/dist/cloud/db/drizzle.d.ts +33 -0
- package/dist/cloud/db/drizzle.js +174 -2
- package/dist/cloud/db/index.d.ts +24 -5
- package/dist/cloud/db/index.js +19 -4
- package/dist/cloud/db/schema.d.ts +397 -3
- package/dist/cloud/db/schema.js +75 -1
- package/dist/cloud/provisioner/index.d.ts +8 -0
- package/dist/cloud/provisioner/index.js +256 -50
- package/dist/cloud/server.js +47 -3
- package/dist/cloud/services/index.d.ts +1 -0
- package/dist/cloud/services/index.js +2 -0
- package/dist/cloud/services/nango.d.ts +3 -4
- package/dist/cloud/services/nango.js +11 -33
- package/dist/cloud/services/workspace-keepalive.d.ts +76 -0
- package/dist/cloud/services/workspace-keepalive.js +234 -0
- package/dist/config/relay-config.d.ts +23 -0
- package/dist/config/relay-config.js +23 -0
- package/dist/daemon/agent-manager.d.ts +20 -1
- package/dist/daemon/agent-manager.js +47 -0
- package/dist/daemon/agent-registry.js +4 -4
- package/dist/daemon/agent-signing.d.ts +158 -0
- package/dist/daemon/agent-signing.js +523 -0
- package/dist/daemon/api.js +18 -1
- package/dist/daemon/cli-auth.d.ts +4 -1
- package/dist/daemon/cli-auth.js +55 -11
- package/dist/daemon/cloud-sync.d.ts +47 -1
- package/dist/daemon/cloud-sync.js +152 -3
- package/dist/daemon/connection.d.ts +28 -0
- package/dist/daemon/connection.js +98 -15
- package/dist/daemon/consensus-integration.d.ts +167 -0
- package/dist/daemon/consensus-integration.js +371 -0
- package/dist/daemon/consensus.d.ts +271 -0
- package/dist/daemon/consensus.js +632 -0
- package/dist/daemon/delivery-tracker.d.ts +34 -0
- package/dist/daemon/delivery-tracker.js +104 -0
- package/dist/daemon/enhanced-features.d.ts +118 -0
- package/dist/daemon/enhanced-features.js +178 -0
- package/dist/daemon/index.d.ts +4 -0
- package/dist/daemon/index.js +5 -0
- package/dist/daemon/rate-limiter.d.ts +68 -0
- package/dist/daemon/rate-limiter.js +130 -0
- package/dist/daemon/router.d.ts +18 -11
- package/dist/daemon/router.js +55 -111
- package/dist/daemon/server.d.ts +13 -1
- package/dist/daemon/server.js +71 -9
- package/dist/daemon/sync-queue.d.ts +116 -0
- package/dist/daemon/sync-queue.js +361 -0
- package/dist/health-worker-manager.d.ts +62 -0
- package/dist/health-worker-manager.js +144 -0
- package/dist/health-worker.d.ts +9 -0
- package/dist/health-worker.js +79 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/memory/context-compaction.d.ts +156 -0
- package/dist/memory/context-compaction.js +453 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/protocol/channels.js +4 -4
- package/dist/protocol/framing.d.ts +72 -10
- package/dist/protocol/framing.js +194 -25
- package/dist/storage/adapter.d.ts +8 -1
- package/dist/storage/adapter.js +11 -0
- package/dist/storage/batched-sqlite-adapter.d.ts +71 -0
- package/dist/storage/batched-sqlite-adapter.js +183 -0
- package/dist/storage/dead-letter-queue.d.ts +196 -0
- package/dist/storage/dead-letter-queue.js +427 -0
- package/dist/storage/dlq-adapter.d.ts +195 -0
- package/dist/storage/dlq-adapter.js +664 -0
- package/dist/trajectory/config.d.ts +32 -14
- package/dist/trajectory/config.js +38 -16
- package/dist/trajectory/integration.js +217 -64
- package/dist/utils/git-remote.d.ts +47 -0
- package/dist/utils/git-remote.js +125 -0
- package/dist/utils/id-generator.d.ts +35 -0
- package/dist/utils/id-generator.js +60 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/precompiled-patterns.d.ts +110 -0
- package/dist/utils/precompiled-patterns.js +322 -0
- package/dist/wrapper/auth-detection.js +1 -1
- package/dist/wrapper/base-wrapper.d.ts +36 -0
- package/dist/wrapper/base-wrapper.js +48 -2
- package/dist/wrapper/client.d.ts +14 -4
- package/dist/wrapper/client.js +84 -31
- package/dist/wrapper/idle-detector.d.ts +102 -0
- package/dist/wrapper/idle-detector.js +279 -0
- package/dist/wrapper/parser.d.ts +4 -0
- package/dist/wrapper/parser.js +19 -1
- package/dist/wrapper/pty-wrapper.d.ts +7 -1
- package/dist/wrapper/pty-wrapper.js +51 -27
- package/dist/wrapper/tmux-wrapper.d.ts +12 -1
- package/dist/wrapper/tmux-wrapper.js +65 -17
- package/package.json +5 -5
- package/scripts/run-migrations.js +43 -0
- package/scripts/verify-schema.js +134 -0
- package/tests/benchmarks/protocol.bench.ts +310 -0
- package/dist/dashboard/out/404.html +0 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_buildManifest.js +0 -1
- package/dist/dashboard/out/_next/static/T1tgCqVWHFIkV7ClEtzD7/_ssgManifest.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/116-2502180def231162.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/117-f7b8ab0809342e77.js +0 -2
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/648-5cc6e1921389a58a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/766-b54f0853794b78c3.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/83-b51836037078006c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/891-6cd50de1224f70bb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/899-bb19a9b3d9b39ea6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-53b8a69f76db17d0.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-8939b0fc700f7eca.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-5af1b6b439858aa6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-f45ecbc3e06134fc.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/history/page-8c8bed33beb2bf1c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-2433bb48965f4333.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-16f3b49e55b1e0ed.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-ac39dc0cc3c26fa7.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-4a5938c18a11a654.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-982a7000fee44014.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ac3a6ac433fd6001.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-09f9caae98a18c09.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-547dd0ca55ecd0ba.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
- package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-2ee6beb2ae96d210.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-5d692157a8eb1fd9.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
- package/dist/dashboard/out/_next/static/css/85d2af9c7ac74d62.css +0 -1
- package/dist/dashboard/out/_next/static/css/fe4b28883eeff359.css +0 -1
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
- package/dist/dashboard/out/alt-logos/logo.svg +0 -38
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
- package/dist/dashboard/out/app/onboarding.html +0 -1
- package/dist/dashboard/out/app/onboarding.txt +0 -7
- package/dist/dashboard/out/app.html +0 -1
- package/dist/dashboard/out/app.txt +0 -7
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/connect-repos.html +0 -1
- package/dist/dashboard/out/connect-repos.txt +0 -7
- package/dist/dashboard/out/history.html +0 -1
- package/dist/dashboard/out/history.txt +0 -7
- package/dist/dashboard/out/index.html +0 -1
- package/dist/dashboard/out/index.txt +0 -7
- package/dist/dashboard/out/login.html +0 -6
- package/dist/dashboard/out/login.txt +0 -7
- package/dist/dashboard/out/metrics.html +0 -1
- package/dist/dashboard/out/metrics.txt +0 -7
- package/dist/dashboard/out/pricing.html +0 -13
- package/dist/dashboard/out/pricing.txt +0 -7
- package/dist/dashboard/out/providers/setup/claude.html +0 -1
- package/dist/dashboard/out/providers/setup/claude.txt +0 -8
- package/dist/dashboard/out/providers/setup/codex.html +0 -1
- package/dist/dashboard/out/providers/setup/codex.txt +0 -8
- package/dist/dashboard/out/providers.html +0 -1
- package/dist/dashboard/out/providers.txt +0 -7
- package/dist/dashboard/out/signup.html +0 -6
- package/dist/dashboard/out/signup.txt +0 -7
- package/dist/dashboard-server/metrics.d.ts +0 -105
- package/dist/dashboard-server/metrics.js +0 -193
- package/dist/dashboard-server/needs-attention.d.ts +0 -24
- package/dist/dashboard-server/needs-attention.js +0 -78
- package/dist/dashboard-server/server.d.ts +0 -15
- package/dist/dashboard-server/server.js +0 -3776
- package/dist/dashboard-server/start.d.ts +0 -6
- package/dist/dashboard-server/start.js +0 -13
- package/dist/dashboard-server/user-bridge.d.ts +0 -103
- package/dist/dashboard-server/user-bridge.js +0 -189
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Consensus Mechanism
|
|
3
|
+
*
|
|
4
|
+
* Enables distributed decision-making across multiple agents.
|
|
5
|
+
* Inspired by russian-code-ts roadmap: "Consensus-based decision making"
|
|
6
|
+
*
|
|
7
|
+
* Consensus Types:
|
|
8
|
+
* 1. Majority Vote - Simple >50% agreement
|
|
9
|
+
* 2. Supermajority - 2/3 or configurable threshold
|
|
10
|
+
* 3. Unanimous - All participants must agree
|
|
11
|
+
* 4. Weighted - Votes weighted by agent role/expertise
|
|
12
|
+
* 5. Quorum - Minimum participation required
|
|
13
|
+
*
|
|
14
|
+
* Use Cases:
|
|
15
|
+
* - Code review approval (2+ agents approve)
|
|
16
|
+
* - Architecture decisions (lead + majority)
|
|
17
|
+
* - Deployment gates (all critical agents agree)
|
|
18
|
+
* - Task assignment (weighted by expertise)
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
21
|
+
import { EventEmitter } from 'node:events';
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Default Configuration
|
|
24
|
+
// =============================================================================
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
defaultTimeoutMs: 5 * 60 * 1000, // 5 minutes
|
|
27
|
+
defaultConsensusType: 'majority',
|
|
28
|
+
defaultThreshold: 0.67, // 2/3 for supermajority
|
|
29
|
+
allowVoteChange: true,
|
|
30
|
+
autoResolve: true,
|
|
31
|
+
broadcastProposals: true,
|
|
32
|
+
};
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Consensus Engine
|
|
35
|
+
// =============================================================================
|
|
36
|
+
export class ConsensusEngine extends EventEmitter {
|
|
37
|
+
config;
|
|
38
|
+
proposals = new Map();
|
|
39
|
+
expiryTimers = new Map();
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
super();
|
|
42
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
43
|
+
}
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// Proposal Management
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
/**
|
|
48
|
+
* Create a new proposal.
|
|
49
|
+
*/
|
|
50
|
+
createProposal(options) {
|
|
51
|
+
const id = `prop_${Date.now()}_${randomUUID().substring(0, 8)}`;
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const timeoutMs = options.timeoutMs ?? this.config.defaultTimeoutMs;
|
|
54
|
+
const proposal = {
|
|
55
|
+
id,
|
|
56
|
+
title: options.title,
|
|
57
|
+
description: options.description,
|
|
58
|
+
proposer: options.proposer,
|
|
59
|
+
consensusType: options.consensusType ?? this.config.defaultConsensusType,
|
|
60
|
+
participants: options.participants,
|
|
61
|
+
quorum: options.quorum,
|
|
62
|
+
threshold: options.threshold ?? this.config.defaultThreshold,
|
|
63
|
+
weights: options.weights,
|
|
64
|
+
createdAt: now,
|
|
65
|
+
expiresAt: now + timeoutMs,
|
|
66
|
+
status: 'pending',
|
|
67
|
+
votes: [],
|
|
68
|
+
metadata: options.metadata,
|
|
69
|
+
thread: options.thread ?? `consensus-${id}`,
|
|
70
|
+
};
|
|
71
|
+
this.proposals.set(id, proposal);
|
|
72
|
+
this.scheduleExpiry(proposal);
|
|
73
|
+
this.emit('proposal:created', proposal);
|
|
74
|
+
return proposal;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Submit a vote on a proposal.
|
|
78
|
+
*/
|
|
79
|
+
vote(proposalId, agent, value, reason) {
|
|
80
|
+
const proposal = this.proposals.get(proposalId);
|
|
81
|
+
if (!proposal) {
|
|
82
|
+
return { success: false, error: 'Proposal not found' };
|
|
83
|
+
}
|
|
84
|
+
if (proposal.status !== 'pending') {
|
|
85
|
+
return { success: false, error: `Proposal is ${proposal.status}` };
|
|
86
|
+
}
|
|
87
|
+
if (!proposal.participants.includes(agent)) {
|
|
88
|
+
return { success: false, error: 'Agent not a participant' };
|
|
89
|
+
}
|
|
90
|
+
if (Date.now() > proposal.expiresAt) {
|
|
91
|
+
this.expireProposal(proposal);
|
|
92
|
+
return { success: false, error: 'Proposal has expired' };
|
|
93
|
+
}
|
|
94
|
+
// Check for existing vote
|
|
95
|
+
const existingVoteIndex = proposal.votes.findIndex(v => v.agent === agent);
|
|
96
|
+
if (existingVoteIndex >= 0) {
|
|
97
|
+
if (!this.config.allowVoteChange) {
|
|
98
|
+
return { success: false, error: 'Vote already cast and changes not allowed' };
|
|
99
|
+
}
|
|
100
|
+
// Remove existing vote
|
|
101
|
+
proposal.votes.splice(existingVoteIndex, 1);
|
|
102
|
+
}
|
|
103
|
+
// Determine vote weight
|
|
104
|
+
const weight = this.getAgentWeight(proposal, agent);
|
|
105
|
+
const vote = {
|
|
106
|
+
agent,
|
|
107
|
+
value,
|
|
108
|
+
weight,
|
|
109
|
+
reason,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
};
|
|
112
|
+
proposal.votes.push(vote);
|
|
113
|
+
this.emit('proposal:voted', proposal, vote);
|
|
114
|
+
// Check for auto-resolution
|
|
115
|
+
if (this.config.autoResolve) {
|
|
116
|
+
const result = this.calculateResult(proposal);
|
|
117
|
+
if (this.canResolveEarly(proposal, result)) {
|
|
118
|
+
this.resolveProposal(proposal, result);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { success: true, proposal };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get a proposal by ID.
|
|
125
|
+
*/
|
|
126
|
+
getProposal(proposalId) {
|
|
127
|
+
return this.proposals.get(proposalId) ?? null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get all proposals for an agent (as participant or proposer).
|
|
131
|
+
*/
|
|
132
|
+
getProposalsForAgent(agent) {
|
|
133
|
+
const results = [];
|
|
134
|
+
for (const proposal of this.proposals.values()) {
|
|
135
|
+
if (proposal.proposer === agent || proposal.participants.includes(agent)) {
|
|
136
|
+
results.push(proposal);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get pending proposals awaiting an agent's vote.
|
|
143
|
+
*/
|
|
144
|
+
getPendingVotesForAgent(agent) {
|
|
145
|
+
const results = [];
|
|
146
|
+
for (const proposal of this.proposals.values()) {
|
|
147
|
+
if (proposal.status !== 'pending')
|
|
148
|
+
continue;
|
|
149
|
+
if (!proposal.participants.includes(agent))
|
|
150
|
+
continue;
|
|
151
|
+
if (proposal.votes.some(v => v.agent === agent))
|
|
152
|
+
continue;
|
|
153
|
+
results.push(proposal);
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Cancel a proposal (only proposer can cancel).
|
|
159
|
+
*/
|
|
160
|
+
cancelProposal(proposalId, agent) {
|
|
161
|
+
const proposal = this.proposals.get(proposalId);
|
|
162
|
+
if (!proposal) {
|
|
163
|
+
return { success: false, error: 'Proposal not found' };
|
|
164
|
+
}
|
|
165
|
+
if (proposal.proposer !== agent) {
|
|
166
|
+
return { success: false, error: 'Only proposer can cancel' };
|
|
167
|
+
}
|
|
168
|
+
if (proposal.status !== 'pending') {
|
|
169
|
+
return { success: false, error: `Proposal is ${proposal.status}` };
|
|
170
|
+
}
|
|
171
|
+
proposal.status = 'cancelled';
|
|
172
|
+
this.clearExpiryTimer(proposalId);
|
|
173
|
+
this.emit('proposal:cancelled', proposal);
|
|
174
|
+
return { success: true };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Force resolve a proposal (for admin/system use).
|
|
178
|
+
*/
|
|
179
|
+
forceResolve(proposalId) {
|
|
180
|
+
const proposal = this.proposals.get(proposalId);
|
|
181
|
+
if (!proposal || proposal.status !== 'pending')
|
|
182
|
+
return null;
|
|
183
|
+
const result = this.calculateResult(proposal);
|
|
184
|
+
this.resolveProposal(proposal, result);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
// ===========================================================================
|
|
188
|
+
// Consensus Calculation
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
/**
|
|
191
|
+
* Calculate current consensus result.
|
|
192
|
+
*/
|
|
193
|
+
calculateResult(proposal) {
|
|
194
|
+
let approveWeight = 0;
|
|
195
|
+
let rejectWeight = 0;
|
|
196
|
+
let abstainWeight = 0;
|
|
197
|
+
for (const vote of proposal.votes) {
|
|
198
|
+
switch (vote.value) {
|
|
199
|
+
case 'approve':
|
|
200
|
+
approveWeight += vote.weight;
|
|
201
|
+
break;
|
|
202
|
+
case 'reject':
|
|
203
|
+
rejectWeight += vote.weight;
|
|
204
|
+
break;
|
|
205
|
+
case 'abstain':
|
|
206
|
+
abstainWeight += vote.weight;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const totalWeight = this.getTotalWeight(proposal);
|
|
211
|
+
const votedWeight = approveWeight + rejectWeight + abstainWeight;
|
|
212
|
+
const participation = totalWeight > 0 ? votedWeight / totalWeight : 0;
|
|
213
|
+
const voters = new Set(proposal.votes.map(v => v.agent));
|
|
214
|
+
const nonVoters = proposal.participants.filter(p => !voters.has(p));
|
|
215
|
+
// Check quorum
|
|
216
|
+
const quorumRequired = proposal.quorum ?? Math.ceil(proposal.participants.length / 2);
|
|
217
|
+
const quorumMet = proposal.votes.length >= quorumRequired;
|
|
218
|
+
// Determine decision based on consensus type
|
|
219
|
+
const decision = this.determineDecision(proposal, {
|
|
220
|
+
approveWeight,
|
|
221
|
+
rejectWeight,
|
|
222
|
+
abstainWeight,
|
|
223
|
+
totalWeight,
|
|
224
|
+
votedWeight,
|
|
225
|
+
quorumMet,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
decision,
|
|
229
|
+
approveWeight,
|
|
230
|
+
rejectWeight,
|
|
231
|
+
abstainWeight,
|
|
232
|
+
participation,
|
|
233
|
+
quorumMet,
|
|
234
|
+
resolvedAt: Date.now(),
|
|
235
|
+
nonVoters,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Determine decision based on consensus type and votes.
|
|
240
|
+
*/
|
|
241
|
+
determineDecision(proposal, counts) {
|
|
242
|
+
const { approveWeight, rejectWeight, votedWeight, quorumMet } = counts;
|
|
243
|
+
switch (proposal.consensusType) {
|
|
244
|
+
case 'unanimous': {
|
|
245
|
+
// All participants must approve - any reject makes it impossible
|
|
246
|
+
const hasReject = proposal.votes.some(v => v.value === 'reject');
|
|
247
|
+
if (hasReject)
|
|
248
|
+
return 'rejected';
|
|
249
|
+
if (proposal.votes.length < proposal.participants.length) {
|
|
250
|
+
return 'no_consensus';
|
|
251
|
+
}
|
|
252
|
+
const allApprove = proposal.votes.every(v => v.value === 'approve');
|
|
253
|
+
return allApprove ? 'approved' : 'rejected';
|
|
254
|
+
}
|
|
255
|
+
case 'supermajority': {
|
|
256
|
+
const threshold = proposal.threshold ?? this.config.defaultThreshold;
|
|
257
|
+
if (votedWeight === 0)
|
|
258
|
+
return 'no_consensus';
|
|
259
|
+
const approveRatio = approveWeight / votedWeight;
|
|
260
|
+
if (approveRatio >= threshold)
|
|
261
|
+
return 'approved';
|
|
262
|
+
const rejectRatio = rejectWeight / votedWeight;
|
|
263
|
+
if (rejectRatio > (1 - threshold))
|
|
264
|
+
return 'rejected';
|
|
265
|
+
return 'no_consensus';
|
|
266
|
+
}
|
|
267
|
+
case 'quorum': {
|
|
268
|
+
if (!quorumMet)
|
|
269
|
+
return 'no_consensus';
|
|
270
|
+
// Fall through to majority
|
|
271
|
+
}
|
|
272
|
+
// eslint-disable-next-line no-fallthrough
|
|
273
|
+
case 'majority': {
|
|
274
|
+
if (votedWeight === 0)
|
|
275
|
+
return 'no_consensus';
|
|
276
|
+
if (approveWeight > rejectWeight)
|
|
277
|
+
return 'approved';
|
|
278
|
+
if (rejectWeight > approveWeight)
|
|
279
|
+
return 'rejected';
|
|
280
|
+
return 'no_consensus'; // Tie
|
|
281
|
+
}
|
|
282
|
+
case 'weighted': {
|
|
283
|
+
// Same as majority but weights are already applied
|
|
284
|
+
if (votedWeight === 0)
|
|
285
|
+
return 'no_consensus';
|
|
286
|
+
if (approveWeight > rejectWeight)
|
|
287
|
+
return 'approved';
|
|
288
|
+
if (rejectWeight > approveWeight)
|
|
289
|
+
return 'rejected';
|
|
290
|
+
return 'no_consensus';
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
return 'no_consensus';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if proposal can be resolved early (consensus mathematically certain).
|
|
298
|
+
*/
|
|
299
|
+
canResolveEarly(proposal, result) {
|
|
300
|
+
const totalWeight = this.getTotalWeight(proposal);
|
|
301
|
+
const remainingWeight = totalWeight - (result.approveWeight + result.rejectWeight + result.abstainWeight);
|
|
302
|
+
switch (proposal.consensusType) {
|
|
303
|
+
case 'unanimous':
|
|
304
|
+
// Can resolve early if anyone rejects
|
|
305
|
+
return proposal.votes.some(v => v.value === 'reject') ||
|
|
306
|
+
proposal.votes.length === proposal.participants.length;
|
|
307
|
+
case 'supermajority': {
|
|
308
|
+
const threshold = proposal.threshold ?? this.config.defaultThreshold;
|
|
309
|
+
const votedWeight = result.approveWeight + result.rejectWeight + result.abstainWeight;
|
|
310
|
+
// Approved if approve ratio already exceeds threshold
|
|
311
|
+
if (votedWeight > 0 && result.approveWeight / votedWeight >= threshold) {
|
|
312
|
+
// Check if remaining votes can't change outcome
|
|
313
|
+
return (result.approveWeight / (votedWeight + remainingWeight)) >= threshold;
|
|
314
|
+
}
|
|
315
|
+
// Rejected if reject ratio exceeds (1 - threshold)
|
|
316
|
+
if (votedWeight > 0 && result.rejectWeight / votedWeight > (1 - threshold)) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
case 'majority':
|
|
322
|
+
case 'weighted':
|
|
323
|
+
// Can resolve if one side has >50% of total weight
|
|
324
|
+
return result.approveWeight > totalWeight / 2 ||
|
|
325
|
+
result.rejectWeight > totalWeight / 2;
|
|
326
|
+
case 'quorum':
|
|
327
|
+
// Need quorum first
|
|
328
|
+
if (!result.quorumMet)
|
|
329
|
+
return false;
|
|
330
|
+
// Then same as majority
|
|
331
|
+
return result.approveWeight > totalWeight / 2 ||
|
|
332
|
+
result.rejectWeight > totalWeight / 2;
|
|
333
|
+
default:
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
// Weight Management
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
/**
|
|
341
|
+
* Get weight for an agent in a proposal.
|
|
342
|
+
*/
|
|
343
|
+
getAgentWeight(proposal, agent) {
|
|
344
|
+
if (proposal.weights) {
|
|
345
|
+
const weightConfig = proposal.weights.find(w => w.agent === agent);
|
|
346
|
+
if (weightConfig)
|
|
347
|
+
return weightConfig.weight;
|
|
348
|
+
}
|
|
349
|
+
return 1; // Default weight
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get total weight of all participants.
|
|
353
|
+
*/
|
|
354
|
+
getTotalWeight(proposal) {
|
|
355
|
+
let total = 0;
|
|
356
|
+
for (const participant of proposal.participants) {
|
|
357
|
+
total += this.getAgentWeight(proposal, participant);
|
|
358
|
+
}
|
|
359
|
+
return total;
|
|
360
|
+
}
|
|
361
|
+
// ===========================================================================
|
|
362
|
+
// Lifecycle Management
|
|
363
|
+
// ===========================================================================
|
|
364
|
+
/**
|
|
365
|
+
* Resolve a proposal with result.
|
|
366
|
+
*/
|
|
367
|
+
resolveProposal(proposal, result) {
|
|
368
|
+
proposal.status = result.decision === 'approved' ? 'approved' :
|
|
369
|
+
result.decision === 'rejected' ? 'rejected' : 'expired';
|
|
370
|
+
proposal.result = result;
|
|
371
|
+
this.clearExpiryTimer(proposal.id);
|
|
372
|
+
this.emit('proposal:resolved', proposal, result);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Expire a proposal.
|
|
376
|
+
*/
|
|
377
|
+
expireProposal(proposal) {
|
|
378
|
+
if (proposal.status !== 'pending')
|
|
379
|
+
return;
|
|
380
|
+
const result = this.calculateResult(proposal);
|
|
381
|
+
proposal.status = 'expired';
|
|
382
|
+
proposal.result = result;
|
|
383
|
+
this.clearExpiryTimer(proposal.id);
|
|
384
|
+
this.emit('proposal:expired', proposal);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Schedule expiry timer for a proposal.
|
|
388
|
+
*/
|
|
389
|
+
scheduleExpiry(proposal) {
|
|
390
|
+
const timeoutMs = proposal.expiresAt - Date.now();
|
|
391
|
+
if (timeoutMs <= 0) {
|
|
392
|
+
this.expireProposal(proposal);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const timer = setTimeout(() => {
|
|
396
|
+
this.expireProposal(proposal);
|
|
397
|
+
}, timeoutMs);
|
|
398
|
+
timer.unref(); // Don't prevent process exit
|
|
399
|
+
this.expiryTimers.set(proposal.id, timer);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Clear expiry timer for a proposal.
|
|
403
|
+
*/
|
|
404
|
+
clearExpiryTimer(proposalId) {
|
|
405
|
+
const timer = this.expiryTimers.get(proposalId);
|
|
406
|
+
if (timer) {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
this.expiryTimers.delete(proposalId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Cleanup all timers (for shutdown).
|
|
413
|
+
*/
|
|
414
|
+
cleanup() {
|
|
415
|
+
for (const timer of this.expiryTimers.values()) {
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
}
|
|
418
|
+
this.expiryTimers.clear();
|
|
419
|
+
}
|
|
420
|
+
// ===========================================================================
|
|
421
|
+
// Statistics
|
|
422
|
+
// ===========================================================================
|
|
423
|
+
/**
|
|
424
|
+
* Get consensus statistics.
|
|
425
|
+
*/
|
|
426
|
+
getStats() {
|
|
427
|
+
let pending = 0, approved = 0, rejected = 0, expired = 0, cancelled = 0;
|
|
428
|
+
let totalParticipation = 0;
|
|
429
|
+
let resolvedCount = 0;
|
|
430
|
+
for (const proposal of this.proposals.values()) {
|
|
431
|
+
switch (proposal.status) {
|
|
432
|
+
case 'pending':
|
|
433
|
+
pending++;
|
|
434
|
+
break;
|
|
435
|
+
case 'approved':
|
|
436
|
+
approved++;
|
|
437
|
+
break;
|
|
438
|
+
case 'rejected':
|
|
439
|
+
rejected++;
|
|
440
|
+
break;
|
|
441
|
+
case 'expired':
|
|
442
|
+
expired++;
|
|
443
|
+
break;
|
|
444
|
+
case 'cancelled':
|
|
445
|
+
cancelled++;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (proposal.result) {
|
|
449
|
+
totalParticipation += proposal.result.participation;
|
|
450
|
+
resolvedCount++;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
total: this.proposals.size,
|
|
455
|
+
pending,
|
|
456
|
+
approved,
|
|
457
|
+
rejected,
|
|
458
|
+
expired,
|
|
459
|
+
cancelled,
|
|
460
|
+
avgParticipation: resolvedCount > 0 ? totalParticipation / resolvedCount : 0,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// =============================================================================
|
|
465
|
+
// Factory Function
|
|
466
|
+
// =============================================================================
|
|
467
|
+
/**
|
|
468
|
+
* Create a consensus engine with the given configuration.
|
|
469
|
+
*/
|
|
470
|
+
export function createConsensusEngine(config) {
|
|
471
|
+
return new ConsensusEngine(config);
|
|
472
|
+
}
|
|
473
|
+
// =============================================================================
|
|
474
|
+
// Relay Integration Helpers
|
|
475
|
+
// =============================================================================
|
|
476
|
+
/**
|
|
477
|
+
* Format a proposal as a relay message for broadcasting.
|
|
478
|
+
*/
|
|
479
|
+
export function formatProposalMessage(proposal) {
|
|
480
|
+
const lines = [
|
|
481
|
+
`📋 **PROPOSAL: ${proposal.title}**`,
|
|
482
|
+
`ID: ${proposal.id}`,
|
|
483
|
+
`From: ${proposal.proposer}`,
|
|
484
|
+
`Type: ${proposal.consensusType}`,
|
|
485
|
+
`Expires: ${new Date(proposal.expiresAt).toISOString()}`,
|
|
486
|
+
'',
|
|
487
|
+
proposal.description,
|
|
488
|
+
'',
|
|
489
|
+
`Participants: ${proposal.participants.join(', ')}`,
|
|
490
|
+
'',
|
|
491
|
+
'Reply with: VOTE <proposal-id> <approve|reject|abstain> [reason]',
|
|
492
|
+
];
|
|
493
|
+
return lines.join('\n');
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Parse a vote command from a relay message.
|
|
497
|
+
*/
|
|
498
|
+
export function parseVoteCommand(message) {
|
|
499
|
+
const match = message.match(/^VOTE\s+(\S+)\s+(approve|reject|abstain)(?:\s+(.+))?$/i);
|
|
500
|
+
if (!match)
|
|
501
|
+
return null;
|
|
502
|
+
return {
|
|
503
|
+
proposalId: match[1],
|
|
504
|
+
value: match[2].toLowerCase(),
|
|
505
|
+
reason: match[3]?.trim(),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Format a consensus result as a relay message.
|
|
510
|
+
*/
|
|
511
|
+
export function formatResultMessage(proposal, result) {
|
|
512
|
+
const statusEmoji = result.decision === 'approved' ? '✅' :
|
|
513
|
+
result.decision === 'rejected' ? '❌' : '⏳';
|
|
514
|
+
const lines = [
|
|
515
|
+
`${statusEmoji} **CONSENSUS RESULT: ${proposal.title}**`,
|
|
516
|
+
`Decision: ${result.decision.toUpperCase()}`,
|
|
517
|
+
`Participation: ${(result.participation * 100).toFixed(1)}%`,
|
|
518
|
+
'',
|
|
519
|
+
`Approve: ${result.approveWeight} | Reject: ${result.rejectWeight} | Abstain: ${result.abstainWeight}`,
|
|
520
|
+
];
|
|
521
|
+
if (result.nonVoters.length > 0) {
|
|
522
|
+
lines.push(`Non-voters: ${result.nonVoters.join(', ')}`);
|
|
523
|
+
}
|
|
524
|
+
return lines.join('\n');
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Parse a PROPOSE command from a relay message.
|
|
528
|
+
*
|
|
529
|
+
* Format:
|
|
530
|
+
* ```
|
|
531
|
+
* PROPOSE: Title of the proposal
|
|
532
|
+
* TYPE: majority|supermajority|unanimous|weighted|quorum
|
|
533
|
+
* PARTICIPANTS: Agent1, Agent2, Agent3
|
|
534
|
+
* DESCRIPTION: Detailed description of what is being proposed
|
|
535
|
+
* TIMEOUT: 3600000 (optional, in milliseconds)
|
|
536
|
+
* QUORUM: 3 (optional, minimum votes)
|
|
537
|
+
* THRESHOLD: 0.67 (optional, for supermajority)
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
export function parseProposalCommand(message) {
|
|
541
|
+
// Check if message starts with PROPOSE:
|
|
542
|
+
if (!message.trim().startsWith('PROPOSE:')) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const lines = message.split('\n').map(line => line.trim());
|
|
546
|
+
// Parse each field
|
|
547
|
+
let title;
|
|
548
|
+
let description;
|
|
549
|
+
let participants;
|
|
550
|
+
let consensusType = 'majority';
|
|
551
|
+
let timeoutMs;
|
|
552
|
+
let quorum;
|
|
553
|
+
let threshold;
|
|
554
|
+
let inDescription = false;
|
|
555
|
+
const descriptionLines = [];
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
if (line.startsWith('PROPOSE:')) {
|
|
558
|
+
title = line.substring('PROPOSE:'.length).trim();
|
|
559
|
+
inDescription = false;
|
|
560
|
+
}
|
|
561
|
+
else if (line.startsWith('TYPE:')) {
|
|
562
|
+
const type = line.substring('TYPE:'.length).trim().toLowerCase();
|
|
563
|
+
if (['majority', 'supermajority', 'unanimous', 'weighted', 'quorum'].includes(type)) {
|
|
564
|
+
consensusType = type;
|
|
565
|
+
}
|
|
566
|
+
inDescription = false;
|
|
567
|
+
}
|
|
568
|
+
else if (line.startsWith('PARTICIPANTS:')) {
|
|
569
|
+
const participantStr = line.substring('PARTICIPANTS:'.length).trim();
|
|
570
|
+
participants = participantStr.split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
571
|
+
inDescription = false;
|
|
572
|
+
}
|
|
573
|
+
else if (line.startsWith('DESCRIPTION:')) {
|
|
574
|
+
description = line.substring('DESCRIPTION:'.length).trim();
|
|
575
|
+
inDescription = true;
|
|
576
|
+
}
|
|
577
|
+
else if (line.startsWith('TIMEOUT:')) {
|
|
578
|
+
const val = parseInt(line.substring('TIMEOUT:'.length).trim(), 10);
|
|
579
|
+
if (!isNaN(val) && val > 0) {
|
|
580
|
+
timeoutMs = val;
|
|
581
|
+
}
|
|
582
|
+
inDescription = false;
|
|
583
|
+
}
|
|
584
|
+
else if (line.startsWith('QUORUM:')) {
|
|
585
|
+
const val = parseInt(line.substring('QUORUM:'.length).trim(), 10);
|
|
586
|
+
if (!isNaN(val) && val > 0) {
|
|
587
|
+
quorum = val;
|
|
588
|
+
}
|
|
589
|
+
inDescription = false;
|
|
590
|
+
}
|
|
591
|
+
else if (line.startsWith('THRESHOLD:')) {
|
|
592
|
+
const val = parseFloat(line.substring('THRESHOLD:'.length).trim());
|
|
593
|
+
if (!isNaN(val) && val > 0 && val <= 1) {
|
|
594
|
+
threshold = val;
|
|
595
|
+
}
|
|
596
|
+
inDescription = false;
|
|
597
|
+
}
|
|
598
|
+
else if (inDescription && line.length > 0) {
|
|
599
|
+
// Continue collecting description lines
|
|
600
|
+
descriptionLines.push(line);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Append continuation lines to description
|
|
604
|
+
if (descriptionLines.length > 0 && description) {
|
|
605
|
+
description = description + '\n' + descriptionLines.join('\n');
|
|
606
|
+
}
|
|
607
|
+
// Validate required fields
|
|
608
|
+
if (!title || !participants || participants.length === 0) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
// Default description if not provided
|
|
612
|
+
if (!description) {
|
|
613
|
+
description = title;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
title,
|
|
617
|
+
description,
|
|
618
|
+
participants,
|
|
619
|
+
consensusType,
|
|
620
|
+
timeoutMs,
|
|
621
|
+
quorum,
|
|
622
|
+
threshold,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Check if a message is a consensus command (PROPOSE or VOTE).
|
|
627
|
+
*/
|
|
628
|
+
export function isConsensusCommand(message) {
|
|
629
|
+
const trimmed = message.trim();
|
|
630
|
+
return trimmed.startsWith('PROPOSE:') || /^VOTE\s+/i.test(trimmed);
|
|
631
|
+
}
|
|
632
|
+
//# sourceMappingURL=consensus.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DeliverEnvelope, AckPayload, Envelope } from '../protocol/types.js';
|
|
2
|
+
import type { StorageAdapter } from '../storage/adapter.js';
|
|
3
|
+
export interface DeliveryReliabilityOptions {
|
|
4
|
+
/** How long to wait for an ACK before retrying (ms) */
|
|
5
|
+
ackTimeoutMs: number;
|
|
6
|
+
/** Maximum attempts (initial send counts as attempt 1) */
|
|
7
|
+
maxAttempts: number;
|
|
8
|
+
/** How long to keep retrying before dropping (ms) */
|
|
9
|
+
deliveryTtlMs: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_DELIVERY_OPTIONS: DeliveryReliabilityOptions;
|
|
12
|
+
export interface DeliveryTrackerConnection {
|
|
13
|
+
id: string;
|
|
14
|
+
send(envelope: DeliverEnvelope): boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class DeliveryTracker {
|
|
17
|
+
private pendingDeliveries;
|
|
18
|
+
private deliveryOptions;
|
|
19
|
+
private storage?;
|
|
20
|
+
private getConnection;
|
|
21
|
+
constructor(options: {
|
|
22
|
+
storage?: StorageAdapter;
|
|
23
|
+
delivery?: Partial<DeliveryReliabilityOptions>;
|
|
24
|
+
getConnection: (id: string) => DeliveryTrackerConnection | undefined;
|
|
25
|
+
});
|
|
26
|
+
get pendingCount(): number;
|
|
27
|
+
track(target: DeliveryTrackerConnection, deliver: DeliverEnvelope): void;
|
|
28
|
+
handleAck(connectionId: string, ackId: string): void;
|
|
29
|
+
clearPendingForConnection(connectionId: string): void;
|
|
30
|
+
private scheduleRetry;
|
|
31
|
+
private markFailed;
|
|
32
|
+
}
|
|
33
|
+
export type AckEnvelope = Envelope<AckPayload>;
|
|
34
|
+
//# sourceMappingURL=delivery-tracker.d.ts.map
|