|
14 | 14 | from aethermesh_core.dispatch import dispatch_local_batch |
15 | 15 | from aethermesh_core.flow_audit import FlowAuditError, audit_local_flow |
16 | 16 | from aethermesh_core.identity import IdentityPersistenceError, load_or_create_identity |
17 | | -from aethermesh_core.job_manifest import ManifestError, load_job_manifest |
| 17 | +from aethermesh_core.job_manifest import ( |
| 18 | + ManifestError, |
| 19 | + load_job_manifest, |
| 20 | + load_job_manifest_jobs, |
| 21 | +) |
18 | 22 | from aethermesh_core.ledger import ( |
19 | 23 | ContributionLedger, |
20 | 24 | LedgerPersistenceError, |
|
48 | 52 | load_node_processing_state, |
49 | 53 | save_node_processing_state, |
50 | 54 | ) |
51 | | -from aethermesh_core.peer_registry import PeerRegistryError, peer_summary_document |
| 55 | +from aethermesh_core.peer_registry import ( |
| 56 | + PeerRegistryError, |
| 57 | + peer_summary_document, |
| 58 | + scheduled_nodes_from_heartbeat_log, |
| 59 | +) |
52 | 60 | from aethermesh_core.receipts import ( |
53 | 61 | ReceiptPersistenceError, |
54 | 62 | build_receipt_document, |
@@ -144,6 +152,26 @@ def build_parser() -> argparse.ArgumentParser: |
144 | 152 | help="Path to write the version 1 assignment-only local message log.", |
145 | 153 | ) |
146 | 154 |
|
| 155 | + peer_dispatch = subcommands.add_parser( |
| 156 | + "dispatch-local-peers", |
| 157 | + help="Write local dispatch messages using heartbeat-discovered peers.", |
| 158 | + ) |
| 159 | + peer_dispatch.add_argument( |
| 160 | + "--peer-message-log-path", |
| 161 | + required=True, |
| 162 | + help="Path to an existing version 1 message log with node heartbeats.", |
| 163 | + ) |
| 164 | + peer_dispatch.add_argument( |
| 165 | + "--manifest", |
| 166 | + required=True, |
| 167 | + help="Path to a version 1 local job manifest whose jobs should be dispatched.", |
| 168 | + ) |
| 169 | + peer_dispatch.add_argument( |
| 170 | + "--message-log-path", |
| 171 | + required=True, |
| 172 | + help="Path to write the version 1 assignment-only local message log.", |
| 173 | + ) |
| 174 | + |
147 | 175 | local_flow = subcommands.add_parser( |
148 | 176 | "run-local-flow", |
149 | 177 | help="Run dispatch plus all available local worker inboxes for a manifest.", |
@@ -431,6 +459,36 @@ def dispatch_local_batch_command( |
431 | 459 | return dispatch.to_dict() |
432 | 460 |
|
433 | 461 |
|
| 462 | +def dispatch_local_peers_command( |
| 463 | + peer_message_log_path: str, |
| 464 | + manifest_path: str, |
| 465 | + message_log_path: str, |
| 466 | +) -> dict[str, object]: |
| 467 | + """Dispatch manifest jobs to nodes discovered from heartbeat messages.""" |
| 468 | + |
| 469 | + nodes = scheduled_nodes_from_heartbeat_log(peer_message_log_path) |
| 470 | + jobs = load_job_manifest_jobs(manifest_path) |
| 471 | + dispatch = dispatch_local_batch( |
| 472 | + manifest_path=manifest_path, |
| 473 | + message_log_path=message_log_path, |
| 474 | + nodes=nodes, |
| 475 | + jobs=jobs, |
| 476 | + ) |
| 477 | + message_log_document = build_dispatch_message_log_document( |
| 478 | + messages=dispatch.messages, |
| 479 | + jobs=dispatch.jobs, |
| 480 | + nodes=dispatch.nodes, |
| 481 | + assignments=dispatch.assignments, |
| 482 | + manifest_path=manifest_path, |
| 483 | + source="dispatch-local-peers", |
| 484 | + ) |
| 485 | + write_message_log(message_log_path, message_log_document) |
| 486 | + payload = dispatch.to_dict() |
| 487 | + payload["command"] = "dispatch-local-peers" |
| 488 | + payload["peer_message_log_path"] = peer_message_log_path |
| 489 | + return payload |
| 490 | + |
| 491 | + |
434 | 492 | def summarize_ledger(ledger_path: str) -> dict[str, object]: |
435 | 493 | """Load an existing ledger and return read-only aggregate totals.""" |
436 | 494 |
|
@@ -837,6 +895,20 @@ def main(argv: Sequence[str] | None = None) -> int: |
837 | 895 | print(json.dumps(payload, sort_keys=True)) |
838 | 896 | return 0 |
839 | 897 |
|
| 898 | + if args.command == "dispatch-local-peers": |
| 899 | + try: |
| 900 | + payload = dispatch_local_peers_command( |
| 901 | + args.peer_message_log_path, args.manifest, args.message_log_path |
| 902 | + ) |
| 903 | + except (ManifestError, PeerRegistryError, MessageLogPersistenceError) as exc: |
| 904 | + print(f"error: {exc}", file=sys.stderr) |
| 905 | + return 1 |
| 906 | + except ValueError as exc: |
| 907 | + print(f"error: local peer dispatch failed: {exc}", file=sys.stderr) |
| 908 | + return 1 |
| 909 | + print(json.dumps(payload, sort_keys=True)) |
| 910 | + return 0 |
| 911 | + |
840 | 912 | if args.command == "run-local-flow": |
841 | 913 | try: |
842 | 914 | payload = run_local_flow(args.manifest, args.output_dir) |
|
0 commit comments