@feltdb/core 0.7.2 → 0.7.4

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.
Files changed (96) hide show
  1. package/dist/application-development.d.ts +76 -0
  2. package/dist/application-development.d.ts.map +1 -0
  3. package/dist/application-development.js +137 -0
  4. package/dist/application-manifest.d.ts +5 -1
  5. package/dist/application-manifest.d.ts.map +1 -1
  6. package/dist/application-manifest.js +10 -3
  7. package/dist/canonical-application.d.ts +26 -0
  8. package/dist/canonical-application.d.ts.map +1 -0
  9. package/dist/canonical-application.js +45 -0
  10. package/dist/cli/ai-model-config.js +97 -0
  11. package/dist/cli/commands.js +485 -30
  12. package/dist/cli/index.js +1 -1
  13. package/dist/cli/proposal-client.js +54 -0
  14. package/dist/cli/publish-engine.js +162 -0
  15. package/dist/cli/source-sync.js +139 -0
  16. package/dist/create/cli.js +1 -1
  17. package/dist/create/create.js +30 -6
  18. package/dist/create/managed-account.js +14 -7
  19. package/dist/create/package-versions.js +1 -1
  20. package/dist/create/server-source/crates/feltdb/src/application.rs +31 -1
  21. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +847 -0
  22. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +338 -29
  23. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  24. package/dist/create/server-source/crates/feltdb/src/lib.rs +1 -0
  25. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +46 -3
  26. package/dist/create/server-source/crates/feltdb-server/src/main.rs +315 -16
  27. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +20 -0
  28. package/dist/create/template/default-project/README.md +57 -0
  29. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  30. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  31. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  32. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  33. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  34. package/dist/create/template/default-project/feltdb.flow +169 -0
  35. package/dist/create/template/default-project/src/App.tsx +20 -0
  36. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  37. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  38. package/dist/create/template/default-project/src/index.tsx +14 -0
  39. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  40. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  41. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  42. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  43. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  44. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  45. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  46. package/dist/create/template/default-project/src/styles-application.css +21 -0
  47. package/dist/create/template/default-project/src/styles.css +1 -0
  48. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  49. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  50. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  51. package/dist/db.d.ts +49 -1
  52. package/dist/db.d.ts.map +1 -1
  53. package/dist/db.js +77 -0
  54. package/dist/error-codes.d.ts +20 -1
  55. package/dist/error-codes.d.ts.map +1 -1
  56. package/dist/error-codes.js +25 -0
  57. package/dist/flowspec.d.ts.map +1 -1
  58. package/dist/flowspec.js +27 -6
  59. package/dist/http-db.d.ts +15 -2
  60. package/dist/http-db.d.ts.map +1 -1
  61. package/dist/http-db.js +113 -5
  62. package/dist/index-core.d.ts +3 -0
  63. package/dist/index-core.d.ts.map +1 -1
  64. package/dist/index-core.js +3 -0
  65. package/dist/proposal.d.ts +85 -0
  66. package/dist/proposal.d.ts.map +1 -0
  67. package/dist/proposal.js +105 -0
  68. package/dist/studio/app.d.ts +5 -1
  69. package/dist/studio/app.d.ts.map +1 -1
  70. package/dist/studio/components/ApplicationDesigner.d.ts +10 -1
  71. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  72. package/dist/studio/components/AskFeltDB.d.ts +20 -0
  73. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  74. package/dist/studio/components/ProposalBrowser.d.ts +3 -0
  75. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  76. package/dist/studio/components/index.d.ts +2 -0
  77. package/dist/studio/components/index.d.ts.map +1 -1
  78. package/dist/studio/components/index.js +4 -4
  79. package/dist/studio/{components-C5p2TfIU.js → components-Bm4sjhcX.js} +531 -192
  80. package/dist/studio/index.js +184 -172
  81. package/dist/studio/studio.css +1 -1
  82. package/dist/studio-app/assets/dist-DA1MYSzp.js +1 -0
  83. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-B-Ccgccb.js} +1 -1
  84. package/dist/studio-app/assets/feltdb_wasm_bg-wH3enJIA.wasm +0 -0
  85. package/dist/studio-app/assets/{index-C1GWyazR.css → index-BRrmvaKw.css} +1 -1
  86. package/dist/studio-app/assets/index-CEIqowCh.js +42 -0
  87. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  88. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  89. package/dist/studio-app/index.html +2 -2
  90. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  91. package/dist/workspace/local-development-authority.d.ts +3 -0
  92. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  93. package/dist/workspace/local-development-authority.js +62 -1
  94. package/package.json +1 -1
  95. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  96. package/dist/studio-app/assets/index-C71X92EK.js +0 -28
@@ -45,14 +45,17 @@
45
45
  //! shutdown exit cleanly
46
46
  //! ```
47
47
 
48
+ use feltdb::authority_failover::{AuthorityStore, ClaimResult, FencingToken, Heartbeat};
48
49
  use feltdb::convergence::VectorClock;
49
50
  use feltdb::distributed_transactions::{
50
51
  CausalCapacity, DistributedTransactionExecutor, ReceiveOutcome, ReplicationMessage,
51
52
  SubmitOutcome,
52
53
  };
53
- use feltdb::replication_protocol::ProtocolTransport;
54
- use feltdb::replica_acknowledgements::{AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement};
54
+ use feltdb::replica_acknowledgements::{
55
+ AcknowledgementStore, ReclamationEvidence, ReplicaAcknowledgement,
56
+ };
55
57
  use feltdb::replica_membership::MembershipStore;
58
+ use feltdb::replication_protocol::ProtocolTransport;
56
59
  use feltdb::state_hash::StateHash;
57
60
  use feltdb::tcp_transport::TcpTransport;
58
61
  use feltdb::transactions::{
@@ -126,7 +129,11 @@ fn deliver(
126
129
  Ok(ReceiveOutcome::AlreadyKnown) => {
127
130
  json!({ "envelope": key, "outcome": "already_known" })
128
131
  }
129
- Ok(ReceiveOutcome::CapacityDeferred { missing, pending_entries, pending_bytes }) => {
132
+ Ok(ReceiveOutcome::CapacityDeferred {
133
+ missing,
134
+ pending_entries,
135
+ pending_bytes,
136
+ }) => {
130
137
  // Durable, but not buffered. The caller's obligation is to stop
131
138
  // reading from this peer; the envelope is admitted from the log
132
139
  // once the backlog drains.
@@ -176,16 +183,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
176
183
  let mut recv_specs: Vec<(String, String)> = Vec::new();
177
184
  let mut max_pending_entries: Option<usize> = None;
178
185
  let mut max_pending_bytes: Option<usize> = None;
186
+ let mut authority_priority: u32 = 0;
187
+ let mut heartbeat_interval_ms: u64 = 1_000;
188
+ let mut heartbeat_failure_threshold: u32 = 5;
179
189
 
180
190
  let mut index = 1;
181
191
  while index < args.len() {
182
192
  match args[index].as_str() {
183
193
  "--node-id" => {
184
194
  index += 1;
185
- node_id = args
186
- .get(index)
187
- .ok_or("--node-id requires a value")?
188
- .clone();
195
+ node_id = args.get(index).ok_or("--node-id requires a value")?.clone();
189
196
  }
190
197
  "--data" => {
191
198
  index += 1;
@@ -195,11 +202,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
195
202
  }
196
203
  "--send" => {
197
204
  index += 1;
198
- send_specs.push(parse_link(args.get(index).ok_or("--send requires PEER=ADDR")?)?);
205
+ send_specs.push(parse_link(
206
+ args.get(index).ok_or("--send requires PEER=ADDR")?,
207
+ )?);
199
208
  }
200
209
  "--recv" => {
201
210
  index += 1;
202
- recv_specs.push(parse_link(args.get(index).ok_or("--recv requires PEER=ADDR")?)?);
211
+ recv_specs.push(parse_link(
212
+ args.get(index).ok_or("--recv requires PEER=ADDR")?,
213
+ )?);
203
214
  }
204
215
  // The bound is settable so an acceptance test can reach it through
205
216
  // the production path. Filling the shipping default would mean
@@ -225,6 +236,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
225
236
  .map_err(|e| format!("--max-pending-bytes: {e}"))?,
226
237
  );
227
238
  }
239
+ // Static priority for authority election. Higher priority nodes are
240
+ // preferred during failover when multiple candidates have equal state.
241
+ "--authority-priority" => {
242
+ index += 1;
243
+ authority_priority = args
244
+ .get(index)
245
+ .ok_or("--authority-priority requires a value")?
246
+ .parse::<u32>()
247
+ .map_err(|e| format!("--authority-priority: {e}"))?;
248
+ }
249
+ "--heartbeat-interval-ms" => {
250
+ index += 1;
251
+ heartbeat_interval_ms = args
252
+ .get(index)
253
+ .ok_or("--heartbeat-interval-ms requires a value")?
254
+ .parse::<u64>()
255
+ .map_err(|e| format!("--heartbeat-interval-ms: {e}"))?;
256
+ }
257
+ "--heartbeat-failure-threshold" => {
258
+ index += 1;
259
+ heartbeat_failure_threshold = args
260
+ .get(index)
261
+ .ok_or("--heartbeat-failure-threshold requires a value")?
262
+ .parse::<u32>()
263
+ .map_err(|e| format!("--heartbeat-failure-threshold: {e}"))?;
264
+ }
228
265
  other => return Err(format!("unknown argument {other}").into()),
229
266
  }
230
267
  index += 1;
@@ -265,9 +302,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
265
302
  .map_err(|e| e.to_string())?,
266
303
  ));
267
304
 
305
+ // Authority state tracks who holds write authority in the cluster. The store
306
+ // is durable and survives restarts, so a node that crashes can rejoin and
307
+ // recover its previous role. The cluster ID is set by the first `authority-claim`
308
+ // command, so it starts as the node's own ID.
309
+ let authority = Arc::new(Mutex::new(
310
+ AuthorityStore::open_with_failure_detector(
311
+ data_dir
312
+ .as_ref()
313
+ .map(|dir| dir.join("authority.json"))
314
+ .unwrap_or_else(|| PathBuf::from("authority.json")),
315
+ &node_id, // cluster_id defaults to node_id until set
316
+ &node_id,
317
+ authority_priority,
318
+ heartbeat_interval_ms,
319
+ heartbeat_failure_threshold,
320
+ )
321
+ .map_err(|e| e.to_string())?,
322
+ ));
323
+
268
324
  let initial_state = StateHash::from_hex("0".repeat(64));
269
- let mut executor =
270
- DistributedTransactionExecutor::with_log(node_id.clone(), initial_state.clone(), log_path.clone())?;
325
+ let mut executor = DistributedTransactionExecutor::with_log(
326
+ node_id.clone(),
327
+ initial_state.clone(),
328
+ log_path.clone(),
329
+ )?;
271
330
 
272
331
  // The bound is set before recovery, not after, because recovery restores
273
332
  // the backlog and has to respect the same limit the live path does.
@@ -309,7 +368,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
309
368
  // missed an operation observably divergent. It lives beside the executor
310
369
  // under one lock so a local commit and an arriving replication cannot
311
370
  // interleave and chain onto a parent that is already stale.
312
- let core = Arc::new(Mutex::new((executor, StateVersion::new(VectorClock::new(), "0".repeat(64)))));
371
+ let recovered_version = executor
372
+ .get_replica_state(&node_id)
373
+ .map(|replica| StateVersion::new(replica.vector_clock, replica.state_hash.to_string()))
374
+ .unwrap_or_else(|| StateVersion::new(VectorClock::new(), "0".repeat(64)));
375
+ let core = Arc::new(Mutex::new((executor, recovered_version)));
313
376
  let should_exit = Arc::new(AtomicBool::new(false));
314
377
  // Per-peer delivery hold.
315
378
  //
@@ -338,7 +401,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
338
401
  .into_iter()
339
402
  .map(|(peer, addr)| {
340
403
  let transport = TcpTransport::new("127.0.0.1:0".to_string());
341
- SendLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
404
+ SendLink {
405
+ peer,
406
+ addr,
407
+ control: Arc::new(Mutex::new(transport.clone())),
408
+ io: transport,
409
+ }
342
410
  })
343
411
  .collect();
344
412
 
@@ -346,7 +414,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
346
414
  .into_iter()
347
415
  .map(|(peer, addr)| {
348
416
  let transport = TcpTransport::new(addr.clone());
349
- RecvLink { peer, addr, control: Arc::new(Mutex::new(transport.clone())), io: transport }
417
+ RecvLink {
418
+ peer,
419
+ addr,
420
+ control: Arc::new(Mutex::new(transport.clone())),
421
+ io: transport,
422
+ }
350
423
  })
351
424
  .collect();
352
425
 
@@ -480,8 +553,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
480
553
  };
481
554
 
482
555
  match verb {
483
- "execute-tx" => {
484
- let record = if argument.is_empty() { "item".to_string() } else { argument };
556
+ "execute-tx" | "authority-write" => {
557
+ let record = if verb == "authority-write" {
558
+ let mut parts = argument.splitn(2, ' ');
559
+ let token = parts.next().and_then(|value| value.parse::<u64>().ok());
560
+ let record = parts.next().unwrap_or("item").to_string();
561
+ let validation = match token {
562
+ Some(token) => authority
563
+ .lock()
564
+ .await
565
+ .validate_quorum_write(FencingToken(token), 2),
566
+ None => Err(feltdb::authority_failover::AuthorityError::StaleToken {
567
+ provided: FencingToken(0),
568
+ current: authority.lock().await.state().current_token(),
569
+ }),
570
+ };
571
+ if let Err(error) = validation {
572
+ println!(
573
+ "AUTHORITY_WRITE_REJECTED {}",
574
+ json!({ "record": record, "reason": error.to_string() })
575
+ );
576
+ io::stdout().flush().ok();
577
+ continue;
578
+ }
579
+ record
580
+ } else if argument.is_empty() {
581
+ "item".to_string()
582
+ } else {
583
+ argument
584
+ };
485
585
 
486
586
  let mut fields = HashMap::new();
487
587
  fields.insert("record".to_string(), json!(record.clone()));
@@ -825,7 +925,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
825
925
  let (frontier, marks) = {
826
926
  let guard = core.lock().await;
827
927
  let (executor, _) = &*guard;
828
- (executor.causal_frontier().clone(), executor.origin_sequence_high_water())
928
+ (
929
+ executor.causal_frontier().clone(),
930
+ executor.origin_sequence_high_water(),
931
+ )
829
932
  };
830
933
  let view: Vec<String> = membership
831
934
  .get_members()
@@ -1054,27 +1157,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1054
1157
  "list-operations" => {
1055
1158
  let guard = core.lock().await;
1056
1159
  let (executor, _) = &*guard;
1057
-
1160
+
1058
1161
  // Get all operations from the log
1059
1162
  let mut all_ops = Vec::new();
1060
1163
  let mut applied_ops = Vec::new();
1061
1164
  let pending_ops = executor.pending_causal_keys();
1062
1165
  let deferred_ops = executor.deferred_causal_keys();
1063
-
1166
+
1064
1167
  if let Some(ref log) = executor.operation_log {
1065
1168
  if let Ok(envelopes) = log.load_all() {
1066
1169
  for env in &envelopes {
1067
- let key = format!("{}:{}", env.envelope_id.originating_node, env.envelope_id.sequence);
1170
+ let key = format!(
1171
+ "{}:{}",
1172
+ env.envelope_id.originating_node, env.envelope_id.sequence
1173
+ );
1068
1174
  all_ops.push(key.clone());
1069
1175
  }
1070
1176
  }
1071
1177
  }
1072
-
1178
+
1073
1179
  // Get applied operations from the barrier
1074
1180
  for key in executor.causal_barrier.applied_keys() {
1075
1181
  applied_ops.push(key.to_string());
1076
1182
  }
1077
-
1183
+
1078
1184
  println!(
1079
1185
  "OPERATIONS {}",
1080
1186
  json!({
@@ -1091,23 +1197,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1091
1197
  "operation-inventory" => {
1092
1198
  let guard = core.lock().await;
1093
1199
  let (executor, _) = &*guard;
1094
-
1200
+
1095
1201
  // Get all operations from the log
1096
1202
  let mut operations = json!({
1097
1203
  "applied": [],
1098
1204
  "pending": [],
1099
1205
  "deferred": [],
1100
1206
  });
1101
-
1207
+
1102
1208
  if let Some(ref log) = executor.operation_log {
1103
1209
  if let Ok(envelopes) = log.load_all() {
1104
1210
  let applied_keys = executor.causal_barrier.applied_keys();
1105
1211
  let pending_keys = executor.pending_causal_keys();
1106
1212
  let deferred_keys = executor.deferred_causal_keys();
1107
-
1213
+
1108
1214
  for env in &envelopes {
1109
- let key = format!("{}:{}", env.envelope_id.originating_node, env.envelope_id.sequence);
1110
-
1215
+ let key = format!(
1216
+ "{}:{}",
1217
+ env.envelope_id.originating_node, env.envelope_id.sequence
1218
+ );
1219
+
1111
1220
  if applied_keys.contains(&key) {
1112
1221
  operations["applied"].as_array_mut().unwrap().push(json!({
1113
1222
  "id": key,
@@ -1130,7 +1239,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1130
1239
  }
1131
1240
  }
1132
1241
  }
1133
-
1242
+
1134
1243
  println!("INVENTORY {}", operations);
1135
1244
  io::stdout().flush().ok();
1136
1245
  }
@@ -1147,6 +1256,203 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1147
1256
  io::stdout().flush().ok();
1148
1257
  }
1149
1258
 
1259
+ // Authority failover commands.
1260
+ //
1261
+ // These commands implement the distributed authority protocol that prevents
1262
+ // split-brain and allows automatic failover when the authority node fails.
1263
+
1264
+ // Claim authority for this node. This generates a new fencing token that
1265
+ // supersedes any previous authority. The claim succeeds only if no other
1266
+ // node has a higher token and appears healthy.
1267
+ "authority-claim" => {
1268
+ let mut guard = authority.lock().await;
1269
+ let result = guard.claim_authority();
1270
+ match result {
1271
+ Ok(ClaimResult::Granted { token }) => {
1272
+ println!(
1273
+ "AUTHORITY_CLAIMED {}",
1274
+ json!({
1275
+ "node": node_id,
1276
+ "fencing_token": token.0,
1277
+ "role": "AUTHORITY",
1278
+ })
1279
+ );
1280
+ }
1281
+ Ok(ClaimResult::Rejected {
1282
+ current_authority,
1283
+ current_token,
1284
+ }) => {
1285
+ println!(
1286
+ "AUTHORITY_REJECTED {}",
1287
+ json!({
1288
+ "current_authority": current_authority,
1289
+ "current_token": current_token.0,
1290
+ "reason": "another authority is still active",
1291
+ })
1292
+ );
1293
+ }
1294
+ Ok(ClaimResult::Fenced { token }) => {
1295
+ println!(
1296
+ "AUTHORITY_FENCED {}",
1297
+ json!({
1298
+ "node": node_id,
1299
+ "fencing_token": token.0,
1300
+ "reason": "this node is fenced",
1301
+ })
1302
+ );
1303
+ }
1304
+ Err(e) => {
1305
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1306
+ }
1307
+ }
1308
+ io::stdout().flush().ok();
1309
+ }
1310
+
1311
+ // Accept a new authority announced by another node.
1312
+ // Format: authority-accept NODE_ID TOKEN
1313
+ "authority-accept" => {
1314
+ let mut parts = argument.split_whitespace();
1315
+ let authority_node = parts.next().unwrap_or_default().to_string();
1316
+ let token_value: u64 = parts.next().unwrap_or("0").parse().unwrap_or(0);
1317
+ let token = FencingToken(token_value);
1318
+
1319
+ let mut guard = authority.lock().await;
1320
+ let result = guard.accept_authority(&authority_node, token);
1321
+ match result {
1322
+ Ok(()) => {
1323
+ let state = guard.state();
1324
+ println!(
1325
+ "AUTHORITY_ACCEPTED {}",
1326
+ json!({
1327
+ "authority_node": authority_node,
1328
+ "fencing_token": token.0,
1329
+ "local_role": format!("{:?}", state.local_role),
1330
+ })
1331
+ );
1332
+ }
1333
+ Err(e) => {
1334
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1335
+ }
1336
+ }
1337
+ io::stdout().flush().ok();
1338
+ }
1339
+
1340
+ // Query the current authority state.
1341
+ "authority-state" => {
1342
+ let guard = authority.lock().await;
1343
+ let state = guard.state();
1344
+ let operations_applied = {
1345
+ let core_guard = core.lock().await;
1346
+ let (executor, _) = &*core_guard;
1347
+ executor
1348
+ .get_replica_state(&node_id)
1349
+ .map(|r| r.operations_applied)
1350
+ .unwrap_or(0)
1351
+ };
1352
+ println!(
1353
+ "AUTHORITY {}",
1354
+ json!({
1355
+ "cluster_id": state.cluster_id,
1356
+ "local_node": state.local_node_id,
1357
+ "local_role": format!("{:?}", state.local_role),
1358
+ "is_authority": state.is_authority(),
1359
+ "is_fenced": state.is_fenced(),
1360
+ "authority_node": state.authority_node(),
1361
+ "current_token": state.current_token().0,
1362
+ "highest_seen_token": state.highest_seen_token.0,
1363
+ "operations_applied": operations_applied,
1364
+ })
1365
+ );
1366
+ io::stdout().flush().ok();
1367
+ }
1368
+
1369
+ // Record a heartbeat from a peer. This is used for failure detection.
1370
+ // Format: heartbeat NODE_ID [TOKEN] [OPS_APPLIED] [IS_AUTHORITY]
1371
+ "heartbeat" => {
1372
+ let mut parts = argument.split_whitespace();
1373
+ let from_node = parts.next().unwrap_or_default().to_string();
1374
+
1375
+ if from_node.is_empty() {
1376
+ println!(
1377
+ "ERROR {}",
1378
+ json!({ "reason": "heartbeat requires NODE_ID" })
1379
+ );
1380
+ } else {
1381
+ let mut guard = authority.lock().await;
1382
+ guard.heartbeat(&from_node);
1383
+ println!(
1384
+ "HEARTBEAT_RECORDED {}",
1385
+ json!({
1386
+ "from_node": from_node,
1387
+ })
1388
+ );
1389
+ }
1390
+ io::stdout().flush().ok();
1391
+ }
1392
+
1393
+ // Check if the current authority has failed based on heartbeat timeout.
1394
+ "authority-failed" => {
1395
+ let guard = authority.lock().await;
1396
+ let failed = guard.authority_failed();
1397
+ let failed_nodes = guard.failed_nodes();
1398
+ let state = guard.state();
1399
+ println!(
1400
+ "AUTHORITY_CHECK {}",
1401
+ json!({
1402
+ "authority_failed": failed,
1403
+ "current_authority": state.authority_node(),
1404
+ "failed_nodes": failed_nodes,
1405
+ })
1406
+ );
1407
+ io::stdout().flush().ok();
1408
+ }
1409
+
1410
+ // Step down from authority role voluntarily.
1411
+ "authority-stepdown" => {
1412
+ let mut guard = authority.lock().await;
1413
+ let was_authority = guard.state().is_authority();
1414
+ let result = guard.step_down();
1415
+ match result {
1416
+ Ok(()) => {
1417
+ println!(
1418
+ "AUTHORITY_STEPDOWN {}",
1419
+ json!({
1420
+ "node": node_id,
1421
+ "was_authority": was_authority,
1422
+ "now_role": format!("{:?}", guard.state().local_role),
1423
+ })
1424
+ );
1425
+ }
1426
+ Err(e) => {
1427
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1428
+ }
1429
+ }
1430
+ io::stdout().flush().ok();
1431
+ }
1432
+
1433
+ // Recover from fenced state after catching up.
1434
+ "authority-recover" => {
1435
+ let mut guard = authority.lock().await;
1436
+ let was_fenced = guard.state().is_fenced();
1437
+ let result = guard.recover_from_fenced();
1438
+ match result {
1439
+ Ok(()) => {
1440
+ println!(
1441
+ "AUTHORITY_RECOVERED {}",
1442
+ json!({
1443
+ "node": node_id,
1444
+ "was_fenced": was_fenced,
1445
+ "now_role": format!("{:?}", guard.state().local_role),
1446
+ })
1447
+ );
1448
+ }
1449
+ Err(e) => {
1450
+ println!("ERROR {}", json!({ "reason": e.to_string() }));
1451
+ }
1452
+ }
1453
+ io::stdout().flush().ok();
1454
+ }
1455
+
1150
1456
  "shutdown" => {
1151
1457
  should_exit.store(true, Ordering::Relaxed);
1152
1458
  println!("SHUTDOWN");
@@ -1156,7 +1462,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
1156
1462
  }
1157
1463
 
1158
1464
  _ => {
1159
- println!("ERROR {}", json!({ "reason": format!("unknown command {verb}") }));
1465
+ println!(
1466
+ "ERROR {}",
1467
+ json!({ "reason": format!("unknown command {verb}") })
1468
+ );
1160
1469
  io::stdout().flush().ok();
1161
1470
  }
1162
1471
  }