statelet-sdk 0.1.5

Rust gRPC client for Statelet distributed key-value store
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
// Copyright 2024 Statelet Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

syntax = "proto3";

package statelet.v1;

option java_outer_classname = "StateletProto";

// Statelet key-value store RPC service.
//
// All keys and values are raw bytes. Column families (cf) are identified by
// a uint32 id. Use USER_COLUMN_FAMILY_ID = 0 for the default user CF.
service Statelet {
  // Liveness check.
  rpc Ping(PingRequest) returns (PingResponse);

  // Write a single key-value pair.
  rpc Put(PutRequest) returns (PutResponse);

  // Read the value for a key. Returns found=false when the key does not exist.
  rpc Get(GetRequest) returns (GetResponse);

  // Delete a key.
  rpc Delete(DeleteRequest) returns (DeleteResponse);

  // Merge an operand into the existing value (requires a MergeOperator on the CF).
  rpc Merge(MergeRequest) returns (MergeResponse);

  // Atomically apply a batch of Put/Delete/Merge operations.
  rpc BatchWrite(BatchWriteRequest) returns (BatchWriteResponse);

  // Atomically apply one conditional gate mutation plus plain trailing
  // Put/Delete/Merge operations. The whole batch commits only when the gate
  // predicate holds at the shard leader.
  rpc ConditionalBatchWrite(ConditionalBatchWriteRequest) returns (ConditionalBatchWriteResponse);

  // Atomic set-if-(not-)exists (compare-and-swap on key presence) for a single
  // key. The existence check and the write commit as one indivisible step at
  // the owning shard leader, backing Redis `SET … NX/XX`, `SETNX` and `GETSET`
  // through the gateway.
  rpc ConditionalSet(ConditionalSetRequest) returns (ConditionalSetResponse);

  // Read multiple keys in a single round-trip.
  rpc BatchGet(BatchGetRequest) returns (BatchGetResponse);

  // ── Text embedding + search (gateway-side embedding) ────────────────────

  // Insert text: gateway embeds text, then stores KV metadata + vector.
  rpc TextPut(TextPutRequest) returns (TextPutResponse);

  // Search by text: gateway embeds query, runs vector search, hydrates KV metadata.
  rpc TextSearch(TextSearchRequest) returns (TextSearchResponse);

  // ── Text + Graph (gateway-side embedding + graph storage) ──────────────

  // Embed text → GraphAddNode (with vector + properties) + optional GraphAddEdge.
  rpc TextGraphPut(TextGraphPutRequest) returns (TextGraphPutResponse);

  // Embed query → GraphSearch → hydrate node properties.
  rpc TextGraphSearch(TextGraphSearchRequest) returns (TextGraphSearchResponse);

  // Query edges for a graph node (delegates to GraphQueryEdges).
  rpc TextGraphQueryEdges(TextGraphQueryEdgesRequest) returns (TextGraphQueryEdgesResponse);

  // Get a graph node's properties by ID.
  rpc TextGraphGetNode(TextGraphGetNodeRequest) returns (TextGraphGetNodeResponse);

  // ── Embedding primitive (gateway-only) ─────────────────────────────────
  //
  // Pure text→vector: embed each text with the gateway's resident dense model
  // and return the raw vectors, storing NOTHING. Lets a caller that owns its
  // own storage pipeline (its own node ids, properties, graph structure) get
  // statelet-local vectors and then write them via VectorPut / GraphAddNode —
  // instead of depending on an external embedding API or ceding node
  // construction to TextGraphPut. The model runs once, in the gateway.
  rpc Embed(EmbedRequest) returns (EmbedResponse);

  // ── Triple store (epic #1432) ──────────────────────────────────────────
  //
  // Write one (s, p, o, valid_from, valid_to, props?) triple into the
  // per-graph triple CF `t:{graph}` (provisioned as an ordinary `CfType::User`
  // CF). Terms are interned through the on-CF dictionary (T2ID/ID2T/META) and
  // the three permutations (SPO authoritative + POS/OSP index-only), the LIT
  // row (for a literal object), the dictionary rows and the bumped META
  // high-water are all written in ONE atomic `WriteBatch` (one Raft entry), so
  // the permutations and dictionary never diverge across a crash. Returns the
  // interned subject/predicate/object ids. Gateway-only (Phase 1 of #1432).
  rpc TriplePut(TriplePutRequest) returns (TriplePutResponse);

  // Query one bound/unbound triple pattern (s?, p?, o?, as_of?) against the
  // per-graph triple CF `t:{graph}`. The handler selects the SPO/POS/OSP index
  // whose leading columns are bound (the 6-pattern BGP table), `seek(prefix)`s
  // and iterates-while-prefix over the merged memtable+`.sst` view, applies the
  // inverted-`valid_from` newest-wins ordering plus the optional `as_of`
  // temporal filter, hides tombstoned triples, and resolves each TermId back to
  // its string through the on-CF ID2T dictionary. Gateway-only (Phase 2 of
  // #1432).
  rpc TripleQuery(TripleQueryRequest) returns (TripleQueryResponse);

  // Evaluate a basic graph pattern (BGP): a list of triple patterns sharing
  // variables. The handler runs a left-deep, selectivity-ordered
  // index-nested-loop join (INLJ) over the Phase-2 single-pattern primitive —
  // it orders patterns most-bound-first, binds variables left-to-right, and
  // evaluates each pattern as a Phase-2 prefix scan parameterized by the current
  // binding. Returns one row of variable→value bindings per solution. No
  // cost-based planner in v1 (documented limitation; structural selectivity
  // only). Gateway-only (Phase 3 of #1432).
  rpc TripleBgp(TripleBgpRequest) returns (TripleBgpResponse);

  // Cross-modal linkage between the vector index (HNSW/SpFresh) and the triple
  // store, exploiting the shared id space (term-id == node-id, no remap). Three
  // modes generalize the existing temporal_join pattern:
  //   1. VECTOR_TO_TRIPLE: HNSW.search(q,k) → node ids → SPO prefix-scan
  //      (id, P?, ?) to filter/re-rank by symbolic structure.
  //   2. TRIPLE_TO_VECTOR: BGP/single-pattern → ids → HNSW.search for
  //      similar-but-unconnected entities.
  //   3. VECTOR_GUIDED_KHOP: rank a triple frontier (k-hop expansion) by vector
  //      similarity, returning the top-N most semantically relevant neighbors.
  // Gateway-only (Phase 3 of #1432).
  rpc TripleLink(TripleLinkRequest) returns (TripleLinkResponse);


  // Resolve the conflict set containing a node: expand contradicts/gedge_rev
  // edges, then return the authoritative claim, the dissenting set, and the
  // policy rationale. Gateway-only; auditable "show consensus" endpoint.
  rpc ResolveConflict(ResolveConflictRequest) returns (ResolveConflictResponse);

  // (#828 LongMemEval Phase 5b) LLM-free entity-resolution candidate generation:
  // given a graph (+ optional query terms) run the blocking-then-scoring resolver
  // (alias rule + entity-mention ANN nearest-neighbor + lexical) and return the
  // candidate clusters (canonical → surfaces, with method+score). Gateway-only;
  // candidate primitive — transitive persistence is Phase 5c.
  rpc ResolveEntities(ResolveEntitiesRequest) returns (ResolveEntitiesResponse);

  // ── Vector index operations ──────────────────────────────────────────────

  // Create or reconfigure an HNSW vector index.
  rpc CreateVectorIndex(CreateVectorIndexRequest) returns (CreateVectorIndexResponse);

  // Drop an HNSW vector index.
  rpc DropVectorIndex(DropVectorIndexRequest) returns (DropVectorIndexResponse);

  // Insert or update a vector in the index.
  rpc VectorPut(VectorPutRequest) returns (VectorPutResponse);

  // Remove a vector from the index.
  rpc VectorDelete(VectorDeleteRequest) returns (VectorDeleteResponse);

  // Approximate nearest neighbor search.
  rpc VectorSearch(VectorSearchRequest) returns (VectorSearchResponse);

  // Retrieve a stored vector by id.
  rpc VectorGet(VectorGetRequest) returns (VectorGetResponse);

  // Batch insert vectors into the index.
  rpc VectorBatchPut(VectorBatchPutRequest) returns (VectorBatchPutResponse);

  // Batch delete vectors from the index.
  rpc VectorBatchDelete(VectorBatchDeleteRequest) returns (VectorBatchDeleteResponse);

  // Train quantization parameters (PQ codebooks, IVF centroids) for an index.
  rpc VectorTrain(VectorTrainRequest) returns (VectorTrainResponse);

  // Sample random vectors from a named index on this node (used for global training).
  rpc VectorSample(VectorSampleRequest) returns (VectorSampleResponse);

  // Export live (id, vector) pairs from a LEGACY independent-family index,
  // paginated by id. Retirement migration (P7) only; graph-backed and exempt
  // index types return FailedPrecondition.
  rpc VectorExport(VectorExportRequest) returns (VectorExportResponse);

  // Ingest sparse (term->weight) documents into a per-index inverted posting store.
  rpc SparseIngest(SparseIngestRequest) returns (SparseIngestResponse);

  // Hybrid dense + sparse retrieval with RRF or weighted fusion.
  rpc HybridSearch(HybridSearchRequest) returns (HybridSearchResponse);

  // Scan keys with an optional prefix filter. Returns a page of key-value pairs.
  rpc Scan(ScanRequest) returns (ScanResponse);

  // Delete all keys matching a prefix. Returns the number of keys deleted.
  rpc DeleteByPrefix(DeleteByPrefixRequest) returns (DeleteByPrefixResponse);

  // Collect per-shard / per-CF statistics from this data node.
  rpc GetNodeStats(GetNodeStatsRequest) returns (GetNodeStatsResponse);

  // Admin: checkpoint this data node — force-flush every hosted shard's state
  // machine, advance idle shards' WAL TRUNCATE floors, and GC reclaimable WAL
  // segments. Called after bulk ingest so a subsequent restart replays a small
  // WAL instead of the whole ingest burst (issue #754). Data-node only; the
  // gateway exposes it as POST /api/v1/admin/checkpoint fanning out per node.
  rpc Checkpoint(CheckpointRequest) returns (CheckpointResponse);

  // Admin: read this node's Hybrid Logical Clock (epic #1478, Phase 1). Returns
  // the current HLC reading plus whether cross-shard transactions are enabled,
  // so the clock that orders cross-shard commits is observable. Sampling the
  // clock advances it (it is a `now()`), so this is a lightweight admin probe,
  // not a hot-path RPC.
  rpc GetClusterClock(GetClusterClockRequest) returns (GetClusterClockResponse);

  // ── Agent State: data-node leaf operations ─────────────────────────────

  // Add a causal step (write props + content atomically).
  rpc AgentAddStep(AgentAddStepRequest) returns (AgentAddStepResponse);

  // Add a causal edge (forward + reverse).
  rpc AgentAddEdge(AgentAddEdgeRequest) returns (AgentAddEdgeResponse);

  // Get a causal step's metadata.
  rpc AgentGetStep(AgentGetStepRequest) returns (AgentGetStepResponse);

  // Get a causal step's content.
  rpc AgentGetContent(AgentGetContentRequest) returns (AgentGetContentResponse);

  // Get edges for a step (incoming or outgoing).
  rpc AgentGetEdges(AgentGetEdgesRequest) returns (AgentGetEdgesResponse);

  // Single-shard BFS traversal returning steps + edges from GraphSST.
  rpc AgentLocalTraverse(AgentLocalTraverseRequest) returns (AgentLocalTraverseResponse);

  // Compare-and-swap put.
  rpc AgentCasPut(AgentCasPutRequest) returns (AgentCasPutResponse);

  // Single-DBImpl optimistic transaction commit with snapshot-isolation
  // conflict detection. The client buffers reads (cf, key, observed_seq) and
  // writes (puts/deletes), then submits them in one call; the server takes
  // sharded key locks, re-validates the read-set against the latest seqs, and
  // atomically applies the writes (or aborts on conflict).
  rpc AgentTxnCommit(AgentTxnCommitRequest) returns (AgentTxnCommitResponse);

  // Coordination primitives. A claim is an atomic SetIfNotExists(claim_key,
  // agent_id); a lease adds a TTL so an un-renewed holder auto-expires; renew
  // and release are fenced so only the live holder can act. Issue #691.
  rpc AgentClaim(AgentClaimRequest) returns (AgentClaimResponse);
  rpc AgentLease(AgentLeaseRequest) returns (AgentLeaseResponse);
  rpc AgentRenew(AgentRenewRequest) returns (AgentRenewResponse);
  rpc AgentRelease(AgentReleaseRequest) returns (AgentReleaseResponse);

  // Cross-shard ACID transactions — Phase 2 (epic #1478): the internal
  // Percolator-style *prewrite*. Conditionally places a LockRecord intent on
  // lock/<cf>/<user_key> in the coordination CF and stages the provisional
  // value, aborting on a conflicting lock or a newer committed version. Gated
  // behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (returns FailedPrecondition
  // when the flag is unset); commit/roll-forward arrive in Phase 3.
  rpc AgentPrewrite(AgentPrewriteRequest) returns (AgentPrewriteResponse);

  // Cross-shard ACID transactions — Phase 3 (epic #1478): the internal commit
  // point + resolution drivers, used by the gateway coordinator. AgentCommitPrimary
  // is the single fence-gated CAS that flips the primary TxnStatus
  // Prewritten->Committed (or Aborted); AgentRollForward replaces a secondary's
  // LockRecord with a WriteRecord and materializes the staged value;
  // AgentRollback drops a secondary's intent + staged value. All gated behind
  // STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
  rpc AgentCommitPrimary(AgentCommitPrimaryRequest) returns (AgentCommitPrimaryResponse);
  rpc AgentRollForward(AgentRollForwardRequest) returns (AgentRollForwardResponse);
  rpc AgentRollback(AgentRollbackRequest) returns (AgentRollbackResponse);

  // Cross-shard ACID transactions — Phase 4 (epic #1478): the read-path lock
  // resolver. Given a (cf, key) and a snapshot read_ts, resolves any blocking
  // prewrite lock via the primary TxnStatus — rolling a committed secondary
  // forward, cleaning an aborted/stale (TTL-expired) intent, or reporting the
  // commit decision is still pending. Idempotent and callable by any reader. The
  // gateway runs this as a pre-step before its causal/vector reads. Gated behind
  // STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
  rpc AgentResolveLock(AgentResolveLockRequest) returns (AgentResolveLockResponse);

  // Cross-shard ACID transactions (epic #1478, issue #1598): read the primary
  // TxnStatus for a primary key from THIS node's coordination shard. The primary
  // commit record is written through the coordination Raft group, so a secondary
  // OWNER shard whose node is not in the coordination shard's replica set cannot
  // read it locally (RF < node_count, disjoint replica sets). The owner-shard
  // resolver routes the primary-status read here (to the coordination-shard
  // leader) instead of its node-local store, mirroring the write side. Internal
  // /admin-only and gated behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
  rpc AgentReadTxnStatus(AgentReadTxnStatusRequest) returns (AgentReadTxnStatusResponse);

  // Cross-shard ACID transactions (epic #1478, issue #1598): enumerate every
  // DECIDED primary TxnStatus (Committed/Aborted) on the receiving node's
  // coordination shard, with its participant (cf,key) list. The gateway's
  // recovery sweep calls this on the coordination-shard leader (the primary
  // statuses live there) and then fans a roll-forward / roll-back to each
  // participant's OWNER shard — the secondary locks the coordination shard cannot
  // itself reach under RF < node_count. Internal/admin-only, gated behind
  // STATELET_CROSS_SHARD_TXN (default OFF).
  rpc AgentListDecidedPrimaries(AgentListDecidedPrimariesRequest) returns (AgentListDecidedPrimariesResponse);

  // Cross-shard ACID transactions — Phase 3 (epic #1478): the user-facing
  // gateway coordinator RPC. Drives prewrite->commit->roll-forward for a
  // write_set spanning any set of shards/Raft groups, returning an all-or-nothing
  // commit decision. A single-shard write_set takes the optimistic fast path and
  // bypasses 2PC. Gated behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (returns
  // FailedPrecondition when the flag is unset).
  rpc CrossShardTxnCommit(CrossShardCommitRequest) returns (CrossShardCommitResponse);

  // Cross-shard ACID transactions — Phase 5 (epic #1478): recovery & liveness.
  // ResolveStaleTxn is the user-facing admin RPC that drives the idempotent
  // stale-lock resolver — it consults each lock's primary TxnStatus (Committed
  // -> roll forward; absent/Prewritten + lock TTL expired -> roll back) and
  // reclaims coordinator-crash / partition-orphaned intents. AgentResolveStaleTxn
  // is the internal per-coordination-shard driver the gateway fans to. Both gated
  // behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (FailedPrecondition when unset).
  rpc ResolveStaleTxn(ResolveStaleTxnRequest) returns (ResolveStaleTxnResponse);
  rpc AgentResolveStaleTxn(AgentResolveStaleTxnRequest) returns (AgentResolveStaleTxnResponse);

  // AgentGcExpiredLocks is the internal per-OWNER-shard sweep the gateway fans to
  // (issue #1795). The decided-primaries scan (AgentListDecidedPrimaries) only
  // enumerates COMMITTED/ABORTED primaries from the coordination shard, so a
  // coordinator that crashed after prewriting secondaries but BEFORE the commit
  // point leaves its primary forever Prewritten — never enumerated, so its
  // orphaned owner-shard intents are only reclaimed if a read happens to hit the
  // exact key. This RPC scans the TTL-expired prewrite locks on one owner shard
  // and resolves each against the coordination-shard primary status (rolling back
  // the never-committed ones), so the gateway sweep reclaims still-Prewritten
  // orphans across every participant shard without a read. Mirrors TiKV's
  // background ResolveLocks sweep over participant Regions. Gated behind
  // STATELET_CROSS_SHARD_TXN, DEFAULT OFF.
  rpc AgentGcExpiredLocks(AgentGcExpiredLocksRequest) returns (AgentGcExpiredLocksResponse);

  // Cross-shard ACID transactions — Phase 6 (epic #1478): buffered
  // BEGIN/COMMIT/ROLLBACK (TiKV-style optimistic 2PC). `TxnBegin` allocates a
  // transaction handle (the primary key every prewrite will fence on); the
  // client buffers its write_set locally and submits it at `TxnCommit`, which
  // drives the same prewrite->commit->roll-forward as CrossShardTxnCommit but
  // pinned to the begun primary. `TxnRollback` discards the handle (optimistic
  // 2PC prewrites nothing before COMMIT, so it is a buffer-drop ack). All gated
  // behind STATELET_CROSS_SHARD_TXN, DEFAULT OFF (FailedPrecondition when unset).
  rpc TxnBegin(TxnBeginRequest) returns (TxnBeginResponse);
  rpc TxnCommit(TxnCommitRequest) returns (TxnCommitResponse);
  rpc TxnRollback(TxnRollbackRequest) returns (TxnRollbackResponse);

  // Expire an existing edge (set valid_to).
  rpc AgentExpireEdge(AgentExpireEdgeRequest) returns (AgentExpireEdgeResponse);

  // Cascade-expire (#693): retract a fact and recursively close every fact
  // transitively derived from it — across agent boundaries — bitemporally,
  // diamond/cycle-safe, with retraction provenance + change-feed emission.
  rpc AgentCascadeExpire(AgentCascadeExpireRequest) returns (AgentCascadeExpireResponse);

  // Supersede a fact with a replacement (#693 phase 4). Closes old_fact, records
  // the Supersedes edge, and — when cascade=true — cascade-closes old_fact's
  // derived dependents (stamped Superseded/Cascaded provenance).
  rpc AgentSupersedeFact(AgentSupersedeFactRequest) returns (AgentSupersedeFactResponse);

  // Transactional memory ingest (#780): dedup / create + provenance edges /
  // supersede candidates, all committed as ONE atomic, snapshot-isolated
  // WriteBatch via the optimistic transaction manager. On a snapshot-isolation
  // conflict the engine retries within a bounded budget, then returns
  // action=Conflict (back-pressure) without writing.
  rpc AgentMemoryIngest(AgentMemoryIngestRequest) returns (AgentMemoryIngestResponse);

  // Get edge version history for a specific (src, dst, type) triple.
  rpc AgentEdgeHistory(AgentEdgeHistoryRequest) returns (AgentEdgeHistoryResponse);

  // ── Memory-scope provenance audit (#697 phase 4) ────────────────────────
  // Read back the immutable provenance log (one record per access decision:
  // AddStep/GetStep/Traverse/FindSimilar/GetEdges, incl. AdminBypass). Gated by
  // ManageMemoryScope. The result is materialized (not streamed) since the audit
  // tool scans a bounded time window; large windows page via after_ts/after_seq.
  rpc AgentQueryProvenance(AgentQueryProvenanceRequest) returns (AgentQueryProvenanceResponse);

  // ── Memory-scope team-membership admin (#697 phase 2c / #794) ───────────
  // Grant or revoke an agent's membership of a team. Durable through the
  // metadata Raft group. Gated by ManageMemoryScope. The grant store is
  // metadata-side, so on the raw Statelet leaf this is unimplemented — operators
  // call it through AgentStateService, which authorizes then applies the op.
  rpc AgentManageTeamGrant(AgentManageTeamGrantRequest) returns (AgentManageTeamGrantResponse);

  // ── Durable agent execution (#846, epic #699 / sub-epic #792) ───────────
  // Raft-backed run/step home: each write is a RAFT_TYPE_KV log entry on the
  // owning shard, replicated to a quorum before ack, so a crashed multi-step
  // agent resumes at the exact failed step on a new leader. RunStep/CompleteStep
  // are split so the server never executes client code over the wire (Temporal/
  // DBOS record-before-effect across the network).
  rpc AgentStartRun(AgentStartRunRequest) returns (AgentStartRunResponse);
  rpc AgentRunStep(AgentRunStepRequest) returns (AgentRunStepResponse);
  rpc AgentCompleteStep(AgentCompleteStepRequest) returns (AgentCompleteStepResponse);
  rpc AgentCheckpointGet(AgentCheckpointGetRequest) returns (AgentCheckpointGetResponse);
  rpc AgentCheckpointLatest(AgentCheckpointLatestRequest) returns (AgentCheckpointLatestResponse);
  rpc AgentProvenanceChainQuery(AgentProvenanceChainQueryRequest) returns (AgentProvenanceChainQueryResponse);
  rpc AgentResumeFromStep(AgentResumeFromStepRequest) returns (AgentResumeFromStepResponse);
  rpc AgentResumeSemantic(AgentResumeSemanticRequest) returns (AgentResumeSemanticResponse);
  rpc AgentGetRunStatus(AgentGetRunStatusRequest) returns (AgentGetRunStatusResponse);
  // Phase 5 (#797): branch/time-travel resume — fork a run from any historical
  // step_seq into a NEW AgentFork branch + child run, leaving the source run
  // untouched (LangGraph "time-travel"). Leaf RPC homed on the source run's shard.
  rpc AgentForkRun(AgentForkRunRequest) returns (AgentForkRunResponse);
  rpc AgentForkAcrossCandidates(AgentForkAcrossCandidatesRequest) returns (AgentForkAcrossCandidatesResponse);
  // Phase 2 (#1699): content-addressed artifact records for large durable-run
  // results. Leaf RPCs are homed on the owning run shard and authorized against
  // RunRecord.agent_id.
  rpc AgentArtifactPut(AgentArtifactPutRequest) returns (AgentArtifactPutResponse);
  rpc AgentArtifactGet(AgentArtifactGetRequest) returns (AgentArtifactGetResponse);
  rpc AgentArtifactResolve(AgentArtifactResolveRequest) returns (AgentArtifactResolveResponse);

  // ── Team time-travel (#787, epic #698 Phase 3) ──────────────────────────
  // Leaf, single-shard "as-of-then" team belief reconstruction. The gateway
  // fans these out to every shard, pins one committed ordinal per shard (the
  // FoundationDB-style read version), merges/dedupes, paginates, and tolerates
  // dead shards (partial view). Calls the in-process #724 operators
  // CausalGraphManager::team_snapshot / team_diff.
  rpc AgentTeamSnapshotLocal(TeamSnapshotLocalRequest) returns (TeamSnapshotLocalResponse);
  rpc AgentTeamDiffLocal(TeamDiffLocalRequest) returns (TeamDiffLocalResponse);

  // ── Bitemporal belief queries: "who believed what, when" ────────────────
  // Combined (valid-time as_of, transaction-time tx_as_of, author) query that
  // reconstructs any agent's (or the team's) belief state at a past instant.
  rpc AgentBeliefQuery(AgentBeliefQueryRequest) returns (AgentBeliefQueryResponse);
  // Per-agent belief divergence at (as_of, tx_as_of): "A believes X, B believes ¬X".
  rpc AgentBeliefDivergence(AgentBeliefDivergenceRequest) returns (AgentBeliefDivergenceResponse);

  // ── Raw agent-state row access (P3) ────────────────────────────────────
  // Read rows out of an agent column family BY NAME, so a coordinator outside
  // the storage process can drive agent semantics itself instead of asking the
  // data node to. This is what lets the agent RPCs move to the gateway: the
  // gateway already links the row codecs (one codebase), it was only missing
  // the bytes.
  //
  // Why a dedicated pair rather than the generic Get/Scan: the agent CFs live
  // on the shared DB and are deliberately NOT registered in the metadata CF
  // registry, so `(cf, key)` routing resolves nothing for them. Registering
  // them would mint a SECOND Raft group over rows a different group already
  // writes — a split-brain shape that has erased data in this system before.
  // Instead these address the pinned coordination shard explicitly and are
  // served ONLY by its leader, exactly like the claim/lease CAS path.
  rpc AgentStateGet(AgentStateGetRequest) returns (AgentStateGetResponse);
  rpc AgentStateScan(AgentStateScanRequest) returns (AgentStateScanResponse);

  // Append one already-decided provenance record to the immutable audit log.
  //
  // The scope DECISION moves to the gateway with the rest of agent semantics;
  // the audit APPEND stays here. §4 of the design already excludes the
  // provenance log from the triple-plane move, and keeping the append local
  // avoids turning a best-effort local write into a cross-process failure mode
  // on every audited read. The gateway sends the record it built; this RPC is
  // internal-token gated, same as the rest of the agent surface, because a
  // caller that could reach it directly could forge audit entries.
  rpc AgentAppendProvenance(AgentAppendProvenanceRequest) returns (AgentAppendProvenanceResponse);

  // Raw adjacency of one anchor, with the bitemporal filters applied and NO
  // scope filtering. Edges are served from an in-memory index rebuilt at open,
  // not read row-by-row, so `AgentStateScan` cannot reconstruct them — and
  // reimplementing bitemporal visibility on the coordinator would put the
  // subtlest filtering in this system in two places. Temporal filtering stays
  // with the index; the scope decision is the caller's.
  //
  // Leaks peer ids by construction. Internal-token gated, never client-facing.
  rpc AgentStateEdges(AgentStateEdgesRequest) returns (AgentStateEdgesResponse);

  // Batch sibling of AgentStateGet over one role. A coordinator filtering an
  // adjacency list has to check every peer's scope; one round trip per peer is
  // fine in-process and untenable across it, so the peers resolve in one call.
  rpc AgentStateBatchGet(AgentStateBatchGetRequest) returns (AgentStateBatchGetResponse);

  // Per-author belief resolution for one edge slot ("A believes X, B believes
  // not-X"). Another reduction over the revision chain — this time grouped by
  // author — so it stays with the index for the same reason the bitemporal form
  // does. Returns every author's belief unfiltered; the caller applies scope.
  rpc AgentStateBeliefDivergence(AgentStateBeliefDivergenceRequest)
      returns (AgentStateBeliefDivergenceResponse);

  // Fetch a run record together with one of its checkpoints, from the shard the
  // `run_id` self-routes to (`run_id >> 40`) — NOT the coordination shard the
  // causal primitives serve, because durable-execution state is homed per run
  // shard.
  //
  // Both in one call because the caller needs the run record to authorize the
  // checkpoint at all: splitting them would make every checkpoint read two
  // round trips to answer one question.
  rpc AgentRunCheckpointGet(AgentRunCheckpointGetRequest)
      returns (AgentRunCheckpointGetResponse);

  // Team belief reconstruction / diff over one shard, UNFILTERED, together with
  // that shard's committed read version.
  //
  // The graph operators (`team_snapshot` / `team_diff`) walk the in-memory
  // index and the read version is the shard's own MVCC seq, so both stay here;
  // visibility filtering, the global sort and pagination are the caller's.
  // Answering the read version in the SAME call is the point — it is the
  // FoundationDB-style per-shard fence the coordinator maxes across shards, and
  // fetching it separately would fence against a different instant than the one
  // the answer was computed at.
  rpc AgentStateTeamRead(AgentStateTeamReadRequest) returns (AgentStateTeamReadResponse);

  // Conditional write PINNED to the coordination shard's Raft group.
  //
  // The generic ConditionalBatchWrite routes by `(cf, key)`, which for agent
  // coordination state is the wrong group: those rows are written through the
  // coordination shard, and ordering them in a different group would let two
  // writers to the same claim key be serialized by two different logs. Same
  // pinning the claim/lease CAS already relies on.
  rpc AgentStateConditionalWrite(AgentStateConditionalWriteRequest)
      returns (ConditionalBatchWriteResponse);

  // Per-key engine VERSIONS (no values) for arbitrary `(cf, key)` pairs, read
  // from the coordination shard together with that shard's current sequence.
  //
  // Versions only, deliberately. Optimistic-commit validation and conflict
  // reporting need nothing else, and a versions-only surface is a far smaller
  // capability than "read any CF by name" — it cannot disclose content. Serves
  // the same admin-only callers `AgentTxnCommit` already restricts itself to.
  rpc AgentStateVersions(AgentStateVersionsRequest) returns (AgentStateVersionsResponse);

  // Allocate a contiguous block of causal step/fact ids from the ONE authority.
  //
  // Uniqueness comes from a single in-process atomic on the coordination
  // shard's causal manager, not from anything a caller could reproduce: two
  // coordinators running their own counters would hand out the same id. So a
  // coordinator that needs ids asks for them, and amortizes the round trip by
  // taking a block.
  rpc AgentStateAllocIds(AgentStateAllocIdsRequest) returns (AgentStateAllocIdsResponse);

  // Subscribe to write events on this shard (server-streaming to gateway).
  // DEPRECATED (CDC Phase 5b, issue #823): superseded by SubscribeCommitted,
  // which is a durable, ordered, offset-addressable, resumable superset of this
  // best-effort live-only feed. Prefer SubscribeCommitted for all new consumers;
  // this RPC remains for backward compatibility and will be removed in a future
  // major version.
  rpc AgentSubscribeWrites(AgentSubscribeWritesRequest) returns (stream AgentWriteEventProto) {
    option deprecated = true;
  }

  // Durable, ordered, offset-addressable, resumable change-feed (CDC) keyed on
  // the stable Raft log index. Catch-up from a past offset (replayed from the
  // durable log) then live-tail; consumer-checkpointed resume (issue #692).
  rpc SubscribeCommitted(SubscribeCommittedRequest) returns (stream CommittedFeedItem);

  // ── Agent State: branch (fork) leaf operations ──────────────────────────
  rpc AgentFork(AgentForkRequest) returns (AgentForkResponse);
  rpc AgentMergeBranch(AgentMergeBranchRequest) returns (AgentMergeBranchResponse);
  rpc AgentDiscardBranch(AgentDiscardBranchRequest) returns (AgentDiscardBranchResponse);
  rpc AgentListBranches(AgentListBranchesRequest) returns (AgentListBranchesResponse);
  rpc AgentBranchPut(AgentBranchPutRequest) returns (AgentBranchPutResponse);
  rpc AgentBranchGet(AgentBranchGetRequest) returns (AgentBranchGetResponse);

  // ── Graph index operations ────────────────────────────────────────────────

  // Create a graph index (6 CFs + HNSW config).
  rpc CreateGraphIndex(CreateGraphIndexRequest) returns (CreateGraphIndexResponse);

  // Drop a graph index and its CFs.
  rpc DropGraphIndex(DropGraphIndexRequest) returns (DropGraphIndexResponse);

  // Add a node with optional vector and properties.
  rpc GraphAddNode(GraphAddNodeRequest) returns (GraphAddNodeResponse);

  // Batch add multiple nodes with vectors and properties.
  rpc GraphBatchAddNode(GraphBatchAddNodeRequest) returns (GraphBatchAddNodeResponse);

  // Remove a node from the graph: evicts its vector from the HNSW index,
  // deletes its properties and every temporal edge that touches it. Used by the
  // conflict-resolver DELETE path so the graph and vector indexes never diverge.
  rpc GraphRemoveNode(GraphRemoveNodeRequest) returns (GraphRemoveNodeResponse);

  // Add a temporal edge between two nodes.
  rpc GraphAddEdge(GraphAddEdgeRequest) returns (GraphAddEdgeResponse);

  // Batch add multiple temporal edges in one atomic write (mirrors
  // GraphBatchAddNode for edges). Collapses N per-edge proposals to ~1.
  rpc GraphBatchAddEdge(GraphBatchAddEdgeRequest) returns (GraphBatchAddEdgeResponse);

  // Internal data-node RPC: apply a shard-local subset of graph writes.
  // Used when a logical graph write spans multiple CF shards/leaders.
  rpc GraphBatchWrite(GraphBatchWriteRequest) returns (GraphBatchWriteResponse);

  // Internal data-node RPC: read the current durable value of a shard-local
  // subset of graph keys. Used to capture pre-images before a multi-leader
  // graph write so a partial failure can be compensated (rows restored).
  rpc GraphBatchRead(GraphBatchReadRequest) returns (GraphBatchReadResponse);

  // HNSW nearest neighbor search on graph vectors.
  rpc GraphSearch(GraphSearchRequest) returns (GraphSearchResponse);

  // Vector-anchored multi-hop expansion (GraphRAG primitive): vector search
  // for anchor nodes, then BFS-expand the induced subgraph from those anchors
  // with depth / edge-type / as_of filters — all in one server-side call.
  rpc GraphSearchExpand(GraphSearchExpandRequest) returns (GraphSearchExpandResponse);

  // Unified GraphRAG retrieval (issue #696): one server-side call that does
  // vector-seed -> graph expansion -> blended rerank by similarity + recency +
  // graph-distance, scoped by valid-time / transaction-time and memory scope,
  // returning ranked facts with provenance and bitemporal validity. Composes
  // GraphSearch (anchors) + GraphQueryEdgesBatch (bitemporal BFS) +
  // GraphGetNodesBatch (hydration). Served by the gateway only.
  rpc GraphRagSearch(GraphRagSearchRequest) returns (GraphRagSearchResponse);

  // Get a graph node's properties by ID (reads from graph node CF directly).
  rpc GraphGetNode(GraphGetNodeRequest) returns (GraphGetNodeResponse);

  // Query temporal edges for a node.
  rpc GraphQueryEdges(GraphQueryEdgesRequest) returns (GraphQueryEdgesResponse);

  // Batched edge query: query temporal edges for many nodes at once, all
  // routed to the same shard. Edge-type and as_of/time filters are applied at
  // the data node. Used by the gateway's cross-shard BFS to expand a whole
  // per-shard frontier in one RPC (O(hops x shards) instead of O(visited)).
  rpc GraphQueryEdgesBatch(GraphQueryEdgesBatchRequest) returns (GraphQueryEdgesBatchResponse);

  // Batched node-properties fetch: hydrate many nodes that route to the same
  // shard in a single RPC. Used by cross-shard traversal prop hydration.
  rpc GraphGetNodesBatch(GraphGetNodesBatchRequest) returns (GraphGetNodesBatchResponse);

  // First-class multi-hop BFS traversal from a start node. Honors direction,
  // depth, edge-type and as_of/time filters, and (on the gateway) drives
  // cross-shard hops by re-dispatching frontiers to shard leaders.
  rpc GraphTraverse(GraphTraverseRequest) returns (GraphTraverseResponse);

  // Shard-local scan of the reverse label posting list (ROLE_LABEL_INDEX):
  // resolve each label string to its interned label_id, prefix-seek the
  // posting list, and return member node ids (optionally hydrated NodeProp
  // JSON), capped. Multi-label = server-side conjunctive intersection
  // (smallest posting list first). The gateway fans this out across the shard
  // set and re-applies the global cap after merge. Used as the label+property
  // MATCH anchor-resolution entry point (epic #1429).
  rpc GraphNodesByLabel(GraphNodesByLabelRequest) returns (GraphNodesByLabelResponse);

  // Temporal join: align graph edges with KV time-series data.
  // For each edge in the time range, looks up the corresponding KV entries
  // at the edge's timestamp. Used for news → price alignment.
  rpc GraphTemporalJoin(GraphTemporalJoinRequest) returns (GraphTemporalJoinResponse);

  // Graph analytics: run PageRank / WCC / DegreeCentrality over a graph index's
  // edges. The gateway fans the computation out across every shard owning the
  // graph's edge CF, merges all local edge lists into one global adjacency, and
  // runs a single global PageRank/WCC/DegreeCentrality (so masses sum to 1, WCC
  // components are not split across shards, and DegreeCentrality normalizes over
  // the full node set). Optionally writes scores back into node properties.
  rpc GraphAnalytics(GraphAnalyticsRequest) returns (GraphAnalyticsResponse);

  // Internal cross-shard fan-out helper for GraphAnalytics: dump one shard's
  // local analytics edge list (the same filtered/deduped (src,dst) pairs
  // `build_analytics_graph` would feed the engine) as packed parallel u64
  // arrays, so the gateway can merge edges from all shards into one global
  // adjacency. Not intended for direct client use.
  rpc GraphAnalyticsEdges(GraphAnalyticsEdgesRequest) returns (GraphAnalyticsEdgesResponse);

  // Internal cross-shard fan-out helper for GraphAnalytics write-back: persist a
  // batch of (node_id, score, component) rows for nodes this shard owns into
  // their ROLE_NodeProp "__analytics" sub-key. The gateway routes each node to
  // its owning shard so a data node only receives its own nodes.
  rpc GraphAnalyticsWriteScores(GraphAnalyticsWriteScoresRequest) returns (GraphAnalyticsWriteScoresResponse);

  // Weighted shortest-path / pathfinding (Dijkstra / A*, k-shortest via Yen's)
  // over user graph edges. Gateway-only: expands a cost-ordered frontier across
  // shard leaders, decoding edge weights from edge properties.
  rpc GraphShortestPath(GraphShortestPathRequest) returns (GraphShortestPathResponse);

  // Read-only declarative pattern-match graph query (an openCypher subset:
  // MATCH path patterns with node/edge-type filters, WHERE on node properties
  // plus an `as_of` temporal predicate, RETURN / LIMIT). Gateway-only: the
  // query is parsed + planned, then compiled to existing engine traversal
  // primitives (GraphTraverse / GraphShortestPath / GraphSearchExpand) and
  // WHERE predicates are evaluated against hydrated ROLE_NodeProp JSON in the
  // distributed-result merge stage. CREATE / MERGE are not supported.
  rpc GraphQuery(GraphQueryRequest) returns (GraphQueryResponse);
}

// ─── Raft Internal Service ────────────────────────────────────────────────────
//
// Used for peer-to-peer replication between Statelet nodes.
// Clients should not call these RPCs directly.
service RaftService {
  // Leader → Follower: replicate log entries and/or send heartbeat.
  rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse);

  // Candidate → All peers: request a vote during leader election.
  rpc RequestVote(RequestVoteRequest) returns (RequestVoteResponse);

  // Leader → Follower: install a full state-machine snapshot.
  rpc InstallSnapshot(InstallSnapshotRequest) returns (InstallSnapshotResponse);

  // Leader → Follower: chunk-streamed variant of InstallSnapshot for
  // shard-sized payloads (see InstallSnapshotChunk).
  rpc InstallSnapshotStream(stream InstallSnapshotChunk) returns (InstallSnapshotResponse);

  // Leader → Target follower: graceful leadership transfer (etcd
  // MsgTimeoutNow). The leader sends this only after verifying the target's
  // log is caught up; the target starts a real election immediately,
  // bypassing the randomized timeout and PreVote.
  rpc TimeoutNow(TimeoutNowRequest) returns (TimeoutNowResponse);

  // Leader → Follower: digest handshake BEFORE an InstallSnapshot transfer.
  // A follower that "needs a snapshot" often already holds byte-identical
  // shard state (raft index-space divergence after the historical reopen
  // resets; restart-time truncate-to-tip): the leader asks the follower for
  // a digest of its materialized range state and compares it with its own —
  // on a match it ships a metadata-only InstallSnapshot (state_verified)
  // that fast-forwards the follower's raft position without moving any data.
  rpc ShardStateDigest(ShardStateDigestRequest) returns (ShardStateDigestResponse);
}

// ─── Ping ─────────────────────────────────────────────────────────────────────

message PingRequest {}

message PingResponse {
  string message = 1; // always "PONG"
}

// ─── Put ──────────────────────────────────────────────────────────────────────

message PutRequest {
  uint32 cf    = 1; // column family id (0 = default user CF)
  bytes  key   = 2;
  bytes  value = 3;
  uint64 shard_id    = 4; // gateway-stamped shard id (0 = skip validation)
  uint64 shard_epoch = 5; // gateway-stamped epoch (0 = skip validation)
}

message PutResponse {}

// ─── Get ──────────────────────────────────────────────────────────────────────

message GetRequest {
  uint32 cf  = 1;
  bytes  key = 2;
  uint64 shard_id    = 3;
  uint64 shard_epoch = 4;
  // Also resolve the key's engine sequence into `GetResponse.seq`. Opt-in
  // because it costs an extra memtable + SST version probe on top of the read:
  // only a coordinator building a read set needs it, and an ordinary Get should
  // not pay for it.
  bool   with_seq    = 5;
}

message GetResponse {
  bool  found = 1; // false ↔ key not found (or tombstoned)
  bytes value = 2; // only meaningful when found = true
  // Engine sequence this key was last written at; 0 when absent. A coordinator
  // that reads a key it does not write needs this to build an
  // `AgentTxnRead.observed_seq` — without it, optimistic transactions can only
  // be driven from inside the storage process. NOT the same clock as the
  // Percolator `read_ts` fields elsewhere in this file: that is a packed HLC,
  // this is the engine's MVCC sequence.
  uint64 seq = 3;
}

// ─── Delete ───────────────────────────────────────────────────────────────────

message DeleteRequest {
  uint32 cf  = 1;
  bytes  key = 2;
  uint64 shard_id    = 3;
  uint64 shard_epoch = 4;
}

message DeleteResponse {}

// ─── Merge ────────────────────────────────────────────────────────────────────

message MergeRequest {
  uint32 cf    = 1;
  bytes  key   = 2;
  bytes  value = 3; // merge operand
  uint64 shard_id    = 4;
  uint64 shard_epoch = 5;
}

message MergeResponse {}

// ─── BatchWrite ───────────────────────────────────────────────────────────────

enum WriteOp {
  PUT    = 0;
  DELETE = 1;
  MERGE  = 2;
}

message WriteEntry {
  uint32   cf    = 1;
  WriteOp  op    = 2;
  bytes    key   = 3;
  bytes    value = 4; // empty for DELETE
  // Absolute expiry (ms since epoch); 0 = no TTL. Leases need per-key expiry
  // at write time — a lease that has to be reaped by a separate sweep is not a
  // lease. Ignored for DELETE.
  uint64   expire_at = 5;
}

message BatchWriteRequest {
  repeated WriteEntry entries = 1;
  uint64 shard_id    = 2;
  uint64 shard_epoch = 3;
}

message BatchWriteResponse {}

// Predicate for a conditional batch gate. Values are deliberately prefixed
// because proto enum variants share package-level generated names in some
// languages.
enum WriteConditionKind {
  WRITE_CONDITION_NONE            = 0;
  WRITE_CONDITION_IF_ABSENT       = 1;
  WRITE_CONDITION_IF_PRESENT      = 2;
  WRITE_CONDITION_IF_VALUE_EQUALS = 3;
  // Multi-key optimistic gate: apply the WHOLE batch only if every entry in
  // `read_set` still has its observed sequence. Unlike the single-key kinds
  // above, the predicate spans keys the batch does not write, which is what
  // makes snapshot isolation expressible on the generic KV wire instead of
  // only through the agent-specific transaction RPCs.
  WRITE_CONDITION_IF_READ_SET_UNCHANGED = 4;
  // Single-key CAS on the engine sequence: apply only if the gate key EXISTS
  // and is still at exactly `condition_seq`.
  //
  // Not expressible as a one-key `read_set`. That gate means "not overwritten
  // since" — it rejects only a NEWER version (`latest > observed`), so an
  // ABSENT key passes it and the write silently CREATES the row. This one means
  // "still exactly this version, and still there", which is what a fenced
  // compare-and-swap actually promises.
  WRITE_CONDITION_IF_SEQ_EQUALS = 5;
}

message ConditionalBatchWriteRequest {
  WriteEntry         gate            = 1; // PUT or DELETE; MERGE is invalid as a gate
  WriteConditionKind condition       = 2;
  bytes              condition_value = 3; // expected value for IF_VALUE_EQUALS
  repeated WriteEntry entries        = 4; // plain entries gated by `gate`
  uint64 shard_id                    = 5;
  uint64 shard_epoch                 = 6;
  // Only for WRITE_CONDITION_IF_READ_SET_UNCHANGED. Each entry is a key the
  // caller read at `observed_seq`; the batch applies only if none of them has
  // been overwritten since. Validated at the SAME atomic point the batch is
  // applied, so no concurrent write can slip in between.
  repeated AgentTxnRead read_set     = 7;
  // Only for WRITE_CONDITION_IF_SEQ_EQUALS: the exact engine sequence the gate
  // key must still hold.
  uint64 condition_seq               = 8;
}

message ConditionalBatchWriteResponse {
  bool  applied    = 1; // false when the predicate failed and no entry applied
  bool  prev_found = 2;
  bytes prev_value = 3;
  // Sequence the batch committed at (0 when not applied). This is the fencing
  // token a claim/lease hands to its holder, and the `commit_seq` an optimistic
  // transaction reports.
  uint64 new_seq   = 4;
  // Sequence the gate key currently holds (0 when absent). On rejection this is
  // the conflicting version, which is what a CAS caller needs to retry against.
  uint64 prev_seq  = 5;
}

// ─── ConditionalSet ──────────────────────────────────────────────────────────

// Predicate for an atomic conditional set (`ConditionalSet`).
enum SetExpectation {
  IF_ABSENT       = 0; // NX: apply the write only if the key has no live value
  IF_PRESENT      = 1; // XX: apply the write only if the key already has a live value
  IF_VALUE_EQUALS = 2; // CAS: apply only if the live value matches expected_value
}

message ConditionalSetRequest {
  uint32 cf             = 1;
  bytes  key            = 2;
  bytes  value          = 3;
  SetExpectation expect = 4;
  uint64 shard_id       = 5;
  uint64 shard_epoch    = 6;
  bytes  expected_value = 7;
}

message ConditionalSetResponse {
  bool  applied    = 1; // predicate held and the write committed
  bool  prev_found = 2; // key had a live value under the apply-time check
  bytes prev_value = 3; // that prior value (empty when prev_found = false)
}

// ─── BatchGet ────────────────────────────────────────────────────────────────

message BatchGetRequest {
  uint32        cf   = 1;
  repeated bytes keys = 2;
  uint64 shard_id    = 3;
  uint64 shard_epoch = 4;
  // Also resolve each key's engine sequence into `BatchGetEntry.seq`. Opt-in
  // for a second reason beyond the per-key probe cost of `GetRequest.with_seq`:
  // it also gives up the LSM-coalesced batch read (one pinned snapshot +
  // grouped block fetches), which reports no per-key version. Leave it false
  // for MGET-style bulk reads.
  bool   with_seq    = 5;
}

message BatchGetEntry {
  bytes key   = 1;
  bool  found = 2;
  bytes value = 3;
  // Engine sequence this key was last written at; 0 when absent. Same clock and
  // same purpose as `GetResponse.seq` — a coordinator batching its reads needs
  // one `observed_seq` per key, or it has to fall back to N point Gets.
  uint64 seq  = 4;
}

message BatchGetResponse {
  repeated BatchGetEntry entries = 1;
}

// ─── Scan ────────────────────────────────────────────────────────────────────

message ScanRequest {
  uint32 cf        = 1; // column family id
  bytes  prefix    = 2; // key prefix filter (empty = scan all keys)
  bytes  cursor    = 3; // resume cursor (empty = start from beginning)
  uint32 limit     = 4; // max entries to return (0 = default 100)
  uint64 shard_id    = 5;
  uint64 shard_epoch = 6;
  // Also report each entry's engine sequence in `ScanEntry.seq`. Unlike the
  // point-read flags this is nearly free (the scan already walks internal keys
  // and knows the version it settled on), but it stays opt-in so the field's
  // meaning is unambiguous: 0 means "not requested", never "version zero".
  bool   with_seq    = 7;
}

message ScanResponse {
  repeated ScanEntry entries    = 1;
  bytes              next_cursor = 2; // empty = no more data
  bool               has_more   = 3;
  bool               partial_failure = 4; // true when some shards failed; client may retry
}

message ScanEntry {
  bytes key   = 1;
  bytes value = 2;
  // Engine sequence of this version — see `GetResponse.seq`.
  uint64 seq  = 3;
}

// ─── DeleteByPrefix ─────────────────────────────────────────────────────────

message DeleteByPrefixRequest {
  uint32 cf     = 1; // column family id
  bytes  prefix = 2; // key prefix to match (must be non-empty)
  uint64 shard_id    = 3;
  uint64 shard_epoch = 4;
}

message DeleteByPrefixResponse {
  uint32 deleted = 1; // number of keys deleted
}

// ─── Raft AppendEntries ───────────────────────────────────────────────────────

message RaftEntry {
  uint64 term       = 1; // term when entry was created
  uint64 index      = 2; // 1-based log position
  bytes  data       = 3; // serialised WriteBatch (empty for heartbeat/no-op)
  uint32 entry_type = 4; // 0 = Normal, 1 = Config
}

message AppendEntriesRequest {
  uint64             term           = 1; // leader's current term
  uint64             leader_id      = 2;
  uint64             prev_log_index = 3; // index of the entry immediately before new ones
  uint64             prev_log_term  = 4;
  repeated RaftEntry entries        = 5; // empty ⇒ heartbeat
  uint64             leader_commit  = 6; // leader's commitIndex
  uint64             shard_id       = 7; // shard id for multi-shard Raft routing (0 = single-group mode)
}

message AppendEntriesResponse {
  uint64 term           = 1; // follower's currentTerm (for leader to update itself)
  bool   success        = 2;
  // Fast roll-back hints (§5.3 optimisation):
  uint64 conflict_term  = 3; // term of the conflicting entry (0 if none)
  uint64 conflict_index = 4; // first index with that term   (0 if none)
}

// ─── Raft RequestVote ─────────────────────────────────────────────────────────

message RequestVoteRequest {
  uint64 term           = 1;
  uint64 candidate_id   = 2;
  uint64 last_log_index = 3;
  uint64 last_log_term  = 4;
  uint64 shard_id       = 5; // shard id for multi-shard Raft routing (0 = single-group mode)
  bool   pre_vote       = 6; // Raft §9.6 PreVote: prospective ballot, must not bump receiver term
}

message RequestVoteResponse {
  uint64 term         = 1; // recipient's currentTerm (used to update candidate)
  bool   vote_granted = 2;
}

// ─── Raft TimeoutNow (graceful leadership transfer) ───────────────────────────

message TimeoutNowRequest {
  uint64 term      = 1; // sender's current term — the target refuses stale senders
  uint64 leader_id = 2;
  uint64 shard_id  = 3; // shard id for multi-shard Raft routing (0 = single-group mode)
}

message TimeoutNowResponse {
  uint64 term     = 1; // target's currentTerm
  bool   accepted = 2; // false: refused (stale term / quarantined state / already leader)
}

// ─── Raft InstallSnapshot ─────────────────────────────────────────────────────

message InstallSnapshotRequest {
  uint64 term                = 1;
  uint64 leader_id           = 2;
  uint64 last_included_index = 3;
  uint64 last_included_term  = 4;
  bytes  data                = 5; // complete snapshot payload
  uint64 shard_id            = 6; // shard id for multi-shard Raft routing (0 = single-group mode)
  // Digest fast-path (see ShardStateDigest): when true, `data` is empty and
  // `verified_digest` echoes the digest the follower reported — the follower
  // re-checks it against its cached value and, on a match, fast-forwards its
  // raft position without any data transfer.
  bool   state_verified      = 7;
  bytes  verified_digest     = 8;
}

// Digest handshake for the InstallSnapshot fast-path. The digest covers the
// follower's MATERIALIZED (post-merge-fold) state over the shard range,
// CF-by-CF in name order — comparable across nodes despite node-local
// physical cf ids and different SST/memtable layouts.
message ShardStateDigestRequest {
  uint64 shard_id  = 1;
  uint64 term      = 2; // leader's term (context; install carries the real checks)
  uint64 leader_id = 3;
  bytes  start_key = 4; // leader's scope — follower must hold the SAME range
  bytes  end_key   = 5;
  uint32 algo      = 6; // 1 = xxh3-128 over (cf-name, key, value) length-framed stream
}

message ShardStateDigestResponse {
  // False when the digest cannot be compared: scope/CF mismatch, unsupported
  // algo, no snapshot scope, or the follower declined (busy). The leader
  // falls back to a full transfer.
  bool   comparable  = 1;
  bytes  digest      = 2; // 16 bytes (xxh3-128, little-endian)
  uint64 entry_count = 3;
  // Responder's `last_applied` at the moment the digest was computed. Lets a
  // suspect rejoiner (unclean-shutdown quarantine) turn a mismatch into a
  // PROOF of state-machine divergence: two replicas at the SAME applied index
  // must hold identical state, so equal positions + unequal digests can only
  // mean one side durably lost applied data.
  uint64 last_applied = 4;
}

// One chunk of a streamed InstallSnapshot transfer. Raft metadata rides on
// every chunk (cheap, and makes each chunk self-describing); the receiver
// takes the header fields from the FIRST chunk. Streaming exists because a
// transfer snapshot is shard-sized (1.39GB observed): as ONE unary message it
// needs a giant gRPC message cap and a 2x encode/decode memory spike on both
// ends, and tonic's bare 4MiB server default silently blocked follower
// healing for days.
//
// payload_format 0 (legacy): `data` is a raw byte slice of ONE monolithic
// msgpack payload; the receiver concatenates slices in stream order and
// decodes the whole thing at once (peak memory = full payload).
// payload_format 1 (framed): each `data` is ONE self-contained msgpack
// `SnapFrame` (Header / CfBegin / Entries / CfEnd / End); the receiver spools
// frames to disk as they arrive and the restore applies them one at a time,
// so neither side ever materializes the full payload in memory. A framed
// stream is terminated by an explicit `eof=true` marker chunk — a stream
// that ends without one was aborted by the sender (e.g. export failure) and
// must be discarded, because gRPC also ends the stream cleanly in that case.
message InstallSnapshotChunk {
  uint64 term                = 1;
  uint64 leader_id           = 2;
  uint64 last_included_index = 3;
  uint64 last_included_term  = 4;
  bytes  data                = 5; // payload slice (format 0) or one frame (format 1)
  uint64 shard_id            = 6;
  uint32 payload_format      = 7; // 0 = legacy monolithic slices, 1 = framed
  bool   eof                 = 8; // framed streams: terminal marker chunk (empty data)
}

message InstallSnapshotResponse {
  uint64 term = 1;
  // True only when the follower's state machine actually restored snapshot
  // state. When false (e.g. an empty/no-op snapshot), the follower did NOT
  // fast-forward last_applied/commit_index, so the leader must keep streaming
  // the missing committed entries via AppendEntries instead of marking the
  // follower caught up. Defaults to false for older peers.
  bool installed = 2;
}

// ─── Metadata Service ─────────────────────────────────────────────────────────
//
// Exposes cluster metadata: shard map, column families, and mutating operations
// that flow through the metadata Raft group.

service MetadataService {
  // List all known shards.
  rpc GetShards(GetShardsRequest) returns (GetShardsResponse);

  // List all registered column families.
  rpc GetColumnFamilies(GetColumnFamiliesRequest) returns (GetColumnFamiliesResponse);

  // Propose a metadata change through the metadata Raft group.
  rpc ProposeOp(ProposeMetadataOpRequest) returns (ProposeMetadataOpResponse);

  // Find the shard responsible for a (CF, key) pair.
  rpc GetShardForKey(GetShardForKeyRequest) returns (GetShardForKeyResponse);

  // Register a data node so the metadata service knows its addresses.
  // Called by each data node on startup.
  rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse);

  // Subscribe to shard lifecycle events (server-streaming).
  // The server first streams a full state snapshot (all current shards, nodes,
  // and column families as synthetic events), then streams live updates as
  // they are committed through the metadata Raft group.
  rpc SubscribeShardEvents(SubscribeShardEventsRequest) returns (stream ShardEventProto);

  // Data nodes periodically report their per-shard/per-CF stats.
  // Stats are cached in memory (not replicated via Raft).
  rpc ReportNodeStats(ReportNodeStatsRequest) returns (ReportNodeStatsResponse);

  // Train a vector index (orchestrated by metadata service).
  // For IVF-PQ/IVF-SQ: global centroid training + distribute to nodes.
  // For SPFresh: local training on each node.
  rpc TrainVectorIndex(TrainVectorIndexRequest) returns (TrainVectorIndexResponse);

  // Train coarse routing centroids for SPFresh (two-level routing).
  rpc TrainCoarseRouting(TrainCoarseRoutingRequest) returns (TrainCoarseRoutingResponse);
}

// ─── Metadata Raft Service ────────────────────────────────────────────────────
//
// Peer-to-peer Raft replication for the metadata Raft group.
// Reuses the same message types as RaftService but is a distinct gRPC service
// so that data-plane and metadata-plane Raft traffic can be separated.
service MetadataRaftService {
  rpc AppendEntries(AppendEntriesRequest) returns (AppendEntriesResponse);
  rpc RequestVote(RequestVoteRequest) returns (RequestVoteResponse);
  rpc InstallSnapshot(InstallSnapshotRequest) returns (InstallSnapshotResponse);
}

// ─── Shard / CF message types ─────────────────────────────────────────────────

enum ShardStatusProto {
  SHARD_NORMAL    = 0;
  SHARD_SPLITTING = 1;
  SHARD_MERGING   = 2;
  SHARD_MIGRATING = 3;
  SHARD_RECOVERING = 4;
}

message ShardInfoProto {
  uint64          shard_id    = 1;
  string          cf          = 2;
  bytes           start_key   = 3;
  bytes           end_key     = 4;
  uint64          leader_node = 5;
  repeated uint64 replicas    = 6;
  ShardStatusProto status     = 7;
  uint64          epoch       = 8;
}

message CfMetadataProto {
  string name      = 1;
  uint32 cf_id     = 2;
  uint32 cf_type   = 3; // 0=System, 1=User, 2=Graph
  string namespace = 4; // B-model authoritative owner
  string database  = 5;
}

// ─── GetShards ────────────────────────────────────────────────────────────────

message GetShardsRequest {}

message GetShardsResponse {
  repeated ShardInfoProto shards = 1;
}

// ─── GetColumnFamilies ────────────────────────────────────────────────────────

message GetColumnFamiliesRequest {}

message GetColumnFamiliesResponse {
  repeated CfMetadataProto column_families = 1;
}

// ─── ProposeOp ────────────────────────────────────────────────────────────────

message ProposeMetadataOpRequest {
  bytes op_json = 1; // JSON-encoded MetadataOp
}

message ProposeMetadataOpResponse {
  bool   success = 1;
  string error   = 2;
}

// ─── GetShardForKey ───────────────────────────────────────────────────────────

message GetShardForKeyRequest {
  string cf  = 1;
  bytes  key = 2;
}

message GetShardForKeyResponse {
  bool             found = 1;
  ShardInfoProto   shard = 2;
}

// ─── RegisterNode ─────────────────────────────────────────────────────────────

message NodeInfoProto {
  uint64 node_id   = 1; // unique node id
  string data_addr = 2; // gRPC address for client-facing data-plane requests
  string raft_addr = 3; // gRPC address for Raft peer-to-peer RPCs
}

message RegisterNodeRequest {
  NodeInfoProto node = 1;
}

message RegisterNodeResponse {
  bool   success = 1;
  string error   = 2;
}

// ─── SubscribeShardEvents ─────────────────────────────────────────────────────

message SubscribeShardEventsRequest {
  uint64 node_id = 1; // id of the subscribing data node
}

// A single committed metadata event, encoded as a JSON MetadataOp.
// On initial connection the server first replays the full current state as
// synthetic events before streaming live updates.
message ShardEventProto {
  bytes op_json = 1; // serde_json-encoded MetadataOp
}

// ─── Vector Index ────────────────────────────────────────────────────────────

enum VectorDistanceMetric {
  VECTOR_L2           = 0; // Squared Euclidean distance
  VECTOR_COSINE       = 1; // 1 - cosine_similarity
  VECTOR_INNER_PRODUCT = 2; // Negative dot product
}

// The type of vector index — determines quantization / structure.
enum VectorIndexType {
  VECTOR_INDEX_HNSW    = 0; // Plain HNSW (exact distances)
  VECTOR_INDEX_PQ_HNSW = 1; // PQ-accelerated HNSW
  VECTOR_INDEX_SQ_HNSW = 2; // SQ8-accelerated HNSW
  VECTOR_INDEX_IVF_PQ  = 3; // IVF with PQ fine quantization
  VECTOR_INDEX_IVF_SQ  = 4; // IVF with SQ8 fine quantization
  VECTOR_INDEX_SPFRESH     = 5; // SPFresh IVF-PQ with LIRE incremental rebalancing
  VECTOR_INDEX_SPFRESH_LSM = 6; // DEPRECATED/RETIRED: route to a graph SpFresh index; create returns FailedPrecondition
  VECTOR_INDEX_DISKANN     = 7; // DiskANN/Vamana SSD-resident graph (PQ in RAM, vectors+graph on SSD)
  VECTOR_INDEX_MULTIVECTOR = 8; // ColBERT-style late-interaction multi-vector index
}

// Configuration for a vector index.
message VectorIndexConfig {
  uint32              dim             = 1; // Vector dimensionality
  VectorDistanceMetric metric         = 2; // Distance metric
  uint32              m               = 3; // Max connections per layer (default 16)
  uint32              m_max0          = 4; // Max connections at layer 0 (default 2*m)
  uint32              ef_construction = 5; // Build-time search width (default 200)
  uint32              ef_search       = 6; // Default query-time search width (default 64)
  VectorIndexType     index_type      = 7; // Index type (default: plain HNSW)
  // ── IVF / SPFresh fields ────────────────────────────────────────────────────
  uint32 nlist                = 8;  // Number of Voronoi cells (coarse clusters)
  uint32 nprobe               = 9;  // Cells to probe at query time
  uint32 pq_num_sub           = 10; // PQ: number of sub-quantizers
  uint32 pq_num_centroids     = 11; // PQ: centroids per sub-quantizer (default 256)
  uint32 pq_max_iter          = 12; // PQ: k-means iterations (default 20)
  uint32 ivf_max_iter         = 13; // IVF: coarse k-means iterations (default 20)
  uint32 split_threshold      = 14; // SPFresh: posting list split threshold (0 = auto)
  uint32 merge_threshold      = 15; // SPFresh: posting list merge threshold (0 = auto)
  float  compact_delete_ratio = 16; // SPFresh: compaction trigger ratio (default 0.3)
  // ── Coarse centroid routing fields ──────────────────────────────────────────
  uint32 nlist_coarse         = 17; // Number of coarse centroids for distributed routing (0 = disable)
  uint32 nprobe_coarse        = 18; // Coarse centroids to probe at search time (default: 1)
  // ── DiskANN / Vamana fields ──────────────────────────────────────────────────
  uint32 diskann_degree       = 19; // DiskANN: max graph out-degree R (default 64)
  uint32 diskann_search_list  = 20; // DiskANN: build/search candidate list L (default 100)
  float  diskann_alpha        = 21; // DiskANN: RobustPrune alpha relaxation (default 1.2)
  // ── Payload secondary indexes (roaring-bitmap / field index) ──────────────────
  // Fields whose payload values get a secondary index for fast pre-filtering.
  // Omitted/empty => no payload index (behavior identical to today's O(n) probe).
  repeated PayloadFieldIndex indexed_fields = 22;
  // ── Multi-vector / late-interaction fields ─────────────────────────────────
  uint32 multivector_candidates_per_token = 23; // Per query token ANN candidates (0 = default 32)
  // ── Tenant-partitioned vector index (multitenancy) ──────────────────────────
  // Designate a payload field as the tenant key. When non-empty, the index becomes
  // a tenant router: tenant-equality filtered searches are scoped to that tenant's
  // sub-structure. Empty => None => behavior identical to today. Graph-backed index
  // types only (HNSW/PQ_HNSW/SQ_HNSW); a tenant_key on any other type is rejected.
  string tenant_key = 24;
  uint32 tenant_promote_threshold = 25; // Promote-to-dedicated-subgraph size (0 = engine default; Phase 2)
}

// A payload field to secondary-index for fast filter pre-filtering.
message PayloadFieldIndex {
  string           field = 1; // Payload field name
  PayloadFieldKind kind  = 2; // EQUALITY (bitmap) or RANGE (sorted)
}

// The kind of secondary index built for a payload field.
enum PayloadFieldKind {
  PAYLOAD_FIELD_EQUALITY = 0; // keyword/bool/exact int/str -> roaring bitmap
  PAYLOAD_FIELD_RANGE    = 1; // numeric -> sorted (value,id)
}

// ─── CreateVectorIndex ───────────────────────────────────────────────────────

message CreateVectorIndexRequest {
  string            index_name = 1; // Unique name for this index
  VectorIndexConfig config     = 2;
}

message CreateVectorIndexResponse {}

// ─── DropVectorIndex ─────────────────────────────────────────────────────────

message DropVectorIndexRequest {
  string index_name = 1;
}

message DropVectorIndexResponse {}

// ─── VectorPut ───────────────────────────────────────────────────────────────

message VectorPutRequest {
  string         index_name = 1;
  uint64         vector_id  = 2; // User-assigned vector id
  repeated float vector     = 3; // The embedding vector
  // Optional typed metadata payload attached to this vector id. Persisted and
  // replicated through the same WAL/Raft record as the vector, and queried via
  // VectorSearchRequest.filter. Additive/wire-compatible: old clients omit it.
  map<string, VectorFilterValue> attributes = 4;
}

message VectorPutResponse {}

// ─── VectorDelete ────────────────────────────────────────────────────────────

message VectorDeleteRequest {
  string index_name = 1;
  uint64 vector_id  = 2;
}

message VectorDeleteResponse {}

// ─── VectorSearch ────────────────────────────────────────────────────────────

// A multi-vector (ColBERT-style) query matrix: `num_tokens * dim` floats stored
// row-major in `values`, with `dim` the per-token dimensionality. Used for
// late-interaction MaxSim search over a multi-vector index.
message MultiVectorQuery {
  uint32         dim    = 1; // Per-token dimensionality (stride into `values`).
  repeated float values = 2; // Flat row-major matrix: num_tokens * dim floats.
}

// ─── Second-stage reranking (optional) ───────────────────────────────────────
//
// First-class, optional second-stage reranker over an over-fetched candidate
// window for plain VectorSearch / HybridSearch. Mirrors Weaviate reranker
// modules, Pinecone's rerank API, Vespa global-phase rank-profiles, and
// Cohere/Qdrant rerank: a stateless, bounded, post-merge stage run *once*
// globally after shard fan-in. Additive & wire-compatible — old clients omit
// this message ⇒ rerank off, behavior identical to today.
message RerankSpec {
  bool   enabled       = 1; // master switch (false / message-absent ⇒ no rerank)
  // over-fetch pool size; 0 ⇒ default max(k*4, 24) clamped to the mode's hard
  // cap (48 for cross-encoder; 200 for the cheaper maxsim late-interaction
  // path, sized for the issue's "top-K ~100-200" fused window).
  uint32 rerank_k      = 2;
  // "cross-encoder" | "maxsim" | "learned" | "score-fusion" | "custom";
  // "" ⇒ "score-fusion".
  //   * "maxsim" — token-level ColBERT-style MaxSim late interaction: the query
  //     and each hydrated passage are encoded into per-token L2-normalized
  //     matrices and scored Σ_q max_d q·d ÷ query-token-count. LLM-free; needs
  //     the embedding model loaded + query_text + passage_field, else it
  //     degrades to score-fusion (never errors).
  //   * "learned" — (#768 LongMemEval Phase 7) non-generative GBDT reranker over
  //     the funnel feature vector [dense, BM25, MaxSim, recency, graph-distance,
  //     granularity], trained on LongMemEval dev relevance labels. Applied as the
  //     FINAL rerank over the fused+reranked candidate set. Needs a model loaded
  //     (STATELET_LEARNED_RERANKER_MODEL); else degrades to score-fusion. LLM-free.
  string model         = 3;
  // KV-key template for passage hydration (cross-encoder / maxsim). Tokens:
  //   {id}    → candidate vector/result id (decimal)
  //   {index} → index_name
  // e.g. "doc:{index}:{id}:text". Empty ⇒ hydration disabled ⇒ fall back to score-fusion.
  string passage_field = 4;
  // score-fusion only: blend = blend*norm_distance + (1-blend)*aux_signal.
  // 0 ⇒ default 1.0 (pure full-precision/coarse distance re-rank). Range [0,1].
  float  signal_blend  = 5;
  // Raw query *text* for the text-based rerankers (both RPCs query by vector,
  // not text). Required for model="cross-encoder" and model="maxsim"; empty ⇒
  // auto-downgrade to score-fusion.
  string query_text    = 6;
  // Dry-run pre-flight validation. When true the RPC validates the rerank spec
  // (passage_field template via validate_passage_field, plus reranker
  // availability for model="cross-encoder") and returns an empty successful
  // response if valid, or an InvalidArgument/FailedPrecondition Status if not —
  // without executing the search. SDKs expose it as rerank_validate(...).
  // Additive & wire-compatible: old clients omit it ⇒ false ⇒ unchanged.
  bool   validate_only = 7;
}

message VectorSearchRequest {
  string         index_name = 1;
  repeated float query      = 2; // Query vector (single-vector indices).
  uint32         k          = 3; // Number of nearest neighbors to return
  uint32         ef_search  = 4; // Optional: override ef_search (0 = use default)
  VectorFilter   filter     = 5; // Optional: metadata/attribute pre/post filter (null = no filter)
  // Query payload selector. `query` (field 2) carries a single-vector query for
  // the classic ANN path; `multi_vector` carries a token matrix for the
  // late-interaction MaxSim path against a multi-vector index. At most one of
  // the two should be populated.
  oneof query_payload {
    MultiVectorQuery multi_vector = 6;
  }
  // ── MMR diversity reranking (optional, single-vector path only) ──
  // Maximal-marginal-relevance post-step: over-fetch candidates, then greedily
  // select k maximizing  lambda*sim(q,d) - (1-lambda)*max_{s in selected} sim(d,s).
  // Additive & wire-compatible: old clients omit these ⇒ MMR off, behavior
  // unchanged. The returned `distance` is always the original query distance;
  // only the selected set and ordering change.
  bool   mmr        = 7; // enable MMR post-step
  float  mmr_lambda = 8; // 0..1 relevance↔diversity tradeoff; 0 ⇒ default 0.5
  uint32 mmr_pool   = 9; // over-fetch multiplier; 0 ⇒ default 4 (fetch_k = k*mmr_pool)
  // Optional second-stage rerank (cross-encoder or model-free score-fusion).
  // Absent ⇒ no rerank.
  RerankSpec rerank = 10;
  // ── Filtered-search planner override (optional) ──
  // Pin the selectivity-adaptive filtered-search strategy instead of letting
  // the cardinality planner choose. Only meaningful when `filter` is set and
  // the target index is graph-backed (Plain/PQ/SQ HNSW); ignored otherwise.
  //   0 = auto (planner chooses from the estimated selectivity) [default]
  //   1 = brute-force exact scan over the matching ids
  //   2 = filtered-HNSW (traverse-through-non-matching, admit-if-match)
  //   3 = ACORN two-hop neighbor expansion
  // Additive & wire-compatible: old clients omit it ⇒ 0 ⇒ unchanged behavior.
  uint32 planner_override = 11;
  // ── Result grouping / field-collapse (optional, single-vector path only) ──
  // Collapse results to at most `group_size` hits per distinct value of the
  // payload attribute `group_field`, returning `groups` distinct group keys
  // total ("one best chunk per document"). Pure post-search collapse over the
  // post-filter candidate pool, structurally identical to the MMR over-fetch /
  // re-select path. Additive & wire-compatible: old clients omit these ⇒
  // grouping off ⇒ behavior byte-identical. Grouping and `mmr` are mutually
  // exclusive (orthogonal selection rules) and requesting both ⇒ InvalidArgument.
  string group_field    = 12; // payload field to group by; empty ⇒ grouping off
  uint32 group_size     = 13; // max hits per group; 0 ⇒ default 1 (one-best-per-group)
  uint32 groups         = 14; // number of distinct group keys to return; 0 ⇒ fall back to k
  uint32 group_overfetch = 15; // over-fetch multiplier; 0 ⇒ default 4, capped at MAX_POOL
  // Missing-field policy. By default (false) candidates that lack `group_field`
  // are dropped from grouped results (matches Qdrant/Weaviate). When true, each
  // missing-field candidate is returned as its own singleton group (group_key
  // empty), counting against the `groups` cap. Additive/wire-compatible: old
  // clients omit it ⇒ false ⇒ drop behavior unchanged from Phase 1.
  bool   group_missing_as_own = 16;
}

// ─── Metadata / attribute filtering for vector search ────────────────────────
//
// A filter is a boolean tree of leaf conditions combined with AND/OR. Leaves
// compare an attribute `field` against a `value` using a comparison `op`.
// Attribute values are typed (string, int, double, bool); comparison between
// mismatched types evaluates to false rather than erroring.

enum VectorFilterOp {
  VECTOR_FILTER_OP_EQ  = 0; // field == value
  VECTOR_FILTER_OP_NE  = 1; // field != value
  VECTOR_FILTER_OP_LT  = 2; // field <  value (numeric)
  VECTOR_FILTER_OP_LTE = 3; // field <= value (numeric)
  VECTOR_FILTER_OP_GT  = 4; // field >  value (numeric)
  VECTOR_FILTER_OP_GTE = 5; // field >= value (numeric)
}

message VectorFilterValue {
  oneof value {
    string string_value = 1;
    int64  int_value    = 2;
    double double_value = 3;
    bool   bool_value   = 4;
  }
}

message VectorFilterCondition {
  string            field = 1;
  VectorFilterOp    op    = 2;
  VectorFilterValue value = 3;
}

// A filter node is either a single leaf condition, or a boolean combination
// (AND/OR) of child filter nodes. Exactly one of the three should be set.
message VectorFilter {
  message And { repeated VectorFilter filters = 1; }
  message Or  { repeated VectorFilter filters = 1; }
  oneof node {
    VectorFilterCondition condition = 1;
    And                   and       = 2;
    Or                    or        = 3;
  }
}

message VectorSearchResult {
  uint64 id       = 1;
  float  distance = 2;
  // Group key for field-collapse results (see VectorSearchRequest.group_field).
  // Empty when grouping is off. Load-bearing for cross-shard merge: the gateway
  // groups across shards on this key. Additive/wire-compatible.
  string group_key = 3;
}

message VectorSearchResponse {
  repeated VectorSearchResult results = 1;
}

// ─── VectorBatchPut ──────────────────────────────────────────────────────────

message VectorBatchPutEntry {
  uint64         vector_id = 1;
  repeated float vector    = 2;
  // Optional typed metadata payload for this entry (see VectorPutRequest.attributes).
  map<string, VectorFilterValue> attributes = 3;
}

message VectorBatchPutRequest {
  string                       index_name = 1;
  repeated VectorBatchPutEntry vectors    = 2;
}

message VectorBatchPutResponse {
  uint32 inserted = 1; // Number of vectors successfully inserted
}

// ─── VectorBatchDelete ───────────────────────────────────────────────────────

message VectorBatchDeleteRequest {
  string          index_name = 1;
  repeated uint64 vector_ids = 2;
}

message VectorBatchDeleteResponse {
  uint32 deleted = 1; // Number of vectors actually removed
  // Vector ids whose owning node could not be deleted in this request. Additive
  // for coarse-routed distributed deletes: callers can distinguish an explicit
  // partial success from a failed all-or-nothing operation.
  repeated uint64 failed_vector_ids = 2;
  string partial_error = 3;
}

// ─── VectorTrain ────────────────────────────────────────────────────

message VectorTrainRequest {
  string index_name = 1;
  repeated float centroids = 2; // Pre-trained centroids (empty = train locally)
  uint32 dim = 3;               // Dimensionality (needed when centroids is non-empty)
}

message VectorTrainResponse {}

// ─── VectorGet ───────────────────────────────────────────────────────────────

message VectorGetRequest {
  string index_name = 1;
  uint64 vector_id  = 2;
}

message VectorGetResponse {
  bool           found  = 1;
  repeated float vector = 2;
}

// ─── VectorSample ─────────────────────────────────────────────────────────

message VectorSampleRequest {
  string index_name   = 1;
  uint32 max_samples  = 2;
}

message VectorSampleResponse {
  repeated float vectors = 1;
  uint32         dim     = 2;
  uint32         count   = 3;
}

// ─── VectorExport (retirement migration, P7) ────────────────────────────────

message VectorExportRequest {
  string index_name = 1;
  uint64 after_id   = 2; // exclusive lower bound; 0 starts from the beginning
  uint32 limit      = 3; // max entries per page (server clamps; 0 = default)
}

message VectorExportEntry {
  uint64         vector_id = 1;
  repeated float vector    = 2;
}

message VectorExportResponse {
  repeated VectorExportEntry entries = 1;
  bool done = 2; // true when this page exhausted the index
  // Index schema carried on every page so the migration driver can create the
  // target graph without a separate metadata round trip (VectorIndexMeta does
  // not record the metric).
  string metric = 3; // "l2" | "cosine" | "ip"
  uint32 dim    = 4;
}

// ─── Sparse ingest + hybrid (dense + sparse) retrieval ────────────────────
//
// A *generic* learned-sparse retrieval path bound to a plain vector index
// (independent of agent-memory graph ingestion). `SparseIngest` populates a
// per-index inverted posting store; `HybridSearch` runs dense ANN + sparse
// top-n and fuses them with RRF or weighted (alpha) fusion. The gateway fans
// `HybridSearch` out to all shards (returning per-shard fusion *inputs* via
// `return_inputs`) and performs the global fusion.

// A single sparse document: doc id + term->weight map. If `text` is set and
// `weights` is empty, the server tokenizes `text` (shared BM25 tokenizer) and
// uses term frequencies as weights.
message SparseDocProto {
  uint64               doc_id  = 1;
  map<string, float>   weights = 2;
  string               text    = 3;
}

message SparseIngestRequest {
  string                  index_name = 1;
  repeated SparseDocProto docs       = 2;
}

message SparseIngestResponse {
  uint32 ingested = 1; // Number of documents indexed
}

enum FusionMode {
  FUSION_MODE_RRF      = 0; // Reciprocal Rank Fusion (rank-based)
  FUSION_MODE_WEIGHTED = 1; // Score-normalized weighted (alpha) fusion
}

message FusionSpecProto {
  FusionMode mode          = 1;
  float      rrf_k         = 2; // RRF k constant (default 60 when 0)
  float      dense_weight  = 3; // RRF: dense list weight (default 1)
  float      sparse_weight = 4; // RRF: sparse list weight (default 1)
  float      alpha         = 5; // Weighted: alpha*dense + (1-alpha)*sparse
}

message HybridSearchRequest {
  string             index_name   = 1;
  repeated float     dense_query  = 2; // Dense query vector
  map<string, float> sparse_query = 3; // Sparse query: term->weight
  string             sparse_text  = 4; // If sparse_query empty, tokenize this
  uint32             k            = 5; // Top-k results to return
  uint32             ef_search    = 6; // Optional dense ef override (0 = default)
  FusionSpecProto    fusion       = 7;
  // When true (gateway → data node), the node returns its raw dense + sparse
  // ranked lists as fusion inputs instead of pre-fused results, so the gateway
  // can fuse globally across shards.
  bool               return_inputs = 8;
  // Optional second-stage rerank applied after global fusion. Absent ⇒ no rerank.
  RerankSpec         rerank        = 9;
}

// One ranked (id, score) entry. For dense inputs `score` is the distance
// (lower better); for sparse inputs and fused results it is a score (higher
// better).
message RankedEntry {
  uint64 id    = 1;
  float  score = 2;
}

// Per-shard raw BM25 sparse inputs, returned alongside `sparse_inputs` when
// return_inputs=true so the gateway can rescore every candidate against
// CORPUS-WIDE statistics (global N / df / avgdl) instead of fusing scores that
// were each computed with shard-local statistics and so are not comparable.
//
// `sparse_local_n` / `sparse_local_sum_dl` are this shard's contribution to the
// global document count and document-length sum (the gateway sums them to get
// global N and avgdl). `sparse_term_df` is this shard's per-query-term document
// frequency (summed to global df). `sparse_doc_inputs` carries, per candidate
// doc, its length and its per-term term frequencies so the gateway can apply
// the exact BM25 formula with the aggregated stats.
message SparseTermDf {
  string term = 1;
  uint64 df   = 2; // posting-list length on this shard
}

message SparseTermTf {
  string term = 1;
  float  tf   = 2; // term frequency of `term` in this doc on this shard
}

message SparseDocInput {
  uint64               id       = 1;
  uint32               doc_len  = 2; // 0 ⇒ length unknown (gateway uses avgdl)
  repeated SparseTermTf term_tfs = 3;
}

message HybridSearchResponse {
  // Populated when return_inputs=false: globally fused / locally fused results.
  repeated RankedEntry results = 1;
  // Populated when return_inputs=true: raw per-shard fusion inputs. The dense
  // distances are globally comparable; `sparse_inputs` carries the shard-local
  // BM25 score and is kept for backward compatibility / fallback only — prefer
  // the corpus-wide rescore built from the raw fields below.
  repeated RankedEntry dense_inputs  = 2;
  repeated RankedEntry sparse_inputs = 3;
  // Raw BM25 inputs for corpus-wide sparse rescoring (return_inputs=true).
  uint64               sparse_local_n      = 4;
  uint64               sparse_local_sum_dl = 5;
  repeated SparseTermDf sparse_term_df     = 6;
  repeated SparseDocInput sparse_doc_inputs = 7;
}

// ─── TrainVectorIndex (metadata service orchestrated) ─────────────────────

message TrainVectorIndexRequest {
  string index_name = 1;
}

message TrainVectorIndexResponse {}

// ─── TrainCoarseRouting (metadata service orchestrated, SPFresh two-level) ──

message TrainCoarseRoutingRequest {
  string index_name       = 1;
  uint32 nlist_coarse     = 2; // Number of coarse centroids to train
  uint32 nprobe_coarse    = 3; // Coarse centroids to probe at search time (default: 1)
  uint32 max_iter         = 4; // k-means iterations (default: 20)
  uint32 samples_per_node = 5; // Vectors to sample per node (default: 1000)
}

message TrainCoarseRoutingResponse {
  uint32 num_centroids = 1;
}

// ─── GetNodeStats ──────────────────────────────────────────────────────────

message CheckpointRequest {}

message CheckpointResponse {
  // Shards whose state machine flush was triggered.
  uint64 shards_flushed = 1;
  // Idle/live shards whose WAL TRUNCATE floor was force-advanced.
  uint64 floors_released = 2;
  // On-disk WAL segment count (incl. active) before / after the checkpoint.
  uint64 segments_before = 3;
  uint64 segments_after = 4;
  // Total WAL bytes before / after.
  uint64 wal_bytes_before = 5;
  uint64 wal_bytes_after = 6;
}

message GetNodeStatsRequest {}

message CfStatsProto {
  uint32 cf_id                 = 1;
  string cf_name               = 2;
  uint64 memtable_memory_bytes = 3;
  uint64 memtable_entry_count  = 4;
  uint64 sst_file_count        = 5;
  uint64 sst_total_bytes       = 6;
  uint64 sst_entry_count       = 7;
  // Column family type: 0=System, 1=User, 2=Graph
  uint32 cf_type               = 8;
}

message VectorIndexStatsProto {
  string index_name    = 1;
  string index_type    = 2; // "HNSW", "PQ_HNSW", "SQ_HNSW", "IVF_PQ", "IVF_SQ", "SPFresh", "TENANTED"
  uint32 dim           = 3;
  uint64 num_vectors   = 4;
  // Tenant tiering (epic #1428, Phase 2). Only meaningful for "TENANTED"
  // indices; 0 for every other index type. `promoted_tenants` is the number of
  // tenants that have graduated to a dedicated O(tenant) sub-graph, and
  // `fallback_vectors` is how many vectors remain in the shared small-tenant
  // fallback graph — so a client can observe the O(tenant) tiering via stats.
  uint32 promoted_tenants  = 5;
  uint64 fallback_vectors  = 6;
}

message ShardStatsProto {
  uint64                  shard_id = 1;
  repeated CfStatsProto   cf_stats = 2;
  // Approximate on-disk bytes for THIS shard's key range on THIS node, from
  // the SST index boundaries — the same estimate auto-split/auto-merge decide
  // on. `cf_stats` cannot answer this: all shards on a node share one DB, so
  // SST bytes are tallied per CF per node and attributed wholesale to the
  // lowest shard id. An estimate, and per replica.
  uint64                  approx_bytes = 3;
}

message GetNodeStatsResponse {
  uint64                   node_id     = 1;
  repeated ShardStatsProto shard_stats = 2;
  // System resource usage
  double cpu_usage_percent   = 3;
  uint64 memory_used_bytes   = 4;
  uint64 memory_total_bytes  = 5;
  uint64 disk_used_bytes     = 6;
  uint64 disk_total_bytes    = 7;
  // Storage-engine metrics (aggregated from local Prometheus counters)
  double wal_bytes_written_total       = 8;
  double block_cache_hits_total        = 9;
  double block_cache_misses_total      = 10;
  double block_cache_evictions_total   = 11;
  double memtable_freeze_total         = 12;
  double memtable_flush_total          = 13;
  // WAL file size on disk (bytes) — sum across all shards on this node.
  uint64 wal_file_size_bytes           = 14;
  // Number of shared raft-wal segment files on disk (GC health signal).
  uint64 wal_file_count                = 19;
  // Per-index vector stats on this node.
  repeated VectorIndexStatsProto vector_index_stats = 15;
  // Cross-shard ACID transactions (epic #1478, Phase 1). `cross_shard_txn_enabled`
  // reflects the effective gate (env flag OR cluster option); default false.
  // `hlc_physical_ms` / `hlc_logical` are the node's current Hybrid Logical
  // Clock reading (physical Unix-ms + logical tie-break), 0 when the feature is
  // OFF (the clock is not sampled). Surfaces the flag + clock for observability.
  bool   cross_shard_txn_enabled = 16;
  uint64 hlc_physical_ms         = 17;
  uint32 hlc_logical             = 18;
}

// ─── GetClusterClock (epic #1478, Phase 1) ─────────────────────────────────

message GetClusterClockRequest {}

message GetClusterClockResponse {
  // This node's identity.
  uint64 node_id          = 1;
  // Current Hybrid Logical Clock reading: physical component (Unix ms) and the
  // 16-bit logical tie-break. Packed form is `physical_ms << 16 | logical`.
  uint64 hlc_physical_ms  = 2;
  uint32 hlc_logical      = 3;
  // The packed single-integer HLC (`physical_ms << 16 | logical`) — the form
  // carried inside replicated records and on the wire.
  uint64 hlc_packed       = 4;
  // Whether cross-shard transactions are enabled on this node (env flag OR
  // cluster option). When false the returned HLC fields are 0 (not sampled).
  bool   cross_shard_txn_enabled = 5;
}

// ─── ReportNodeStats ──────────────────────────────────────────────────────

message ReportNodeStatsRequest {
  // Reuses the same payload as GetNodeStatsResponse.
  uint64                   node_id     = 1;
  repeated ShardStatsProto shard_stats = 2;
}

message ReportNodeStatsResponse {}

// ═══════════════════════════════════════════════════════════════════════════
// Agent State DB
// ═══════════════════════════════════════════════════════════════════════════

// Client-facing gateway service for agent state operations.
// The gateway generates execution plans locally and routes to data nodes.
service AgentStateService {
  // ── Branch (fork) operations ─────────────────────────────────────────
  rpc Fork(AgentForkRequest) returns (AgentForkResponse);
  rpc MergeBranch(AgentMergeBranchRequest) returns (AgentMergeBranchResponse);
  rpc DiscardBranch(AgentDiscardBranchRequest) returns (AgentDiscardBranchResponse);
  rpc ListBranches(AgentListBranchesRequest) returns (AgentListBranchesResponse);
  rpc BranchPut(AgentBranchPutRequest) returns (AgentBranchPutResponse);
  rpc BranchGet(AgentBranchGetRequest) returns (AgentBranchGetResponse);

  // ── Causal graph operations ──────────────────────────────────────────
  rpc AddStep(AgentAddStepRequest) returns (AgentAddStepResponse);
  rpc AddEdge(AgentAddEdgeRequest) returns (AgentAddEdgeResponse);
  rpc GetStep(AgentGetStepRequest) returns (AgentGetStepResponse);
  rpc GetContent(AgentGetContentRequest) returns (AgentGetContentResponse);
  rpc GetEdges(AgentGetEdgesRequest) returns (AgentGetEdgesResponse);
  rpc Traverse(AgentTraverseRequest) returns (AgentTraverseResponse);
  rpc FindSimilarChains(AgentFindSimilarChainsRequest) returns (AgentFindSimilarChainsResponse);

  // ── Reactive state operations ────────────────────────────────────────
  rpc CasPut(AgentCasPutRequest) returns (AgentCasPutResponse);
  rpc TxnCommit(AgentTxnCommitRequest) returns (AgentTxnCommitResponse);

  // ── Coordination primitives (claim / lease / renew / release) ─────────
  // Native etcd/Consul/Zookeeper-style coordination: a claim is an atomic
  // SetIfNotExists(claim_key, agent_id); a lease is the same with a TTL so an
  // un-renewed holder auto-expires; renew/release are fenced so only the live
  // holder can extend/drop the key. See issue #691.
  rpc Claim(AgentClaimRequest) returns (AgentClaimResponse);
  rpc Lease(AgentLeaseRequest) returns (AgentLeaseResponse);
  rpc Renew(AgentRenewRequest) returns (AgentRenewResponse);
  rpc Release(AgentReleaseRequest) returns (AgentReleaseResponse);

  // ── Temporal graph operations ────────────────────────────────────────
  rpc ExpireEdge(AgentExpireEdgeRequest) returns (AgentExpireEdgeResponse);
  rpc EdgeHistory(AgentEdgeHistoryRequest) returns (AgentEdgeHistoryResponse);
  // ── Memory-scope provenance audit (#697 phase 4) ──────────────────────
  // Gated by ManageMemoryScope; the gateway authorizes then forwards to the
  // data node that owns the `_agent_provenance` CF.
  rpc QueryProvenance(AgentQueryProvenanceRequest) returns (AgentQueryProvenanceResponse);
  // ── Memory-scope team-membership admin (#697 phase 2c / #794) ─────────
  // Grant/revoke an agent's team membership; gated by ManageMemoryScope. The
  // gateway authorizes then applies a durable metadata-Raft op so the change
  // immediately affects Team-scope reads after the next internal-token refresh.
  rpc AgentManageTeamGrant(AgentManageTeamGrantRequest) returns (AgentManageTeamGrantResponse);
  // ── Team time-travel (#787) — gateway facade fan-out/fence/merge ──────
  rpc AgentTeamSnapshotLocal(TeamSnapshotLocalRequest) returns (TeamSnapshotLocalResponse);
  rpc AgentTeamDiffLocal(TeamDiffLocalRequest) returns (TeamDiffLocalResponse);
  // Cascade-expire a fact and its derived dependents (#693).
  rpc CascadeExpire(AgentCascadeExpireRequest) returns (AgentCascadeExpireResponse);
  // Supersede a fact with a replacement, optionally cascading (#693 phase 4).
  rpc SupersedeFact(AgentSupersedeFactRequest) returns (AgentSupersedeFactResponse);
  // Transactional memory ingest (#780): forwarded to the owning data node's
  // AgentMemoryIngest (atomic, snapshot-isolated dedup/create/supersede).
  rpc MemoryIngest(AgentMemoryIngestRequest) returns (AgentMemoryIngestResponse);

  // ── Bitemporal belief queries ("who believed what, when") ─────────────
  rpc BeliefQuery(AgentBeliefQueryRequest) returns (AgentBeliefQueryResponse);
  rpc BeliefDivergence(AgentBeliefDivergenceRequest) returns (AgentBeliefDivergenceResponse);

  // ── Durable agent execution (#846, epic #699 / sub-epic #792) ─────────
  // Run-id-pinned routing: StartRun picks a shard by hashing run_key/agent_id
  // through the ShardRouter and returns run_id = (shard_id << 40) | local_seq;
  // every subsequent call extracts the owning shard as run_id >> 40 — no
  // metadata lookup needed to route. The gateway resolves that shard's leader
  // and retries on NotLeader.
  rpc StartRun(AgentStartRunRequest) returns (AgentStartRunResponse);
  rpc RunStep(AgentRunStepRequest) returns (AgentRunStepResponse);
  rpc CompleteStep(AgentCompleteStepRequest) returns (AgentCompleteStepResponse);
  rpc CheckpointGet(AgentCheckpointGetRequest) returns (AgentCheckpointGetResponse);
  rpc CheckpointLatest(AgentCheckpointLatestRequest) returns (AgentCheckpointLatestResponse);
  rpc ProvenanceChainQuery(AgentProvenanceChainQueryRequest) returns (AgentProvenanceChainQueryResponse);
  rpc ResumeFromStep(AgentResumeFromStepRequest) returns (AgentResumeFromStepResponse);
  rpc ResumeSemantic(AgentResumeSemanticRequest) returns (AgentResumeSemanticResponse);
  rpc GetRunStatus(AgentGetRunStatusRequest) returns (AgentGetRunStatusResponse);
  // Phase 5 (#797): branch/time-travel resume. Routed by source_run_id >> 40 to
  // the shard owning the source run; the child run is allocated on that same
  // shard so its self-routing run_id stays addressable.
  rpc ForkRun(AgentForkRunRequest) returns (AgentForkRunResponse);
  rpc ForkAcrossCandidates(AgentForkAcrossCandidatesRequest) returns (AgentForkAcrossCandidatesResponse);
  rpc ArtifactPut(AgentArtifactPutRequest) returns (AgentArtifactPutResponse);
  rpc ArtifactGet(AgentArtifactGetRequest) returns (AgentArtifactGetResponse);
  rpc ArtifactResolve(AgentArtifactResolveRequest) returns (AgentArtifactResolveResponse);

  // ── Streaming ops ────────────────────────────────────────────────────
  rpc WatchPrefix(AgentWatchPrefixRequest) returns (stream AgentWatchEventProto);
}

// ─── Agent: Branch messages ─────────────────────────────────────────────────

message AgentForkRequest {
  string label            = 1;
  uint64 parent_branch_id = 2; // 0 = fork from main timeline
}

message AgentForkResponse {
  uint64 branch_id = 1;
}

message AgentMergeBranchRequest {
  uint64 branch_id = 1;
}

message AgentMergeBranchResponse {}

message AgentDiscardBranchRequest {
  uint64 branch_id = 1;
}

message AgentDiscardBranchResponse {}

message AgentListBranchesRequest {}

message AgentBranchMetaProto {
  uint64 id                  = 1;
  uint64 parent_id           = 2;
  uint64 parent_snapshot_seq = 3;
  uint64 created_at          = 4;
  string status              = 5; // "Active", "Merged", "Discarded"
  string label               = 6;
}

message AgentListBranchesResponse {
  repeated AgentBranchMetaProto branches = 1;
}

message AgentBranchPutRequest {
  uint64 branch_id = 1;
  uint32 cf        = 2;
  bytes  key       = 3;
  bytes  value     = 4;
}

message AgentBranchPutResponse {}

message AgentBranchGetRequest {
  uint64 branch_id = 1;
  uint32 cf        = 2;
  bytes  key       = 3;
}

message AgentBranchGetResponse {
  bool  found = 1;
  bytes value = 2;
}

// ─── Agent: Causal graph messages ───────────────────────────────────────────

message AgentAddStepRequest {
  string agent_id     = 1;
  string step_type    = 2; // "Observe", "Think", "Act", "Tool", "Result"
  uint64 branch_id    = 3;
  bytes  content      = 4;
  bytes  metadata     = 5;
  repeated float embedding = 6; // optional embedding vector
  // Memory scope (issue #697 / #849). Empty `scope` defaults to "world"
  // (backward compatible). `scope_owner` is the team id for "team", the agent
  // id for "private", empty for "world". `field_acl_json` is an optional
  // JSON-encoded `[{"json_pointer","min_scope"}]` array of field-level ACL rules.
  string scope          = 7;  // "world" | "team" | "private"
  string scope_owner    = 8;  // team id / agent id; empty for world
  bytes  field_acl_json = 9;  // optional JSON [{json_pointer, min_scope}]
}

message AgentAddStepResponse {
  uint64 step_id = 1;
}

message AgentAddEdgeRequest {
  uint64 src_step_id = 1;
  uint64 dst_step_id = 2;
  string edge_type   = 3; // "Triggers", "Informs", "Branches", "Merges"
  bytes  props       = 4;
  uint64 valid_from  = 5; // 0 = use current time (backward compatible)
  uint64 valid_to    = 6; // 0 = permanent (backward compatible)
  // Provenance: the agent whose write created this belief. Empty = unknown
  // author (backward compatible); set to enable per-agent belief queries.
  string author_agent_id = 7;
}

message AgentAddEdgeResponse {}

message AgentGetStepRequest {
  uint64 step_id = 1;
}

message AgentGetStepResponse {
  bool   found      = 1;
  bytes  step_json  = 2; // JSON-serialized CausalStep
}

message AgentGetContentRequest {
  uint64 step_id = 1;
}

message AgentGetContentResponse {
  bool  found   = 1;
  bytes content = 2;
}

message AgentGetEdgesRequest {
  uint64 step_id   = 1;
  string direction = 2; // "forward", "backward"
  string edge_type = 3; // optional filter, empty = all types
  uint64 at_timestamp   = 4; // 0 = no temporal filter (all edges)
  uint64 window_start   = 5; // >0 with window_end = window query
  uint64 window_end     = 6;
}

message AgentEdgeProto {
  uint64 peer_step_id = 1;
  string edge_type    = 2;
  bytes  props        = 3;
  uint64 valid_from   = 4;
  uint64 valid_to     = 5;
}

message AgentGetEdgesResponse {
  repeated AgentEdgeProto edges = 1;
}

message AgentLocalTraverseRequest {
  uint64 start_step_id = 1;
  string direction     = 2; // "forward", "backward", "both"
  uint32 max_depth     = 3;
}

message AgentLocalTraverseResponse {
  repeated bytes step_jsons = 1; // JSON-serialized CausalStep array
  repeated AgentEdgeProto edges = 2;
}

message AgentTraverseRequest {
  uint64 start_step_id = 1;
  string direction     = 2;
  uint32 max_depth     = 3;
}

// ── Memory-scope provenance audit (#697 phase 4) ──────────────────────────
// Query the immutable provenance log for a time window, optionally narrowed to
// a single target step. Empty/zero `to_ts` means "now". `after_ts`/`after_seq`
// resume after the last record of a previous page; `limit` 0 = server default.
message AgentQueryProvenanceRequest {
  uint64 from_ts   = 1; // inclusive lower bound (ms since epoch); 0 = beginning
  uint64 to_ts     = 2; // inclusive upper bound (ms since epoch); 0 = now
  uint64 step_id   = 3; // optional: only records for this target step (0 = any)
  uint64 after_ts  = 4; // pagination cursor: resume strictly after (after_ts, after_seq)
  uint64 after_seq = 5;
  uint32 limit     = 6; // max records to return (0 = server default)
}

// One immutable provenance record (mirrors auth::scope::ProvenanceRecord). The
// payload is also carried as canonical JSON in `record_json` so the offline
// re-check tool can deserialize the exact `ProvenanceRecord` without re-deriving
// fields.
message AgentProvenanceRecordProto {
  uint64 decision_ts          = 1;
  uint64 seq                  = 2; // per-record monotonic sequence (key tiebreaker)
  string caller_user_id       = 3;
  string caller_agent_id      = 4; // empty = no agent principal on the token
  repeated string caller_team_ids = 5;
  string op                   = 6; // add_step | get_step | traverse | find_similar | get_edges
  uint64 target_step_id       = 7;
  string target_scope         = 8; // world | team | private
  string target_owner         = 9;
  string decision             = 10; // allowed | denied_scope | denied_field | admin_bypass
  repeated string denied_fields = 11; // populated for denied_field decisions
  string grants_snapshot_hash = 12; // SHA-256 hex of the caller's grant set
  bytes  record_json          = 13; // canonical JSON of the stored ProvenanceRecord
}

message AgentQueryProvenanceResponse {
  repeated AgentProvenanceRecordProto records = 1;
  uint64 next_after_ts  = 2; // pagination cursor for the next page (0 = exhausted)
  uint64 next_after_seq = 3;
}

// Grant or revoke an agent's membership of a team (#697 phase 2c / #794).
message AgentManageTeamGrantRequest {
  string agent_id = 1;
  string team_id  = 2;
  bool   revoke   = 3; // false = grant, true = revoke
}
message AgentManageTeamGrantResponse {
  bool   ok           = 1;
  uint64 effective_at = 2; // ms since epoch the change took effect
}

message AgentTraverseResponse {
  repeated bytes step_jsons = 1;
  repeated AgentEdgeProto edges = 2;
}

message AgentFindSimilarChainsRequest {
  repeated float query_embedding = 1;
  uint32 k           = 2; // number of similar chains to return
  uint32 chain_depth = 3; // BFS depth per anchor
  uint32 ef          = 4; // vector search ef (0 = default)
  // Optional HLC snapshot timestamp (packed u64) for cross-shard txn read-path
  // lock resolution (epic #1478, Phase 4). 0 = the gateway uses a fresh now().
  // Only consulted when STATELET_CROSS_SHARD_TXN is ON; otherwise ignored and
  // reads behave exactly as today.
  uint64 read_ts     = 5;
}

message AgentCausalChainProto {
  bytes  anchor_step_json = 1;
  float  distance         = 2;
  repeated bytes step_jsons = 3;
  repeated AgentEdgeProto edges = 4;
}

message AgentFindSimilarChainsResponse {
  repeated AgentCausalChainProto chains = 1;
}

// ─── Agent: Reactive state messages ─────────────────────────────────────────

message AgentCasPutRequest {
  uint32 cf           = 1;
  bytes  key          = 2;
  uint64 expected_seq = 3; // logical version from get_with_seq or current_version
  bytes  new_value    = 4;
}

message AgentCasPutResponse {
  bool   success    = 1;
  uint64 new_seq    = 2; // on success, the new logical version
  uint64 actual_seq = 3; // on conflict, the current logical version
}

// One read observed by an optimistic transaction: the (cf, key) and the
// snapshot seq it was read at.
message AgentTxnRead {
  uint32 cf           = 1;
  bytes  key          = 2;
  uint64 observed_seq = 3; // snapshot seq at read time
}

// One buffered write in an optimistic transaction. `delete = true` makes this a
// tombstone (the `value` field is ignored).
message AgentTxnWrite {
  uint32 cf     = 1;
  bytes  key    = 2;
  bytes  value  = 3;
  bool   delete = 4;
}

message AgentTxnCommitRequest {
  repeated AgentTxnRead  read_set  = 1;
  repeated AgentTxnWrite write_set = 2;
}

message AgentTxnCommitResponse {
  bool   committed     = 1; // true = applied, false = conflict-aborted
  uint64 commit_seq    = 2; // on commit, the engine MVCC seq after the write
  // On conflict, the offending key and the latest seq that beat the snapshot.
  uint32 conflict_cf   = 3;
  bytes  conflict_key  = 4;
  uint64 conflict_seq  = 5;
}

// ─── Agent: Coordination primitive messages (claim/lease/renew/release) ─────
//
// `key` is the caller-supplied coordination key; the server namespaces it into
// the dedicated coordination CF. `agent_id` is the claimant's identity. On a
// successful acquire the server returns a `fence` (the engine sequence the claim
// committed at) to carry on subsequent fenced writes; on a failed acquire it
// returns the current `holder` so the loser can observe who holds the key.

message AgentClaimRequest {
  bytes  key      = 1;
  string agent_id = 2;
}

message AgentClaimResponse {
  bool   acquired = 1; // true = caller now holds the key
  string holder   = 2; // on failure, the current holder's agent_id
  uint64 fence    = 3; // fencing token (acquire: caller's; failure: holder's)
}

message AgentLeaseRequest {
  bytes  key      = 1;
  string agent_id = 2;
  uint64 ttl_ms   = 3; // lease TTL in milliseconds; 0 = no expiry (plain claim)
}

message AgentLeaseResponse {
  bool   acquired = 1;
  string holder   = 2;
  uint64 fence    = 3;
}

message AgentRenewRequest {
  bytes  key      = 1;
  string agent_id = 2;
  uint64 fence    = 3; // the holder's current fence (must match to renew)
  uint64 ttl_ms   = 4; // new TTL window from now
}

message AgentRenewResponse {
  bool   acquired = 1; // true = lease extended; false = fence no longer matches
  string holder   = 2;
  uint64 fence    = 3; // refreshed fence on success
}

message AgentReleaseRequest {
  bytes  key   = 1;
  uint64 fence = 2; // the holder's fence (must match to release)
}

message AgentReleaseResponse {
  bool released = 1; // true = key dropped; false = fence no longer matches
}

// ─── Cross-shard transactions: prewrite (epic #1478, Phase 2) ───────────────
//
// The internal Percolator prewrite: place a LockRecord intent on the user key
// and stage the provisional value, both as one atomic conditional write in the
// coordination CF. Aborts on a conflicting lock or a write-pointer carrying a
// commit_ts >= start_ts. INTERNAL/admin-only and gated behind
// STATELET_CROSS_SHARD_TXN (default OFF).

message AgentPrewriteRequest {
  uint32 cf          = 1; // target CF of the user key
  bytes  key         = 2; // user key being prewritten
  bytes  primary_ref = 3; // the txn's primary key (every secondary points back)
  uint64 start_ts    = 4; // HLC start_ts (packed u64), from Phase 1
  uint64 ttl_ms      = 5; // lock TTL in milliseconds
  uint64 fence       = 6; // primary claim's fencing token
  bytes  value       = 7; // provisional value for a Put (ignored when delete)
  bool   delete      = 8; // true = Delete intent (the value field is ignored)
  // true = READ intent: lock the key so no concurrent transaction can commit
  // over it, stage nothing, and release (not roll forward) at commit. This is
  // how a cross-shard transaction detects READ-WRITE conflicts; `value` and
  // `delete` are ignored. Re-reading the key before commit would not do — a
  // concurrent txn could commit between the check and the primary CAS.
  bool   read_only   = 9;
}

message AgentPrewriteResponse {
  bool   locked = 1; // true = intent placed; false = conflict (lock or newer ver)
  uint64 fence  = 2; // on lock: the per-key seq the lock committed at
}

// ─── Cross-shard transactions: commit + resolve (epic #1478, Phase 3) ───────
//
// Internal/admin-only drivers (gated behind STATELET_CROSS_SHARD_TXN, default
// OFF) the gateway coordinator fans to the pinned coordination shard.

// The commit point: CAS the primary TxnStatus Prewritten->Committed at commit_ts,
// gated IfSeqEquals(fence). A coordinator that lost its lease (the per-key seq
// advanced past `fence`) is rejected (#784/#894).
message AgentCommitPrimaryRequest {
  uint32 primary_cf  = 1; // CF of the primary key
  bytes  primary_key = 2; // the txn's primary key (holds the single commit point)
  uint64 fence       = 3; // primary claim's fencing token (gates the CAS)
  uint64 commit_ts   = 4; // HLC commit_ts (packed u64)
  // The participants (cf, key, delete-op) the txn prewrote, recorded on the status
  // so a resolver can finish any secondary the coordinator didn't roll forward —
  // each participant's `delete` flag records its op so the resolver can decide
  // put-vs-tombstone without consulting the (possibly reaped) lock (#1626).
  repeated CrossShardTxnWrite participants = 5;
  // The txn's HLC start_ts (packed u64), recorded so a resolver can locate each
  // participant's staged value after the prewrite lock TTL-expired (#1626).
  uint64 start_ts = 6;
}

message AgentCommitPrimaryResponse {
  bool   committed  = 1; // true = TxnStatus flipped to Committed; false = rejected
  uint64 commit_seq = 2; // per-key seq the commit record committed at
}

// Roll a single committed secondary forward: replace its LockRecord with a
// WriteRecord{commit_ts}, advance the write pointer, materialize the staged
// value, clear the lock. Idempotent.
message AgentRollForwardRequest {
  uint32 cf        = 1; // target CF of the secondary user key
  bytes  key       = 2; // the secondary user key
  uint64 start_ts  = 3; // the txn's HLC start_ts (locates the staged value)
  uint64 commit_ts = 4; // the txn's HLC commit_ts
  // The op the prewrite recorded (true = Delete/tombstone, false = Put). Carried
  // so a resolver materializing a committed write after the lock TTL-expired does
  // not have to consult the reaped lock to decide put-vs-tombstone (issue #1626).
  bool   delete    = 5;
  // true = this participant was a READ intent: release the lock and write
  // NOTHING (no value, no write-pointer, no WriteRecord). Without this the
  // two-valued `delete` would make a read intent look like a Put and the
  // roll-forward would fail closed looking for a staged value that, by
  // design, never existed.
  bool   read_only = 6;
}

message AgentRollForwardResponse {
  bool rolled = 1; // true = roll-forward applied (always true on success)
}

// Roll a single secondary back: drop its LockRecord intent + staged value.
// Idempotent.
message AgentRollbackRequest {
  uint32 cf       = 1; // target CF of the secondary user key
  bytes  key      = 2; // the secondary user key
  uint64 start_ts = 3; // the txn's HLC start_ts (locates the staged value)
}

message AgentRollbackResponse {
  bool rolled_back = 1; // true = intent + staged value dropped
}

// ─── Cross-shard transactions: read-path lock resolution (epic #1478, Phase 4) ─
//
// The read-path resolver: consult any prewrite lock on (cf, key) for a snapshot
// read at read_ts and resolve it via the primary TxnStatus. Idempotent and
// callable by any reader. Internal/admin-only and gated behind
// STATELET_CROSS_SHARD_TXN (default OFF).
message AgentResolveLockRequest {
  uint32 cf      = 1; // target CF of the user key being read
  bytes  key     = 2; // the user key
  uint64 read_ts = 3; // HLC snapshot read timestamp (packed u64)
}

// The resolution outcome (mirrors reactive::ResolveOutcome).
enum ResolveLockOutcome {
  RESOLVE_LOCK_CLEAR         = 0; // no blocking lock (or cleaned): read normally
  RESOLVE_LOCK_ROLLED_FORWARD = 1; // committed secondary rolled forward
  RESOLVE_LOCK_PENDING       = 2; // live lock, decision pending: back off + retry
}

message AgentResolveLockResponse {
  ResolveLockOutcome outcome = 1;
  // On ROLLED_FORWARD, the HLC commit_ts the version became visible at.
  uint64 commit_ts = 2;
}

// ─── Cross-shard transactions: coordination-shard primary-status read (#1598) ─
//
// Read the primary TxnStatus for `primary_key` from the receiving node's pinned
// coordination shard. The owner-shard read-path/recovery resolver consults this
// (against the coordination-shard leader) so a committed primary is observed
// even when the owner shard's node is not in the coordination shard's replica
// set. Internal/admin-only and gated behind STATELET_CROSS_SHARD_TXN (default
// OFF).
message AgentReadTxnStatusRequest {
  bytes primary_key = 1; // the txn's primary key (raw suffix bytes)
}

// Mirrors reactive::TxnState. `present = false` means no primary status record
// exists yet (still mid-prewrite or the coordinator crashed before the commit
// point) — the resolver treats it the same as a node-local absent status.
enum TxnStatusState {
  TXN_STATUS_PREWRITTEN = 0; // primary still undecided
  TXN_STATUS_COMMITTED  = 1; // primary committed at commit_ts
  TXN_STATUS_ABORTED    = 2; // primary aborted
}

message AgentReadTxnStatusResponse {
  bool           present   = 1; // false = no TxnStatus record present
  TxnStatusState state     = 2; // the decision (only meaningful when present)
  uint64         commit_ts = 3; // HLC commit_ts (packed u64), 0 unless committed
  // Every participant (cf, user_key, delete-op) the txn prewrote, so the recovery
  // driver can fan a roll-forward / roll-back to each participant's OWNER shard
  // (#1598). Each participant's `delete` flag records its prewrite op so a
  // resolver can roll forward (put-vs-tombstone) without the reaped lock (#1626).
  repeated CrossShardTxnWrite participants = 4;
  // The txn's HLC start_ts (packed u64), recorded on the status so a resolver can
  // locate each participant's staged value even after the lock TTL-expired (#1626).
  uint64 start_ts = 5;
}

// One decided primary returned by AgentListDecidedPrimaries (#1598): the raw
// primary suffix bytes plus its decision + participants.
message DecidedPrimary {
  bytes          primary_ref = 1; // raw primary suffix bytes (Claim-role key body)
  TxnStatusState state       = 2; // COMMITTED or ABORTED (never PREWRITTEN)
  uint64         commit_ts   = 3; // HLC commit_ts (packed u64), 0 unless committed
  // (cf, user_key, delete-op) each participant prewrote; `delete` lets a resolver
  // roll forward put-vs-tombstone without the reaped lock (#1626).
  repeated CrossShardTxnWrite participants = 4;
  uint64 start_ts = 5; // txn HLC start_ts: locate staged values post lock-expiry (#1626)
}

message AgentListDecidedPrimariesRequest {
  uint64 max_primaries = 1; // cap the enumeration (0 = unbounded)
}

message AgentListDecidedPrimariesResponse {
  repeated DecidedPrimary primaries = 1;
}

// ─── Cross-shard transactions: gateway coordinator API (epic #1478, Phase 3) ─

// One write in a cross-shard transaction's write_set. `delete = true` makes this
// a tombstone (the `value` field is ignored).
message CrossShardTxnWrite {
  uint32 cf     = 1;
  bytes  key    = 2;
  bytes  value  = 3;
  bool   delete = 4;
  // Only meaningful in `AgentCommitPrimaryRequest.participants`: this
  // participant is a lock-only READ intent, to be RELEASED rather than rolled
  // forward. Recording it is what lets a post-crash resolver clean up read
  // locks at all — a participant list of writes only leaves them stranded on
  // their owner shards, blocking every other prewrite on those keys until the
  // lock TTL expires. Always false in a `write_set`.
  bool   read_only = 5;
}

message CrossShardCommitRequest {
  repeated CrossShardTxnWrite write_set = 1; // the atomically-committed writes
  repeated AgentTxnRead       read_set  = 2; // optional read-set (snapshot seqs)
  // The transaction's primary key (every secondary lock points back to it). When
  // empty the coordinator synthesizes a per-txn primary in the coordination CF.
  bytes  primary_key = 3;
  uint64 ttl_ms      = 4; // lock TTL for the prewritten intents
}

message CrossShardCommitResponse {
  bool   committed    = 1; // true = the txn committed atomically across all shards
  uint64 commit_ts    = 2; // on commit, the HLC commit_ts (packed u64)
  string abort_reason = 3; // on abort, why (conflicting lock / newer version / lost lease)
}

// ─── Cross-shard transactions: recovery & liveness (epic #1478, Phase 5) ────

// Admin request to resolve stale / orphaned cross-shard locks. When
// `primary_key` is supplied, only that transaction is resolved (roll forward if
// its primary committed, roll back if aborted). Otherwise the whole `lock/`
// prefix of the coordination CF is swept and each lock resolved against its
// primary TxnStatus. Gated behind STATELET_CROSS_SHARD_TXN (default OFF) and
// restricted to cluster administrators.
message ResolveStaleTxnRequest {
  bytes  primary_key = 1; // resolve only this txn's primary; empty = sweep all locks
  uint64 max_locks   = 2; // cap the sweep (0 = unlimited); ignored when primary_key set
}

message ResolveStaleTxnResponse {
  bool   enabled        = 1; // false = STATELET_CROSS_SHARD_TXN is OFF (nothing scanned)
  uint64 scanned        = 2; // locks scanned in the sweep
  uint64 rolled_forward = 3; // locks whose primary committed (rolled forward)
  uint64 rolled_back    = 4; // stale/aborted locks reclaimed (rolled back)
  uint64 still_pending  = 5; // live locks left untouched (primary undecided, TTL live)
}

// Internal per-coordination-shard driver (same fields as the admin RPC). The
// gateway routes this to the pinned coordination shard, which runs the resolver
// over its local coordination CF.
message AgentResolveStaleTxnRequest {
  bytes  primary_key = 1;
  uint64 max_locks   = 2;
}

message AgentResolveStaleTxnResponse {
  uint64 scanned        = 1;
  uint64 rolled_forward = 2;
  uint64 rolled_back    = 3;
  uint64 still_pending  = 4;
}

// Internal per-owner-shard expired-lock sweep (issue #1795). The gateway fans
// this to every owner shard of the participant CFs; each data node scans the
// TTL-expired prewrite locks in its shard's collapsed coordination CF and
// resolves each against the coordination-shard primary status (a still-undecided
// or aborted primary => roll back; a committed primary is left for the read-path
// roll-forward). Reclaims the still-Prewritten orphans the decided-primaries scan
// never enumerates.
message AgentGcExpiredLocksRequest {
  uint64 shard_id  = 1; // the owner shard to sweep (resolved on the data node)
  uint64 max_locks = 2; // cap the per-shard sweep (0 = unbounded)
}

message AgentGcExpiredLocksResponse {
  uint64 reclaimed = 1; // expired, never-committed locks rolled back on this shard
}

// ─── Cross-shard transactions: BEGIN / COMMIT / ROLLBACK (epic #1478, Phase 6) ─
//
// TiKV-style optimistic 2PC: the client buffers its write_set locally between
// BEGIN and COMMIT and submits it in one shot at COMMIT, which runs the same
// prewrite->commit->roll-forward as CrossShardTxnCommit. Gated behind
// STATELET_CROSS_SHARD_TXN, DEFAULT OFF.

message TxnBeginRequest {
  // Optional caller-supplied primary key (a coordination-CF claim every prewrite
  // fences on). When empty the server synthesizes a per-txn primary.
  bytes primary_key = 1;
}

message TxnBeginResponse {
  // The transaction handle: the primary key the matching TxnCommit must pass so
  // its prewrites fence on the same claim. Opaque to the client.
  bytes txn_id = 1;
}

message TxnCommitRequest {
  bytes txn_id = 1;                            // the handle from TxnBegin
  repeated CrossShardTxnWrite write_set = 2;   // the client-buffered writes
  repeated AgentTxnRead       read_set  = 3;   // optional read-set (snapshot seqs)
  uint64 ttl_ms = 4;                           // lock TTL for the prewritten intents
}

message TxnCommitResponse {
  bool   committed    = 1; // true = the txn committed atomically across all shards
  uint64 commit_ts    = 2; // on commit, the HLC commit_ts (packed u64)
  string abort_reason = 3; // on abort, why
}

message TxnRollbackRequest {
  bytes txn_id = 1; // the handle from TxnBegin
}

message TxnRollbackResponse {
  bool rolled_back = 1; // true = the handle was discarded (optimistic 2PC: a no-op)
}

message AgentWatchPrefixRequest {
  string agent_id = 1;
  uint32 cf       = 2;
  bytes  prefix   = 3;
}

message AgentWatchEventProto {
  string event_type = 1; // "put", "delete"
  uint32 cf         = 2;
  bytes  key        = 3;
  bytes  value      = 4;
  uint64 seq        = 5;
}

// DEPRECATED (CDC Phase 5b, issue #823): request for the deprecated
// AgentSubscribeWrites RPC. Use SubscribeCommittedRequest instead — the durable
// committed change-feed is a drop-in superset (offset-addressable + resumable).
message AgentSubscribeWritesRequest {
  option deprecated = true;
  uint64 shard_id = 1;
  uint32 cf       = 2;
  bytes  prefix   = 3;
}

message AgentWriteEventProto {
  uint32 cf   = 1;
  bytes  key  = 2;
  string op   = 3; // "put" or "delete"
  uint64 seq  = 4;
}

// ─── Durable committed change-feed (CDC) — issue #692 ───────────────────────

message SubscribeCommittedRequest {
  uint64 shard_id      = 1;
  uint64 from_offset   = 2;  // first Raft index to deliver; 0 = live-only (from current commit)
  uint32 cf            = 3;  // 0 = all column families
  bytes  key_prefix    = 4;  // empty = no prefix filter
  bool   include_values = 5; // include the value bytes for Put/Merge changes
}

message CommittedChangeProto {
  uint64 offset       = 1;  // = LogEntry.index (stable Raft offset)
  uint64 term         = 2;  // LogEntry.term
  uint32 seq_in_entry = 3;  // position of this key within the batch (tiebreaker)
  uint32 cf           = 4;
  bytes  key          = 5;
  string op           = 6;  // "put" | "delete" | "merge"
  bytes  value        = 7;  // present for put/merge when include_values is set
}

// Requested offset is no longer in the durable log (compacted away); the client
// should full-rescan its prefix and resume from earliest_offset.
message CompactedNotice {
  uint64 earliest_offset = 1;   // new compaction floor (snapshot_index)
  uint64 snapshot_offset = 2;   // state-machine watermark (last_applied) at notice time:
                                // Scan() reflects state at >= this offset, so a Scan-then-resume
                                // client rebuilds baseline <= snapshot_offset then resumes from
                                // snapshot_offset+1. Old clients default this to 0 → resume at
                                // earliest_offset (the pre-Phase-5a behavior).
}

// One item in the SubscribeCommitted stream: either a change, a heartbeat that
// advances a filtered consumer's high-watermark, or a compaction notice.
message CommittedFeedItem {
  oneof item {
    CommittedChangeProto change    = 1;
    uint64               heartbeat = 2;  // high-watermark offset (no matching change)
    CompactedNotice      compacted = 3;
  }
}

// ─── Agent: Temporal graph messages ─────────────────────────────────────────

message AgentExpireEdgeRequest {
  uint64 src_step_id = 1;
  uint64 dst_step_id = 2;
  string edge_type   = 3;
  uint64 expire_at   = 4; // timestamp at which the edge becomes invalid
}

message AgentExpireEdgeResponse {}

message AgentCascadeExpireRequest {
  uint64 root_fact          = 1; // fact to retract; its dependents cascade-close
  uint64 expire_at          = 2; // valid_to timestamp (ms); 0 = now
  string triggered_by_agent = 3; // provenance: which agent triggered it
  string triggered_by_run   = 4; // provenance: which run
  bool   follow_informs     = 5; // also follow soft Informs deps (default false)
  uint32 max_depth          = 6; // 0 = default (64)
  uint64 max_nodes          = 7; // 0 = default (100000)
  // Phase 5 cross-shard fan-out: when true, only close `root_fact`'s dependents
  // (the root is already retired on its home shard) — do NOT re-retract the
  // root. The gateway sets this when forwarding the cascade frontier to peer
  // shards. Default false = retract the root + cascade (single-shard path).
  bool   dependents_only    = 10;
}

message AgentCascadeExpireResponse {
  uint64          root              = 1;
  repeated uint64 closed_facts      = 2; // dependent facts whose valid_to closed
  uint32          closed_edges      = 3; // number of support edges severed
  uint32          max_depth_reached = 4;
  bool            truncated         = 5; // a depth/node guard stopped the walk
}

message AgentSupersedeFactRequest {
  uint64 new_fact           = 1; // replacement fact
  uint64 old_fact           = 2; // fact being superseded
  uint64 expire_at          = 3; // valid_to timestamp (ms); 0 = now
  string triggered_by_agent = 4;
  string triggered_by_run   = 5;
  bool   cascade            = 6; // also cascade-close old_fact's derived deps
  bool   follow_informs     = 7; // (cascade only) follow soft Informs deps
  uint32 max_depth          = 8; // (cascade only) 0 = default (64)
  uint64 max_nodes          = 9; // (cascade only) 0 = default (100000)
}

message AgentSupersedeFactResponse {
  // Populated only when cascade = true (same shape as cascade-expire).
  repeated uint64 closed_facts      = 1;
  uint32          closed_edges      = 2;
  uint32          max_depth_reached = 3;
  bool            truncated         = 4;
}

// ─── Agent: transactional memory ingest (#780) ──────────────────────────────

// One ANN candidate the caller already retrieved + scope-filtered.
message AgentIngestCandidate {
  uint64 fact_id = 1; // existing fact id
  float  sim     = 2; // cosine similarity to the incoming content
}

message AgentMemoryIngestRequest {
  string scope                             = 1;
  string content                           = 2;
  optional uint64 embedding_id             = 3;
  repeated AgentIngestCandidate candidates = 4; // (existing_fact_id, cosine_sim)
  repeated uint64 provenance_steps         = 5; // DerivedFrom episode steps
  float  dedup_threshold                   = 6;
  float  supersede_threshold               = 7;
  uint64 fence                             = 8; // Phase 3 (#784) fencing token; 0 = ungated
  // Optional typed attribution recorded on a newly-created fact body.
  string author_agent_id                   = 9;
  float  confidence                        = 10;
  string run_id                            = 11;
  // Phase 3 (#784): the claim-key bytes the lease was taken on (exactly what
  // was passed to AgentClaim/AgentLease). Required when fence != 0 — the handler
  // adds (coord_cf, lease_key, observed=fence) to the txn read-set so the commit
  // aborts if the lease moved. Ignored when fence == 0.
  bytes  lease_key                         = 12;
}

message AgentMemoryIngestResponse {
  uint32          action          = 1; // 0=Added 1=Deduplicated 2=Superseded 3=Conflict
  uint64          fact_id         = 2; // current fact id (new, or deduped existing)
  repeated uint64 superseded      = 3; // facts whose valid_to this ingest closed
  bool            committed       = 4; // false iff action==Conflict (txn aborted)
  uint64          conflict_fact_id = 5; // on data Conflict, the candidate whose version moved; 0 on fence loss
  bool            fence_lost      = 6; // Phase 3 (#784): true iff Conflict was a lost lease (fence moved), not a data conflict
}

message AgentEdgeHistoryRequest {
  uint64 src_step_id = 1;
  uint64 dst_step_id = 2;
  string edge_type   = 3;
}

message AgentTemporalEdgeProto {
  uint64 src         = 1;
  uint64 dst         = 2;
  string edge_type   = 3;
  uint64 valid_from  = 4;
  uint64 valid_to    = 5;
  bytes  props       = 6;
  // Bitemporal transaction-time (recorded-at), ms. tx_from = when this edge
  // revision became believed (0 = always known / legacy). tx_to = when it was
  // superseded/corrected (0 = still believed).
  uint64 tx_from     = 7;
  uint64 tx_to       = 8;
  // Provenance: the authoring agent id (empty = unknown author).
  string author_agent_id = 9;
}

message AgentEdgeHistoryResponse {
  repeated AgentTemporalEdgeProto edges = 1;
}

// ─── Durable agent execution (#846, epic #699 / sub-epic #792) ───────────────
//
// Raft-backed run/step home. RunStep/CompleteStep are split because the server
// must not execute client code over the wire: RunStep records `Started` (B1) +
// replay-check, the client runs the effect, CompleteStep records `Completed` +
// idempotency index + advances next_step_seq (B2). This preserves the
// Temporal/DBOS record-before-effect invariant across the network.

message AgentStartRunRequest {
  string agent_id         = 1;
  uint64 parent_branch_id = 2;
  bytes  input            = 3;
  // Optional routing/idempotency key; when empty the shard is picked by hashing
  // agent_id through the ShardRouter.
  string run_key          = 4;
}

message AgentStartRunResponse {
  // run_id = (shard_id << 40) | local_seq — self-routing, cluster-unique.
  uint64 run_id = 1;
}

message AgentRunStepRequest {
  uint64 run_id          = 1;
  uint64 step_seq        = 2;
  bytes  input           = 3;
  bytes  idempotency_key = 4; // optional; default = hash(run_id, step_seq, input)
}

message AgentRunStepResponse {
  // True when a Completed event already exists (idempotency hit / resume): the
  // client skips the effect and uses recorded_result.
  bool   already_completed = 1;
  bytes  recorded_result   = 2;
  uint32 attempt           = 3;
  uint64 causal_step_id    = 4;
}

message AgentCompleteStepRequest {
  uint64 run_id   = 1;
  uint64 step_seq = 2;
  bytes  result   = 3;
}

message AgentCompleteStepResponse {
  uint64 next_step_seq = 1;
}

message AgentSemanticCheckpointProto {
  uint64 run_id                 = 1;
  uint64 step_seq               = 2;
  uint64 causal_step_id         = 3;
  uint64 tx_at                  = 4;
  uint64 belief_raft_index      = 5;
  bool   has_belief_raft_index  = 6;
  repeated string artifact_refs = 7;
  bytes  summary                = 8;
  bytes  result_digest          = 9;
  string warning                = 10;
}

message AgentCheckpointGetRequest {
  uint64 run_id   = 1;
  uint64 step_seq = 2;
}

message AgentCheckpointGetResponse {
  bool found = 1;
  AgentSemanticCheckpointProto checkpoint = 2;
}

message AgentCheckpointLatestRequest {
  uint64 run_id = 1;
}

message AgentCheckpointLatestResponse {
  bool found = 1;
  AgentSemanticCheckpointProto checkpoint = 2;
}

message AgentProvenanceChainNodeProto {
  string kind      = 1; // decision, memory, graph_write, redacted, missing
  uint64 step_id   = 2;
  string agent_id  = 3;
  string step_type = 4;
  uint64 timestamp = 5;
  uint64 branch_id = 6;
  bytes  metadata  = 7;
  bool   redacted  = 8;
}

message AgentProvenanceChainQueryRequest {
  uint64 run_id     = 1;
  uint64 step_seq   = 2;
  // When true, step_seq is ignored and the latest semantic checkpoint for the
  // run is used.
  bool latest       = 3;
  // Server-capped bounds for fan-out from the decision node.
  uint32 max_memory_nodes = 4;
  uint32 max_graph_nodes  = 5;
}

message AgentProvenanceChainQueryResponse {
  bool found = 1;
  AgentSemanticCheckpointProto checkpoint = 2;
  AgentProvenanceChainNodeProto decision = 3;
  repeated AgentProvenanceChainNodeProto memories = 4;
  repeated AgentArtifactMetadataProto artifacts = 5;
  repeated AgentProvenanceChainNodeProto graph_writes = 6;
  repeated string missing = 7;
  bool partial = 8;
}

message AgentResumeFromStepRequest {
  uint64 run_id          = 1;
  uint64 consumer_offset = 2; // consumer's checkpointed change-feed offset
}

message AgentResumeFromStepResponse {
  uint64 resume_seq                     = 1;
  repeated AgentStepEventProto replayed = 2; // recorded results below resume_seq
  // Phase 4 (#795): the consumer's checkpointed offset exceeded the engine's
  // durable high-water and was clamped (Kafka OffsetOutOfRange -> auto.offset.reset).
  // Additive + optional: old clients ignore it and still resume safely.
  bool   consumer_offset_ahead          = 3;
  // The offset actually used after max(consumer_offset, high_water) + clamp.
  // Equals resume_seq; surfaced separately for observability/symmetry.
  uint64 effective_offset               = 4;
}

message AgentSemanticCandidateProto {
  string candidate_id = 1;
  bytes  value        = 2;
  float  confidence   = 3;
  float  probability  = 4;
  bytes  metadata     = 5;
}

message AgentCandidateForkProto {
  AgentSemanticCandidateProto candidate = 1;
  uint64 run_id                         = 2;
  uint64 branch_id                      = 3;
  uint64 fork_step_seq                  = 4;
  uint64 inherited_steps                = 5;
}

message AgentResumeSemanticRequest {
  uint64 run_id                       = 1;
  repeated AgentSemanticCandidateProto candidates = 2;
  float  confidence_threshold         = 3; // 0/NaN => server default
  uint32 top_k                        = 4; // 0 => server default, capped
  string label_prefix                 = 5;
}

message AgentResumeSemanticResponse {
  AgentSemanticCheckpointProto checkpoint = 1;
  uint64 resume_seq                       = 2;
  repeated AgentStepEventProto replayed   = 3;
  AgentSemanticCandidateProto selected_candidate = 4;
  repeated AgentCandidateForkProto forks  = 5;
  bool   consumer_offset_ahead            = 6;
  uint64 effective_offset                 = 7;
}

message AgentGetRunStatusRequest {
  uint64 run_id = 1;
}

message AgentGetRunStatusResponse {
  bool   found         = 1;
  uint64 run_id        = 2;
  string agent_id      = 3;
  string status        = 4; // "Running", "Completed", "Failed", "Suspended"
  uint64 next_step_seq = 5;
  uint64 created_at     = 6;
  uint64 updated_at     = 7;
}

message AgentStepEventProto {
  uint64 step_seq       = 1;
  uint64 causal_step_id = 2;
  uint32 attempt        = 3;
  uint64 ts             = 4;
  bytes  result         = 5;
}

// Phase 5 (#797): branch/time-travel resume request. Fork `source_run_id` at the
// historical `fork_step_seq` into a NEW AgentFork branch + child run. The source
// run is left untouched; the child inherits the parent's recorded Completed
// prefix [0..fork_step_seq) verbatim (no re-execution) and continues forward
// execution from fork_step_seq on the new timeline.
message AgentForkRunRequest {
  uint64 source_run_id = 1;
  uint64 fork_step_seq = 2; // must be <= source.next_step_seq
  string label         = 3; // optional branch label; defaults to a descriptive one
}

message AgentForkRunResponse {
  // Self-routing child run_id (lives on the same shard as the source run).
  uint64 run_id          = 1;
  // The newly allocated AgentFork BranchId carried on the child's RunRecord and
  // every inherited/forward CausalStep.
  uint64 branch_id       = 2;
  // The child's next_step_seq == fork point; forward execution continues here.
  uint64 fork_step_seq   = 3;
  // Count of parent Completed events copied into the child (== fork_step_seq).
  uint64 inherited_steps = 4;
}

message AgentForkAcrossCandidatesRequest {
  uint64 source_run_id                 = 1;
  uint64 fork_step_seq                 = 2; // 0 => latest semantic checkpoint + 1
  repeated AgentSemanticCandidateProto candidates = 3;
  uint32 top_k                         = 4; // 0 => server default, capped
  string label_prefix                  = 5;
}

message AgentForkAcrossCandidatesResponse {
  uint64 fork_step_seq                 = 1;
  repeated AgentCandidateForkProto forks = 2;
}

message AgentArtifactPutRequest {
  uint64 run_id    = 1;
  uint64 step_seq  = 2;
  bytes  content   = 3; // inline payload; leave empty when uri is set
  string uri       = 4; // external payload reference; requires sha256
  bytes  sha256    = 5; // optional for inline, required for uri; 32 bytes
  uint64 size      = 6; // external payload size; inline size is derived
  string mime_type = 7;
}

message AgentArtifactMetadataProto {
  string artifact_id = 1; // lowercase hex sha256
  uint64 run_id      = 2;
  uint64 step_seq    = 3;
  bytes  sha256      = 4;
  uint64 size        = 5;
  string mime_type   = 6;
  string uri         = 7;
  bool   inline      = 8;
  uint64 created_at  = 9;
}

message AgentArtifactPutResponse {
  AgentArtifactMetadataProto artifact = 1;
}

message AgentArtifactGetRequest {
  uint64 run_id      = 1;
  string artifact_id = 2; // lowercase hex sha256
}

message AgentArtifactGetResponse {
  bool found = 1;
  AgentArtifactMetadataProto artifact = 2;
  bytes content = 3; // populated for inline artifacts
}

message AgentArtifactResolveRequest {
  uint64 run_id      = 1;
  string artifact_id = 2;
}

message AgentArtifactResolveResponse {
  bool found = 1;
  AgentArtifactMetadataProto artifact = 2;
}

// ─── Team time-travel (#787, epic #698 Phase 3) ──────────────────────────────
//
// "As-of-then" replay of what a team knew at (V, T) over the wire. The leaf
// handlers call the already-shipped engine operators (PR #724); the gateway
// fans out, pins a per-shard committed ordinal (FoundationDB-style read
// version), merges/dedupes, paginates on the global sort key, and reports
// partial coverage when a shard is unreachable (Elasticsearch `_shards` style).

// How a single logical edge's belief changed in a team diff window. Mirrors
// `KnowledgeChangeType` in src/agent/types.rs 1:1 (Dolt `dolt_diff_*` shape).
enum ChangeType {
  ASSERTED  = 0; // newly believed in the window
  RETRACTED = 1; // believed at T1, no longer at T2
  REVALUED  = 2; // believed at both, different revision
}

// Full bitemporal edge tuple carried by team time-travel. A superset of
// AgentTemporalEdgeProto (which lacks no field but is reused name-wise for
// belief queries); kept distinct so the team surface can evolve independently.
message TeamTemporalEdgeProto {
  uint64 src        = 1;
  uint64 dst        = 2;
  string edge_type  = 3;
  uint64 valid_from = 4;
  uint64 valid_to   = 5;
  bytes  props      = 6;
  uint64 tx_from    = 7;
  uint64 tx_to      = 8;
  string agent_id   = 9; // authoring agent id (empty = unknown)
}

// One classified change in the team's knowledge between two transaction
// instants, with the before/after edge revisions where applicable.
message KnowledgeChangeProto {
  ChangeType change_type       = 1;
  TeamTemporalEdgeProto before = 2; // present for RETRACTED, REVALUED
  TeamTemporalEdgeProto after  = 3; // present for ASSERTED, REVALUED
}

// ─── Raw agent-state row access (P3) ─────────────────────────────────────────

// Which agent column family to read. A NAME, not an id: the agent CFs are
// created directly on the shared DB, so their physical ids are a node-local
// detail that a coordinator must not have to know (and must not be able to
// guess wrong — naming the CF is what makes the allowlist check possible).
message AgentStateGetRequest {
  string cf_name = 1;
  bytes  key     = 2;
  // Logical row address. When `role` is non-zero the server builds the physical
  // key itself from (`entity_id`, `role`, `key`-as-suffix) and `cf_name` is
  // ignored.
  //
  // Prefer this over a raw `key`. Whether a row lives in the collapsed causal CF
  // under an `[entity_id][role][suffix]` key or in a legacy per-role CF under
  // the bare suffix is SERVER state (`collapsed_active()`), and it differs
  // between stores. A coordinator that hand-built physical keys would have to
  // track the storage layout to stay correct — which is exactly the coupling
  // that keeps agent semantics stuck inside the storage process. Addressing a
  // row logically leaves layout where it belongs.
  uint32 role      = 3; // CausalRole tag byte; 0 = address by raw key instead
  uint64 entity_id = 4; // ignored unless `role` is set
  bool   system    = 5; // address the SYSTEM_ENTITY namespace, not `entity_id`
  // Snapshot-bounded read: resolve the row as of this engine sequence, ignoring
  // every version committed after it. 0 = read the latest committed version.
  //
  // Required for an optimistic transaction driven from outside the storage
  // process. Such a transaction baselines its read set on ONE snapshot; reading
  // "latest" instead would let a write committed after the snapshot leak into
  // the decision while the read set still claims the older baseline — the value
  // and the version it is validated against would describe different instants.
  uint64 snapshot_seq = 6;
}

message AgentStateGetResponse {
  bool   found = 1;
  bytes  value = 2;
  // Engine sequence of the row's latest version; 0 when absent. Same clock and
  // same purpose as `GetResponse.seq` — this is how a gateway-side agent
  // transaction builds `AgentTxnRead.observed_seq` for a row it read.
  uint64 seq   = 3;
}

message AgentStateScanRequest {
  string cf_name = 1;
  bytes  prefix  = 2;
  bytes  cursor  = 3; // resume cursor (exclusive); empty = from the beginning
  uint32 limit   = 4; // 0 = server default
}

message AgentStateScanEntry {
  bytes  key   = 1;
  bytes  value = 2;
  uint64 seq   = 3;
}

// A JSON-serialized `ProvenanceRecord`, built by the coordinator that made the
// decision. Sent whole rather than field-by-field: the record's shape
// (caller identity, target scope, grant-set hash, decision) is the audit
// contract, and re-marshalling it through a parallel proto message would give
// it a second definition to drift against.
message AgentAppendProvenanceRequest {
  // JSON-serialized `ProvenanceRecord`s. Batched because a scope-pruning
  // traversal decides one per visited step: sending them individually would put
  // a round trip on every node of a BFS.
  repeated bytes records = 1;
}

message AgentAppendProvenanceResponse {
  // How many records were appended. Failure is reported in-band, not as an RPC
  // error: the read that produced these decisions has already been answered, so
  // an audit-write failure must not retroactively fail it (matching
  // `record_decision`, which counts + logs and continues).
  uint64 appended = 1;
  bool   applied  = 2; // false if ANY record failed to append
}

message AgentStateEdgesRequest {
  // Batched for the same reason as the provenance append: a traversal expands a
  // whole BFS level at once, and one round trip per anchor would make depth ×
  // fan-out round trips out of what is one index lookup per anchor locally.
  repeated uint64 anchor_step_ids = 1;
  string direction      = 2; // "forward" | "backward" | "both"
  string edge_type      = 3; // empty = every type
  // `at_timestamp > 0` selects edges valid at that instant; otherwise a
  // non-zero `window_end` selects edges overlapping [window_start, window_end];
  // with neither, every revision is returned.
  uint64 at_timestamp   = 4;
  uint64 window_start   = 5;
  uint64 window_end     = 6;
  // Bitemporal belief form (`AgentBeliefQuery`): when `bitemporal` is set the
  // three fields above are ignored and the server instead resolves the LATEST
  // believed revision of each edge at (`as_of` valid-time, `tx_as_of`
  // transaction-time), optionally narrowed to one author.
  //
  // This cannot be done by the caller from the raw edge list: "latest revision
  // per edge" is a reduction over the revision chain, not a per-edge predicate,
  // and getting it wrong silently returns a superseded belief as current.
  bool   bitemporal     = 7;
  uint64 as_of          = 8;  // 0 = now
  uint64 tx_as_of       = 9;  // 0 = latest known
  string author_agent_id = 10; // empty = any author
}

message AgentStateEdgeProto {
  // Which requested anchor this edge belongs to. Explicit rather than inferred
  // from src/dst: under "both" an edge can attach to an anchor from either end,
  // and a self-edge attaches from both.
  uint64 anchor     = 10;
  uint64 src        = 1;
  uint64 dst        = 2;
  string edge_type  = 3;
  uint64 valid_from = 4;
  uint64 valid_to   = 5; // 0 = permanently valid
  uint64 tx_from    = 6;
  uint64 tx_to      = 7; // 0 = still believed
  string author_agent_id = 8;
  bytes  props      = 9;
}

message AgentStateEdgesResponse {
  repeated AgentStateEdgeProto edges = 1;
}

message AgentStateVersionKey {
  uint32 cf  = 1;
  bytes  key = 2;
}

message AgentStateAllocIdsRequest {
  uint32 count = 1; // ids to reserve; 0 is rejected
}

message AgentStateAllocIdsResponse {
  // The block is `[start, start + count)`. Ids are never reissued, so an
  // unused tail is a harmless gap.
  uint64 start = 1;
  uint32 count = 2;
}

message AgentStateVersionsRequest {
  repeated AgentStateVersionKey keys = 1;
}

message AgentStateVersionsResponse {
  // Positional: `seqs[i]` is the version of `keys[i]`; 0 for a key that has
  // never been written.
  repeated uint64 seqs = 1;
  // The coordination shard's current engine sequence, captured with the probe.
  // A read-only optimistic commit reports this as its `commit_seq`.
  uint64 read_version  = 2;
}

// One LOGICAL causal row, named by (namespace, entity, role, suffix).
//
// The server expands it into every physical row the write must produce. While
// the collapsed-CF migration window is open that is TWO rows — the legacy
// per-role CF and the entity-major mirror — and emitting only one of them
// produces a write that reads back as absent, or a layout a rollback can no
// longer trust. Naming the row logically keeps that expansion where the reads
// are.
message AgentStateRoleEntry {
  uint32 role      = 1; // CausalRole tag byte
  uint64 entity_id = 2;
  bool   system    = 3; // address the SYSTEM_ENTITY namespace
  bytes  suffix    = 4; // the legacy per-CF key
  bytes  value     = 5;
  bool   delete    = 6;
  // Absolute expiry (ms since epoch); 0 = no TTL. A lease acquire needs the
  // expiry on the SAME write as the value.
  uint64 expire_at = 7;
}

message AgentStateConditionalWriteRequest {
  // Same shape as ConditionalBatchWriteRequest minus the shard fields — the
  // shard is not the caller's to choose here, it is the pinned coordination
  // shard by construction.
  WriteEntry         gate            = 1;
  WriteConditionKind condition       = 2;
  bytes              condition_value = 3;
  repeated WriteEntry entries        = 4;
  repeated AgentTxnRead read_set     = 5;
  uint64 condition_seq               = 6;
  // Logical causal rows to apply alongside `entries`, expanded server-side.
  // Prefer these over raw `entries` for anything in a causal CF.
  repeated AgentStateRoleEntry role_entries = 7;
  // The GATE, named logically. Use this instead of `gate` for anything in a
  // causal CF: the coordination CF is created node-locally and is NOT in the
  // metadata registry, so a coordinator has no way to learn its physical id —
  // and any id it guessed would be a different CF on a different node.
  //
  // The server puts the predicate on the row reads resolve to, and any
  // additional mirror rows ride along as plain entries so the whole batch is
  // still decided by the one predicate.
  AgentStateRoleEntry role_gate = 8;
  // Read-set entries named logically, resolved server-side and merged into
  // `read_set`. Needed for the same reason `role_gate` is: a causal row's
  // physical address depends on the store's layout, and the coordination CF has
  // no metadata id at all — a coordinator that built these itself would either
  // guess wrong or be unable to name the row.
  repeated AgentStateRoleRead role_read_set = 9;
}

// One read-set entry, named by (namespace, entity, role, suffix).
message AgentStateRoleRead {
  uint32 role      = 1; // CausalRole tag byte
  uint64 entity_id = 2;
  bool   system    = 3;
  bytes  suffix    = 4;
  // The version the caller observed. The batch applies only if this row has not
  // advanced past it.
  uint64 observed_seq = 5;
}

message AgentStateTeamReadRequest {
  string team_id            = 1;
  repeated string agent_ids = 2;
  uint64 valid_at           = 3;
  // Snapshot form: the belief instant. Diff form: the window.
  bool   diff               = 4;
  uint64 tx_at              = 5; // snapshot only
  uint64 tx_from            = 6; // diff only
  uint64 tx_to              = 7; // diff only
}

message AgentStateTeamReadResponse {
  // Snapshot form.
  repeated bytes step_jsons                     = 1;
  repeated TeamTemporalEdgeProto edges          = 2;
  // Diff form.
  repeated KnowledgeChangeProto changes         = 3;
  // This shard's committed ordinal at the instant the answer was computed.
  uint64 read_version                           = 4;
}

message AgentRunCheckpointGetRequest {
  uint64 run_id   = 1;
  // When `latest` is false, the checkpoint at exactly this step_seq.
  uint64 step_seq = 2;
  bool   latest   = 3;
}

message AgentRunCheckpointGetResponse {
  // The run record, JSON-serialized. Absent when the run does not exist. The
  // caller authorizes against this (owner agent + namespace/database scope)
  // before looking at the checkpoint.
  bool  run_found        = 1;
  bytes run_json         = 2;
  bool  checkpoint_found = 3;
  bytes checkpoint_json  = 4;
}

message AgentStateBeliefDivergenceRequest {
  uint64 src_step_id = 1;
  uint64 dst_step_id = 2; // 0 = any destination
  string edge_type   = 3; // empty = every type
  uint64 as_of       = 4; // 0 = now
  uint64 tx_as_of    = 5; // 0 = latest known
}

message AgentStateAuthorBelief {
  string author_agent_id = 1;
  // An author who is KNOWN to hold no belief here is distinct from one whose
  // belief was filtered out: the divergence verdict depends on that difference.
  //
  // NOT named `has_edge`: protoc's C++ generator names a scalar getter after
  // the bare field and a message field's presence check `has_<field>`, so
  // `has_edge` alongside `edge` emits the same method twice and the generated
  // header does not compile. Field number 2 is unchanged, so the rename is
  // binary-wire compatible.
  bool   edge_present    = 2;
  AgentStateEdgeProto edge = 3;
}

message AgentStateBeliefDivergenceResponse {
  repeated AgentStateAuthorBelief beliefs = 1;
  // Whether the authors actually disagree, computed BEFORE scope filtering.
  // The caller must recompute it after filtering — two beliefs that disagree
  // are not a disagreement the caller can see if one of them is invisible.
  bool divergent = 2;
}

message AgentStateBatchGetRequest {
  repeated uint64 entity_ids = 1;
  uint32 role                = 2; // CausalRole tag byte
}

message AgentStateBatchGetEntry {
  uint64 entity_id = 1;
  bool   found     = 2;
  bytes  value     = 3;
  uint64 seq       = 4;
}

message AgentStateBatchGetResponse {
  repeated AgentStateBatchGetEntry entries = 1;
}

message AgentStateScanResponse {
  repeated AgentStateScanEntry entries = 1;
  bytes  next_cursor = 2; // empty = no more rows
  bool   has_more    = 3;
  // The coordination shard this page was served from, and the engine sequence
  // it was read at. A multi-page walk that sees `read_seq` move has raced a
  // concurrent write and, if it needs a consistent view, must restart.
  uint64 shard_id    = 4;
  uint64 read_seq    = 5;
}

message TeamSnapshotLocalRequest {
  string team_id            = 1; // empty => all agents on this shard
  repeated string agent_ids = 2; // optional explicit membership (AND with team_id)
  uint64 valid_at           = 3; // V (ms); 0 => now
  uint64 tx_at              = 4; // T; 0 => latest belief
  uint64 tx_index           = 5; // explicit belief ordinal (overrides tx_at); set by gateway fence
  uint32 max_edges          = 6; // page size (0 => leaf default)
  bytes  page_token         = 7; // opaque resume cursor
}

message TeamSnapshotLocalResponse {
  repeated bytes step_jsons            = 1; // JSON-serialized CausalStep (AgentLocalTraverseResponse convention)
  repeated TeamTemporalEdgeProto edges = 2;
  uint64 resolved_tx_index             = 3; // per-shard ordinal this answer was pinned at (fence stamp)
  bytes  next_page_token               = 4; // empty => last page
  bool   partial                       = 5; // a required shard was unreachable => snapshot is incomplete
  repeated string missing_agents       = 6; // agents whose shard could not be contacted (parity with HTTP 206)
}

// Which data path served (or should serve) a team diff. Additive enum: AUTO is
// the wire default (tag 0) so existing callers are unchanged. See issue #838.
enum DiffMode {
  DIFF_AUTO         = 0; // changefeed if the window is fully retained, else version-scan head + changefeed tail
  DIFF_CHANGEFEED   = 1; // force the windowed durable-changefeed scan
  DIFF_REPLAY       = 2; // alias of changefeed (replay_committed-backed); reserved for future divergence
  DIFF_VERSION_SCAN = 3; // force the #724 full-version-scan oracle path
}

message TeamDiffLocalRequest {
  string team_id            = 1;
  repeated string agent_ids = 2;
  uint64 tx_from            = 3; // T1
  uint64 tx_to              = 4; // T2; 0 => up to latest belief
  uint64 valid_at           = 5; // optional domain filter
  uint32 max_changes        = 6;
  bytes  page_token         = 7;
  DiffMode mode             = 10; // default AUTO (issue #838)
  uint64 tx_from_index      = 11; // explicit belief ordinal i1 (skip _belief_index resolve)
  uint64 tx_to_index        = 12; // explicit belief ordinal i2
}

message TeamDiffLocalResponse {
  repeated KnowledgeChangeProto changes = 1;
  uint64 resolved_from_index            = 2;
  uint64 resolved_to_index              = 3; // = fence ordinal when tx_to==0
  bytes  next_page_token                = 4;
  DiffMode served_by                    = 10; // which path answered (observability, issue #838)
  bool   partial                        = 11; // a required shard was unreachable => diff is incomplete
  uint64 earliest_offset                = 12; // earliest retained Raft offset when partial
  repeated string missing_agents        = 13; // agents whose shard could not be contacted (parity with TeamSnapshotLocalResponse)
}

// ─── Agent: Bitemporal belief queries ("who believed what, when") ────────────

message AgentBeliefQueryRequest {
  uint64 start_step_id   = 1;
  string direction       = 2; // "forward" | "backward" | "both"
  uint32 max_depth       = 3; // 0 => single-hop edge list, >0 => traverse
  uint64 as_of           = 4; // valid-time V (0 => now)
  uint64 tx_as_of        = 5; // transaction-time T (0 => now == "current belief")
  string author_agent_id = 6; // empty => team / authoritative view
  string edge_type       = 7; // optional
}

message AgentBeliefQueryResponse {
  repeated AgentTemporalEdgeProto edges = 1;
  repeated bytes step_jsons = 2; // when max_depth > 0
}

message AgentBeliefDivergenceRequest {
  uint64 src_step_id = 1;
  uint64 dst_step_id = 2; // 0 => all neighbors
  string edge_type   = 3; // optional
  uint64 as_of       = 4; // valid-time V (0 => now)
  uint64 tx_as_of    = 5; // transaction-time T (0 => now)
}

// One author's resolved belief at (V, T). `edge` is absent (edge_present =
// false) when that author has no believed edge there ("believes ¬X").
//
// `edge_present` is deliberately not called `has_edge` — see
// AgentStateAuthorBelief for why that name breaks the C++ codegen.
message AgentAuthorBeliefProto {
  string author_agent_id = 1;
  bool edge_present      = 2;
  AgentTemporalEdgeProto edge = 3;
}

message AgentBeliefDivergenceResponse {
  repeated AgentAuthorBeliefProto beliefs = 1;
  bool divergent = 2;
}

// ─── Graph Index ────────────────────────────────────────────────────────────

message CreateGraphIndexRequest {
  string name              = 1; // graph index name (e.g., "news")
  uint32 dim               = 2; // vector dimensionality
  uint32 m                 = 3; // HNSW M parameter
  uint32 m_max0            = 4; // HNSW max neighbors at layer 0
  uint32 ef_construction   = 5; // HNSW ef_construction
  uint32 ef_search         = 6; // HNSW ef_search
  string metric            = 7; // "l2", "cosine", "ip"
  uint64 edge_segment_duration = 8; // time segment for edge partitioning (ms)
  string backend           = 9; // "disk_hnsw" or "spfresh" (default: "spfresh")
  // Per-graph default recency half-life (ms) for graph_rag_search recency
  // decay. 0 = use the server ~180d constant. A GraphRagSearch sending
  // decay_half_life_ms == 0 resolves to this persisted value when set.
  uint64 default_decay_half_life_ms = 10;
  // In-list vector quantizer: "sq8", "pq", or "rabitq". "" = backend default
  // (disk_hnsw -> sq8, spfresh -> pq), which is byte-identical to pre-field
  // behavior. Part of the graph's schema identity: a re-create with a
  // different quantizer is rejected (change it via an explicit rebuild).
  string quantizer = 11;
  // P5b: shared-CF sub-index id (+1 encoding: 0 = dedicated CF, N = sub N-1)
  // so the field is optional without proto3 optional syntax.
  uint32 shared_sub_plus1 = 12;
  // The metadata-registry cf_id this graph is registered under. A data node
  // creates its physical CF at exactly this id, keeping the node-local
  // physical id space and the cluster-wide metadata space identical — a
  // replicated write batch names its CF by number, so letting each node pick
  // its own would wedge apply or land rows in the wrong column family.
  //
  // The gateway reserves the id (MetadataOp::ReserveCfId) before fanning out,
  // so this is authoritative rather than advisory. 0 means the reservation
  // could not be obtained; the node then allocates locally and the applier's
  // id translation covers the difference.
  uint32 cf_id_hint = 13;
}

message CreateGraphIndexResponse {
  // The 6 CF IDs allocated for this graph index.
  uint32 adj_cf     = 1;
  uint32 vec_cf     = 2;
  uint32 sq_cf      = 3;
  uint32 edge_cf    = 4;
  uint32 erev_cf    = 5;
  uint32 node_cf    = 6;
}

message DropGraphIndexRequest {
  string name = 1;
}

message DropGraphIndexResponse {}

message GraphAddNodeRequest {
  string graph_name     = 1;
  uint64 node_id        = 2;
  bytes  properties     = 3;
  repeated float vector = 4; // optional; empty = no vector
  repeated string labels = 5; // optional first-class node labels (multi-label)
}

message GraphAddNodeResponse {}

message GraphBatchAddNodeRequest {
  string graph_name                  = 1;
  repeated GraphAddNodeEntry nodes   = 2;
}

message GraphAddNodeEntry {
  uint64 node_id        = 1;
  bytes  properties     = 2;
  repeated float vector = 3;
  repeated string labels = 4; // optional first-class node labels (multi-label)
}

message GraphBatchAddNodeResponse {}

message GraphRemoveNodeRequest {
  string graph_name = 1;
  uint64 node_id    = 2;
}

message GraphRemoveNodeResponse {}

message GraphAddEdgeRequest {
  string graph_name  = 1;
  uint64 src         = 2;
  uint64 dst         = 3;
  string edge_type   = 4;
  uint64 valid_from  = 5; // timestamp (ms)
  uint64 valid_to    = 6; // 0 = no expiry
  bytes  properties  = 7;
  // Bitemporal transaction-time (recorded-at), ms. tx_from = when this edge
  // revision became believed (0 = default to the write time at the data node).
  // tx_to = when it was superseded/corrected (0 = still believed).
  uint64 tx_from     = 8;
  uint64 tx_to       = 9;
}

message GraphAddEdgeResponse {}

message GraphBatchAddEdgeRequest {
  string graph_name                 = 1;
  repeated GraphAddEdgeEntry edges  = 2;
}

message GraphAddEdgeEntry {
  uint64 src         = 1;
  uint64 dst         = 2;
  string edge_type   = 3;
  uint64 valid_from  = 4; // timestamp (ms)
  uint64 valid_to    = 5; // 0 = no expiry
  bytes  properties  = 6;
  uint64 tx_from     = 7; // 0 = default to write time at the data node
  uint64 tx_to       = 8; // 0 = still believed
}

message GraphBatchAddEdgeResponse {}

message GraphBatchWriteEntry {
  string cf_name   = 1;
  WriteOp op       = 2;
  bytes key        = 3;
  bytes value      = 4;
  uint64 expire_at = 5;
}

message GraphBatchWriteRequest {
  string graph_name = 1;
  repeated GraphBatchWriteEntry entries = 2;
}

message GraphBatchWriteResponse {}

message GraphBatchReadEntry {
  string cf_name = 1;
  bytes  key     = 2;
}

message GraphBatchReadRequest {
  string graph_name = 1;
  repeated GraphBatchReadEntry entries = 2;
}

message GraphBatchReadValue {
  bool   found     = 1;
  bytes  value     = 2;
  uint64 expire_at = 3;
}

message GraphBatchReadResponse {
  // One value per request entry, in the same order.
  repeated GraphBatchReadValue values = 1;
}

message GraphSearchRequest {
  string graph_name     = 1;
  repeated float query  = 2;
  uint32 k              = 3;
  uint32 ef             = 4; // 0 = use default ef_search
}

message GraphSearchResult {
  uint64 node_id  = 1;
  float  distance = 2;
}

message GraphSearchResponse {
  repeated GraphSearchResult results = 1;
}

// ─── Vector-anchored multi-hop expansion (GraphRAG) ───────────────────────────

message GraphSearchExpandRequest {
  string graph_name     = 1;
  repeated float query  = 2; // query embedding for anchor selection
  uint32 k              = 3; // number of vector anchors (top-k)
  uint32 ef             = 4; // 0 = use default ef_search
  uint32 depth          = 5; // BFS hop count from each anchor (0 = anchors only)
  string edge_type      = 6; // empty = all edge types
  uint64 as_of          = 7; // 0 = no time bound; only edges with valid_from <= as_of
  bool   reverse        = 8; // true = expand along incoming edges
  // Upper bound on total nodes returned (anchors + expanded). 0 = engine
  // default (DEFAULT_MAX_EXPAND_NODES = 10000, never unbounded); pass an
  // explicit large value (e.g. u32 max) to raise the cap.
  uint32 max_nodes      = 9;
  // Per-node edge fanout cap: when expanding a single frontier node, scan at
  // most this many of its edges, so a high-degree supernode cannot be
  // materialized in full (OOM / latency). 0 = engine default.
  uint32 max_degree     = 10;
  // Transaction-time (as-of-then belief) point, ms. 0 = current belief (no
  // bound). When set, only edges believed at `tx_as_of` are traversed/returned
  // (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)); legacy edges
  // with tx_from == 0 are treated as always known. This excludes edges that
  // were transactionally corrected/retracted after `tx_as_of`, matching the
  // bitemporal predicate the adjacent traversal path already enforces.
  uint64 tx_as_of       = 11;
}

message GraphExpandNode {
  uint64 node_id    = 1;
  // Vector distance to the query for anchor nodes; for nodes reached only via
  // BFS this is the distance of the anchor they were discovered from.
  float  distance   = 2;
  uint32 hop        = 3; // 0 = anchor, 1 = one hop away, ...
  bool   is_anchor  = 4;
}

message GraphSearchExpandResponse {
  repeated GraphExpandNode nodes = 1;
  repeated GraphEdge       edges = 2;
}

// ─── Unified GraphRAG retrieval (#696) ────────────────────────────────────────
// One call: vector seed -> bitemporal graph expansion -> blended rerank
// (similarity + recency + graph-distance) -> facts with provenance + validity.
// All new fields default to 0/empty ⇒ backward compatible.

// How the three component scores are fused into one scalar.
enum ScoreFusion {
  RRF    = 0; // rank-based reciprocal-rank fusion (robust across score scales)
  LINEAR = 1; // weighted linear blend of normalized component scores
}

message GraphRagSearchRequest {
  string graph_name      = 1;
  // Exactly one of {query_text, query} is required. query_text needs an
  // embedding model on the gateway; query is a pre-embedded vector (model-free).
  string query_text      = 2;
  repeated float query   = 3;
  repeated string extra_queries = 4; // optional multi-query; embedded + searched too

  // ── seed + expansion (mirrors GraphSearchExpandRequest) ──
  uint32 k               = 5;  // vector anchors (top-k)
  uint32 ef              = 6;  // 0 = default ef_search
  uint32 depth           = 7;  // BFS hops (0 = anchors only)
  string edge_type       = 8;  // empty = all
  bool   reverse         = 9;
  uint32 max_nodes       = 10; // cap on expanded set
  uint32 max_degree      = 11; // per-node fanout cap

  // ── bitemporal scope (reused on the durable graph edge codec) ──
  uint64 as_of           = 12; // valid-time point (0 = no bound)
  uint64 tx_as_of        = 13; // transaction-time point (0 = current belief)

  // ── memory scope (residual node-property predicate; not an ACL) ──
  string scope           = 14; // matches props.scope (empty = any)
  string user            = 15; // matches props.user (empty = any)
  string session_id      = 16; // matches props.session_id (empty = any)

  // ── blending ──
  ScoreFusion fusion     = 17; // RRF (default) | LINEAR
  float w_similarity     = 18; // linear weights (used when fusion = LINEAR)
  float w_recency        = 19;
  float w_graph_distance = 20;
  uint64 decay_half_life_ms = 21; // recency half-life; 0 = server default (~180d)
  uint64 now_ms          = 22; // reference epoch for recency (0 = wall clock)
  bool   use_reranker    = 23; // optional cross-encoder pass (no-op if absent)
  bool   use_mmr         = 24; // optional MMR diversification
  float  mmr_lambda      = 25;
  uint32 final_k         = 26; // results returned after rerank (0 = k)
  // graph_distance = Personalized PageRank stationary mass over the induced
  // anchor-seeded subgraph (Zep node-distance / mention-reranker analogue,
  // best-accuracy). unset/false ⇒ cheap 1/(1+hop) default. The server flag
  // STATELET_GRAPHRAG_PPR=1 forces PPR on for all calls.
  bool   use_ppr_distance = 27;
}

message GraphRagFact {
  uint64 node_id        = 1;
  bytes  properties     = 2; // hydrated ROLE_NODE JSON (provenance source)
  float  score          = 3; // final blended score (higher = better)
  // component scores for explainability / benchmark ablation:
  float  similarity     = 4; // 1 - normalized vector distance
  float  recency        = 5; // decay weight in [0,1]
  float  graph_distance = 6; // PPR mass when use_ppr_distance, else 1/(1+hop)
  uint32 hop            = 7; // 0 = anchor
  bool   is_anchor      = 8;
  // validity of the strongest contributing edge (bitemporal provenance):
  uint64 valid_from     = 9;
  uint64 valid_to       = 10;
  uint64 tx_from        = 11;
  uint64 tx_to          = 12;
  repeated GraphEdge supporting_edges = 13; // edges that reached this fact
}

message GraphRagSearchResponse {
  repeated GraphRagFact facts = 1;
  bool truncated = 2; // true if max_nodes / frontier cap hit during expansion
}

message GraphGetNodeRequest {
  string graph_name = 1;
  uint64 node_id    = 2;
  // Optional bitemporal visibility point for the node's property blob. 0 means
  // no filter for that time axis.
  uint64 as_of      = 3;
  uint64 tx_as_of   = 4;
}

message GraphGetNodeResponse {
  bool  found      = 1;
  bytes properties = 2;
  repeated uint32 label_ids = 3; // node's first-class label ids (empty if none)
}

message GraphQueryEdgesRequest {
  string graph_name  = 1;
  uint64 node_id     = 2;
  string edge_type   = 3; // empty = all types
  uint64 time_start  = 4; // 0 = no lower bound
  uint64 time_end    = 5; // 0 = no upper bound
  bool   reverse     = 6; // true = incoming edges
  // Per-node edge fanout cap: scan at most this many of the node's edges so a
  // high-degree supernode cannot be materialized in full over RPC (OOM /
  // latency). 0 = engine default (DEFAULT_MAX_DEGREE).
  uint32 max_degree  = 7;
}

message GraphEdge {
  uint64 src         = 1;
  uint64 dst         = 2;
  string edge_type   = 3;
  uint64 valid_from  = 4;
  uint64 valid_to    = 5;
  bytes  properties  = 6;
  // Bitemporal transaction-time (recorded-at), ms. tx_from = 0 for legacy edges
  // with no encoded transaction time (== "always known"). tx_to = 0 = still
  // believed.
  uint64 tx_from     = 7;
  uint64 tx_to       = 8;
  // Raw interned edge-type id as decoded from the on-disk edge key. Unlike
  // `edge_type` (the human-facing resolution, which can be a synthetic
  // `type_{id}` fallback), this is authoritative for reconstructing the exact
  // stored key — internal removal paths use it to tombstone an edge's
  // forward/reverse rows on their owning shards. 0 for legacy producers and
  // for edge sources that carry no interned key (e.g. the JSON gedge store).
  uint32 type_id     = 9;
}

message GraphQueryEdgesResponse {
  repeated GraphEdge edges = 1;
}

// Batched edge query for a set of nodes that all live on the same shard.
message GraphQueryEdgesBatchRequest {
  string graph_name = 1;
  // Nodes whose edges to fetch (all routed to the same shard by the caller).
  repeated uint64 node_ids = 2;
  string edge_type  = 3; // empty = all types (applied at the data node)
  uint64 time_start = 4; // 0 = no lower bound
  uint64 time_end   = 5; // 0 = no upper bound
  bool   reverse    = 6; // true = incoming edges
  // Point-in-time filter (ms), applied at the data node:
  // valid_from <= as_of && (valid_to == 0 || valid_to > as_of). 0 = no filter.
  uint64 as_of      = 7;
  // As-of-then transaction-time filter (ms), applied at the data node:
  // tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of). 0 = no filter.
  uint64 tx_as_of   = 8;
  // Per-node edge fanout cap, applied at the data node: scan at most this many
  // of each node's edges so a high-degree supernode cannot be materialized in
  // full over RPC (OOM / latency). 0 = engine default (DEFAULT_MAX_DEGREE).
  uint32 max_degree = 9;
  // Optional HLC snapshot timestamp (packed u64) for cross-shard txn read-path
  // lock resolution (epic #1478, Phase 4). 0 = the gateway uses a fresh now().
  // Only consulted when STATELET_CROSS_SHARD_TXN is ON; otherwise ignored and
  // edge reads behave exactly as today.
  uint64 read_ts    = 10;
}

// Per-node edge list (mirrors the order of GraphQueryEdgesBatchRequest.node_ids).
message GraphNodeEdges {
  uint64 node_id = 1;
  repeated GraphEdge edges = 2;
}

message GraphQueryEdgesBatchResponse {
  repeated GraphNodeEdges nodes = 1;
}

// Batched node-properties fetch for nodes on the same shard.
message GraphGetNodesBatchRequest {
  string graph_name = 1;
  repeated uint64 node_ids = 2;
}

message GraphNodeProps {
  uint64 node_id    = 1;
  bool   found      = 2;
  bytes  properties = 3;
  repeated uint32 label_ids = 4; // node's first-class label ids (empty if none)
  repeated string labels    = 5; // resolved label strings (for label-filtered match)
}

message GraphGetNodesBatchResponse {
  repeated GraphNodeProps nodes = 1;
}

// ─── Multi-hop Traversal ────────────────────────────────────────────────────

// Direction of edge expansion during a multi-hop traversal.
enum GraphTraverseDirection {
  GRAPH_TRAVERSE_FORWARD = 0; // follow outgoing edges (ROLE_EDGE)
  GRAPH_TRAVERSE_REVERSE = 1; // follow incoming edges (ROLE_EREV)
  GRAPH_TRAVERSE_BOTH    = 2; // follow both
}

message GraphTraverseRequest {
  string graph_name = 1;
  uint64 start_node = 2;
  GraphTraverseDirection direction = 3;
  // Maximum BFS depth (hops) from the start node. 0 = start node only.
  uint32 max_depth  = 4;
  // Only follow edges of this type (empty = all types).
  string edge_type  = 5;
  // Point-in-time filter (ms): only follow edges valid at this instant
  // (valid_from <= as_of && (valid_to == 0 || valid_to > as_of)). 0 = no filter.
  uint64 as_of      = 6;
  // Cap on the total number of expanded/visited nodes to bound latency.
  // 0 = use a server default (currently 100000). Pass uint32 max (0xFFFFFFFF)
  // to explicitly opt out of the cap (unbounded traversal).
  uint32 max_frontier = 7;
  // When true, populate `path` (the edge chain from start to each node).
  bool   return_paths = 8;
  // Per-node edge fanout cap: when expanding a single frontier node, scan at
  // most this many of its edges, so a high-degree supernode cannot be
  // materialized in full (OOM / latency). 0 = engine default.
  uint32 max_degree   = 9;
  // As-of-then transaction-time filter (ms): only follow edges that were already
  // believed at this recorded-time
  // (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)). Combined with
  // `as_of` this gives bitemporal "what was believed at tx_as_of about valid-time
  // as_of". 0 = no transaction-time filter (legacy edges are always known).
  uint64 tx_as_of     = 10;
}

// A node reached by the traversal, with its depth, hydrated properties and
// (optionally) the path of edges from the start node.
message GraphTraverseNode {
  uint64 node_id    = 1;
  uint32 depth      = 2;
  bytes  properties = 3; // hydrated ROLE_NODE props (empty if none)
  bool   has_props  = 4;
  // The chain of edges from the start node to this node (only when
  // return_paths is set). Empty for the start node itself.
  repeated GraphEdge path = 5;
  // The node's first-class label ids (resolved client-side via the dict, or
  // used by the gateway for a cheap u32 membership check during label-filtered
  // matching). Empty for legacy/unlabeled nodes.
  repeated uint32 label_ids = 6;
}

message GraphTraverseResponse {
  // All visited nodes (including the start node), in BFS order.
  repeated GraphTraverseNode nodes = 1;
  // All edges traversed during expansion (deduplicated tree edges).
  repeated GraphEdge edges = 2;
  // True if the traversal stopped early because max_frontier was reached.
  bool truncated = 3;
}

// ─── Nodes-by-label scan ──────────────────────────────────────────────────────

message GraphNodesByLabelRequest {
  string graph_name = 1;
  // Conjunctive (AND) label set: a node is returned only if it carries every
  // label. The engine intersects the posting lists, scanning the smallest
  // first (selectivity heuristic). An unknown label ⇒ empty result.
  repeated string labels = 2;
  // Cap on the number of returned node ids. 0 = engine default
  // (effective_max_frontier). Applied per shard; the gateway re-applies the
  // global cap after merging shard results.
  uint32 max_nodes = 3;
  // When true, include each node's ROLE_NodeProp JSON so the caller can apply
  // a property residual (WHERE / inline map) without a second round trip.
  bool hydrate_props = 4;
  // Reserved for temporal label validity. 0 = now.
  uint64 as_of = 5;
}

message GraphNodesByLabelNode {
  uint64 node_id    = 1;
  // Hydrated ROLE_NodeProp JSON (empty when hydrate_props is false or the node
  // has no props).
  bytes  properties = 2;
}

message GraphNodesByLabelResponse {
  repeated GraphNodesByLabelNode nodes = 1;
  // True if the (per-shard) cap was hit, so the result may be incomplete.
  bool truncated = 2;
}

// ─── Temporal Join ──────────────────────────────────────────────────────────

message GraphTemporalJoinRequest {
  string graph_name    = 1; // graph index name
  uint64 node_id       = 2; // source node (e.g., symbol node)
  string edge_type     = 3; // filter by edge type (empty = all)
  uint64 time_start    = 4; // time range start (ms), 0 = no bound
  uint64 time_end      = 5; // time range end (ms), 0 = no bound
  // KV lookup config: for each edge timestamp, look up these KV keys.
  // Key template supports {timestamp} placeholder, e.g. "AAPL:{timestamp}".
  string kv_key_template = 6;
  uint32 kv_cf           = 7; // column family for KV lookups (0 = default)
}

message TemporalJoinEntry {
  // The graph edge.
  GraphEdge edge         = 1;
  // The KV value at the edge's timestamp (empty if not found).
  bytes  kv_value        = 2;
  bool   kv_found        = 3;
  // The KV key that was looked up.
  string kv_key          = 4;
}

message GraphTemporalJoinResponse {
  repeated TemporalJoinEntry entries = 1;
}

// ─── Graph Analytics ──────────────────────────────────────────────────────────

enum GraphAnalyticsAlgorithm {
  GRAPH_ANALYTICS_PAGERANK           = 0;
  GRAPH_ANALYTICS_WCC                = 1; // weakly connected components
  GRAPH_ANALYTICS_DEGREE_CENTRALITY  = 2;
  GRAPH_ANALYTICS_LABEL_PROPAGATION  = 3; // deferred (community detection)
}

message GraphAnalyticsRequest {
  string graph_name                  = 1;
  GraphAnalyticsAlgorithm algorithm  = 2;
  // Optional: keep only edges of this type (empty = all types).
  string edge_type                   = 3;
  // Optional: only edges live at this timestamp (ms); 0 = current/all.
  uint64 as_of                       = 4;
  // PageRank / iterative params (0 = engine default).
  uint32 iterations                  = 5;
  float  tolerance                   = 6;
  float  damping                     = 7; // PageRank damping factor (default 0.85)
  // If true, persist scores back into each node's properties under a reserved
  // "__analytics" sub-key so later traversals/searches can order by them.
  bool   write_back                  = 8;
  // Optional cap on returned rows (0 = return all). Write-back still applies to
  // every node regardless of this cap.
  uint32 top_k                       = 9;
  // Optional transaction-time (recorded-at / belief) point (ms); 0 = current
  // belief / no filter. When set, only edges believed at `tx_as_of` are counted
  // (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)); legacy edges
  // (tx_from == 0) are always known. This is the same predicate the bitemporal
  // traversal read path applies, so analytics excludes belief-retracted edges
  // consistently with every other graph reader (issue #1920).
  uint64 tx_as_of                    = 10;
}

message GraphAnalyticsScore {
  uint64 node_id   = 1;
  double score     = 2;
  // For WCC: the component label (smallest node id in the component). 0 for
  // score-based algorithms.
  uint64 component = 3;
}

message GraphAnalyticsResponse {
  string algorithm                  = 1; // canonical name actually run
  uint64 node_count                 = 2;
  uint64 edge_count                 = 3;
  uint32 iterations                 = 4; // iterations performed (0 for non-iterative)
  bool   wrote_back                 = 5;
  repeated GraphAnalyticsScore scores = 6;
}

// Internal: ask one shard to dump its local analytics edge list. Mirrors the
// edge_type / as_of filters of GraphAnalyticsRequest so each shard applies the
// same temporal/type filtering before returning its (src,dst) pairs.
message GraphAnalyticsEdgesRequest {
  string graph_name = 1;
  string edge_type  = 2; // empty = all types
  uint64 as_of      = 3; // 0 = current/all
  // Per-shard ownership filter (issue #1601). When non-empty, the data node
  // dumps ONLY edges whose source's forward-edge routing key
  // (`[ROLE_EDGE][src:u64 BE]`) falls inside one of these half-open
  // `[start_key, end_key)` ranges — i.e. the shards this node is the leader of.
  // Without it, a node that leads some shards but also holds follower replicas
  // of OTHER shards (replication factor > 1) dumps the replicated edges too, so
  // the gateway union double-counts every replicated directed edge. An empty
  // list disables the filter (full local dump) for backward compatibility.
  repeated GraphKeyRange owned_ranges = 4;
  // Transaction-time (recorded-at / belief) point (ms); 0 = current belief / no
  // filter. Mirrors GraphAnalyticsRequest.tx_as_of so each shard applies the
  // same belief filter before dumping its (src,dst) pairs (issue #1920).
  uint64 tx_as_of   = 5;
}

// A half-open `[start_key, end_key)` key range. An empty `end_key` means
// "to the end" (no upper bound); an empty `start_key` means "from the start".
message GraphKeyRange {
  bytes start_key = 1;
  bytes end_key   = 2;
}

// Packed parallel arrays: src[i] -> dst[i] is one directed edge. Kept as two
// `repeated uint64` (rather than repeated GraphEdge) so the per-shard dump is
// compact — analytics only needs endpoints, not types/timestamps/properties.
message GraphAnalyticsEdgesResponse {
  repeated uint64 src = 1;
  repeated uint64 dst = 2;
}

// Internal: write a batch of analytics scores back into node properties for the
// nodes this shard owns. The gateway routes each node to its owning shard, so
// a data node only ever receives its own node ids here.
message GraphAnalyticsWriteScoresRequest {
  string graph_name = 1;
  string algorithm  = 2; // canonical name (e.g. "PageRank", "WCC")
  repeated GraphAnalyticsScore scores = 3;
}

message GraphAnalyticsWriteScoresResponse {
  uint64 wrote = 1; // number of node-property rows written
}

// ─── Weighted shortest path / pathfinding ───────────────────────────────────
//
// Cost-ordered (BinaryHeap / Dijkstra) traversal over user graph edges. The
// per-edge cost is decoded from the edge's `properties` bytes according to the
// chosen `weight_encoding`. Paths are computed as-of `as_of` by reusing the
// temporal valid_from/valid_to filter.

// How to decode a non-negative edge cost (weight) from an edge's properties.
enum GraphWeightEncoding {
  // No weight is read; every edge costs 1.0 (== unweighted hop count).
  GRAPH_WEIGHT_UNIT = 0;
  // Properties are the UTF-8 text "<key>:<float>" (e.g. "weight:0.9"). The
  // `weight_key` field selects <key> (default "weight"). The first matching
  // "<key>:<number>" token wins; missing key falls back to `default_weight`.
  GRAPH_WEIGHT_TEXT_KEY = 1;
  // Properties are a UTF-8 JSON object; `weight_key` names a top-level numeric
  // field (default "weight"). Missing/non-numeric falls back to `default_weight`.
  GRAPH_WEIGHT_JSON_KEY = 2;
  // Properties begin with a little-endian f32 cost in the first 4 bytes. This
  // is a NEW convention defined by this RPC (no such prefix existed before);
  // writers that want it must prepend the 4 weight bytes themselves.
  GRAPH_WEIGHT_F32_LE_PREFIX = 3;
}

// Pathfinding algorithm. A* currently shares Dijkstra's relaxation (no spatial
// heuristic is available for arbitrary node ids), so it behaves as Dijkstra
// but is accepted for forward compatibility.
enum GraphPathAlgorithm {
  GRAPH_PATH_DIJKSTRA = 0;
  GRAPH_PATH_ASTAR    = 1;
}

message GraphShortestPathRequest {
  string graph_name              = 1;
  uint64 src                     = 2;
  // Single destination. Used when `targets` is empty.
  uint64 dst                     = 3;
  // Optional destination set; the search stops once the k cheapest of these
  // are settled. When non-empty, `dst` is ignored.
  repeated uint64 targets        = 4;
  string edge_type               = 5; // optional: only traverse this edge type
  GraphWeightEncoding weight_encoding = 6;
  string weight_key              = 7; // key for TEXT_KEY / JSON_KEY (default "weight")
  double default_weight          = 8; // fallback cost when no weight decodes (default 1.0)
  GraphPathAlgorithm algorithm   = 9;
  // k-shortest loopless paths via Yen's algorithm (0 or 1 = single best path).
  uint32 k                       = 10;
  // As-of timestamp (ms): only edges live at this instant are traversed
  // (valid_from <= as_of < valid_to, valid_to == 0 meaning no expiry).
  // 0 = no temporal filter.
  uint64 as_of                   = 11;
  // Prune any path whose accumulated cost exceeds this (0 = no limit).
  double max_cost                = 12;
  // Safety bound on settled nodes per Dijkstra run (0 = engine default).
  uint64 max_expansions          = 13;
  // As-of-then transaction-time filter (ms): only traverse edges believed at
  // this recorded-time (tx_from <= tx_as_of && (tx_to == 0 || tx_to > tx_as_of)).
  // 0 = no transaction-time filter.
  uint64 tx_as_of                = 14;
}

message GraphPathEdge {
  uint64 src         = 1;
  uint64 dst         = 2;
  string edge_type   = 3;
  double weight      = 4; // decoded edge cost
  uint64 valid_from  = 5;
}

message GraphPath {
  repeated uint64 nodes        = 1; // ordered node ids from src to dst
  repeated GraphPathEdge edges = 2; // edges between consecutive nodes
  double total_cost            = 3;
}

message GraphShortestPathResponse {
  repeated GraphPath paths = 1; // ascending by total_cost (best first)
}

// ─── Declarative pattern-match graph query (openCypher subset) ───────────────
//
// Read-only. The gateway parses a fixed openCypher subset and compiles each
// MATCH segment to engine traversal primitives:
//   * linear path patterns        -> GraphTraverse (edge_type / as_of / depth)
//   * anchored vector hints       -> GraphSearchExpand (expand_from_anchors)
//   * shortestPath(...) patterns  -> GraphShortestPath
// WHERE predicates over node properties and an `as_of(<ms>)` temporal predicate
// are evaluated against hydrated ROLE_NodeProp JSON at the gateway. RETURN /
// LIMIT shape the rows. CREATE / MERGE are rejected.

message GraphQueryRequest {
  // The graph index to query. When empty the gateway resolves a default graph
  // (same policy as the other Graph* RPCs).
  string graph_name = 1;
  // The openCypher-subset query text.
  string cypher     = 2;
  // Hard cap on returned rows regardless of any LIMIT in the query (0 = no
  // extra cap; the parsed LIMIT, if any, still applies).
  uint32 max_rows   = 3;
  // Full bitemporal point-in-time (epic #1635, Phase 4). Out-of-band defaults
  // for the valid-time (`as_of`) and transaction-time (`tx_as_of`) the query is
  // evaluated against. `0` means "current" (no temporal filter) for each axis,
  // matching every other Graph* read path. An `AS OF <valid>, <tx>` clause in
  // the `cypher` text overrides these when present; otherwise these supply the
  // temporal context out-of-band. `tx_as_of` is threaded into every downstream
  // read-path request that carries the field (traverse / shortest-path /
  // expand / rag / triple BGP), replacing the previously hardcoded `0`.
  uint64 as_of      = 4;
  uint64 tx_as_of   = 5;
}

// One projected column value. Exactly one of the typed fields is meaningful per
// `kind`; `json` carries hydrated node properties verbatim.
message GraphQueryValue {
  enum Kind {
    NULL    = 0;
    INT     = 1; // node id / integer property
    DOUBLE  = 2; // numeric property
    STRING  = 3; // string property / edge type
    BOOL    = 4; // boolean property
    JSON    = 5; // raw ROLE_NodeProp JSON bytes for a whole node
  }
  Kind   kind       = 1;
  int64  int_value  = 2;
  double dbl_value  = 3;
  string str_value  = 4;
  bool   bool_value = 5;
  bytes  json_value = 6;
}

message GraphQueryRow {
  repeated GraphQueryValue values = 1;
}

message GraphQueryResponse {
  // RETURN column names, in projection order.
  repeated string columns       = 1;
  // Result rows, in projection order.
  repeated GraphQueryRow rows   = 2;
  // Non-fatal query warnings (epic #1429, Phase 4). Populated when a label-scan
  // anchor resolution hit the per-shard frontier cap so the anchor set was
  // truncated (the worst case is a hub label, e.g. `:Person`, with no property
  // residual to narrow it). Surfaced — never silently truncated — so the client
  // knows the result may be incomplete.
  repeated string warnings      = 3;
}

// ─── Text Embedding + Search (gateway-side) ─────────────────────────────────
// The gateway runs the embedding model locally, then routes to data nodes
// for vector index and KV storage. MCP clients send raw text, never vectors.

message TextPutRequest {
  string index_name = 1; // vector index name
  uint64 vector_id  = 2; // unique ID for this entry
  string text       = 3; // raw text — gateway embeds this
  bytes  metadata   = 4; // JSON metadata stored alongside in KV
  uint32 kv_cf      = 5; // column family for KV metadata (0 = default)
}

message TextPutResponse {}

message TextSearchRequest {
  string index_name = 1; // vector index name
  string query      = 2; // raw text query — gateway embeds this
  uint32 k          = 3; // number of nearest neighbors
  uint32 ef_search  = 4; // optional ef_search override (0 = default)
  uint32 kv_cf      = 5; // column family for KV hydration (0 = default)
  bytes  kv_prefix  = 6; // optional: only hydrate keys matching this prefix
}

message TextSearchResult {
  uint64 id       = 1;
  float  distance = 2;
  bytes  metadata = 3; // hydrated KV value (JSON metadata)
}

message TextSearchResponse {
  repeated TextSearchResult results = 1;
}

// ─── Text + Graph (gateway-side embedding + graph storage) ──────────────────

message TextGraphPutRequest {
  string graph_name   = 1; // graph index name (e.g. "default")
  uint64 node_id      = 2; // unique node ID (microsecond timestamp)
  string text         = 3; // raw text — gateway embeds this
  bytes  properties   = 4; // JSON properties stored on the graph node
  // Optional: create an edge from this node to another
  uint64 edge_target  = 5; // 0 = no edge
  string edge_type    = 6; // e.g. "supersedes", "related_to"
  uint64 edge_valid_from = 7; // 0 = now
  uint64 edge_valid_to   = 8; // 0 = permanent
  // Backward-compatibility flag only. Gateway now uses server-side unified
  // inference mode configuration for all requests.
  bool   skip_server_llm = 9;
}

message TextGraphPutResponse {
  uint64 node_id = 1;
}

message TextGraphSearchRequest {
  string graph_name = 1;
  string query      = 2; // raw text — gateway embeds this
  uint32 k          = 3;
  uint32 ef         = 4; // 0 = default
  // Backward-compatibility flag only. Gateway now uses server-side unified
  // inference mode configuration for all requests.
  bool   skip_server_llm = 5;
  // Optional caller-provided rewritten queries for better retrieval.
  // Each entry is embedded and searched alongside the main query.
  // Used by MCP plugins where the host LLM generates rewrites at zero cost.
  repeated string extra_queries = 6;
  // Per-type result limits.  When set (> 0), the response additionally
  // populates `fact_results` and `chunk_results` with at most this many
  // entries each.  The combined `results` field is always populated for
  // backward compatibility.
  uint32 fact_k     = 10;  // max extracted-fact results (0 = default 7)
  uint32 chunk_k    = 11;  // max raw-chunk results      (0 = default 5)
  // When true, the gateway also returns a response-level answer-oriented
  // evidence bundle built from the final ranked result set.
  bool include_answer_bundle = 12;
  // (#827 LongMemEval Phase 5a) Multi-granularity ingest + RRF fusion. When
  // non-empty, the gateway runs one ANN ranking per requested granularity
  // (sentence/round/session/fact), folds each to sessions, and fuses the
  // per-granularity session rankings via Reciprocal Rank Fusion. Empty ⇒
  // legacy sentence-only behavior (zero regression). Recognized values:
  // "sentence", "round", "session", "fact".
  repeated string granularities = 13;
  // RRF constant `k` in score(d) = Σ_g weight_g / (rrf_k + rank_g(d)).
  // 0 ⇒ default 60 (the Elasticsearch/OpenSearch `rrf` retriever default).
  uint32 rrf_k = 14;
  // Conflict-as-data per-request opt-in (issue #812 / #783). When set to a
  // value other than UNSPECIFIED, the gateway runs a read-time conflict
  // re-rank over the final result set: it scans `contradicts` edges, groups
  // contradictory claims into conflict sets, arbitrates each set with this
  // policy (PR #711), and demotes the losing claims below the winner (never
  // removing them) before truncating to `k`. UNSPECIFIED = use the graph/env
  // default (off unless configured) → byte-identical to pre-#812 behaviour.
  ConflictPolicy conflict_policy = 15;
  // When true, `conflict_resolutions_json` includes the full per-loser detail
  // (demoted_score, reason) for every demoted claim. When false, the JSON is
  // still emitted for any resolved set but with the loser list collapsed to
  // node ids only. No effect unless `conflict_policy` fires.
  bool include_dissent = 16;
  // (#830 LongMemEval Phase 5d) Read-time entity consolidation. When true, the
  // gateway dereferences each query entity term through the persisted `gcanon:`
  // identity cluster (Phase 5c) and OR-expands retrieval over the cluster's
  // surface forms via the existing `load_alias_map` query-expansion seam, so
  // evidence written under *any* surface in the cluster is recalled regardless
  // of which surface the query used. Each consolidated result is stamped with
  // `canonical_entity_id`. Default false ⇒ byte-identical to pre-#830 behaviour;
  // a `gcanon:` miss / resolution-off falls back to today's recall (never an
  // error). Honoured only when entity resolution is enabled (this flag OR the
  // `STATELET_ENTITY_RESOLVE` env default).
  bool entity_consolidate = 17;
  // Optional caller-supplied reference date for relative-time queries and the
  // response-level answer-oriented memory pack. Kept out of `query` so date
  // tokens do not perturb retrieval ranking.
  string context_date = 18;
}

message TextGraphSearchResult {
  uint64 node_id    = 1;
  float  distance   = 2;
  bytes  properties = 3; // hydrated JSON properties
  // (#827) Granularities whose per-granularity session ranking contributed to
  // this result's fused RRF score (e.g. ["sentence","round"]). Empty when
  // multi-granularity fusion was not used.
  repeated string contributing_granularities = 4;
  // (#830 LongMemEval Phase 5d) The canonical entity id this result resolved to
  // when `entity_consolidate` was set and one of the result's surface forms
  // dereferenced through a persisted `gcanon:` identity cluster. 0 when the
  // result was not consolidated (resolution off, no cluster hit, or no surface
  // overlap) — so existing clients reading 0 see the pre-#830 behaviour.
  uint64 canonical_entity_id = 5;
}

message TextGraphSearchResponse {
  repeated TextGraphSearchResult results = 1;
  // Per-type results (populated when fact_k / chunk_k > 0 in the request).
  repeated TextGraphSearchResult fact_results  = 2; // extracted facts only
  repeated TextGraphSearchResult chunk_results = 3; // raw session chunks only
  // Optional UTF-8 JSON answer bundle built by the gateway when requested.
  bytes answer_bundle_json = 4;
  // Optional UTF-8 JSON object for the gateway-selected primary answer result.
  bytes primary_answer_result_json = 5;
  // Optional ordered UTF-8 JSON answer-result objects when answer bundling is enabled.
  repeated bytes answer_results_json = 6;
  // Conflict-as-data resolution log (issue #812 / #783). UTF-8 JSON array of
  // ResolvedClaim objects (#781 shape), one per arbitrated conflict set:
  // `{ winner, losers:[{node_id, demoted_score, reason}], policy, set_id }`.
  // Empty string when `conflict_policy` was UNSPECIFIED / the default was off,
  // the graph was conflict-cold, or no conflict set was found.
  string conflict_resolutions_json = 7;
  // Ready-to-read PLAIN-TEXT evidence block assembled by the gateway when
  // `STATELET_READER_BLOCK=1`, for feeding an LLM reader directly (no client-side
  // formatting). Empty when the flag is off or no compact memory was built.
  string memories = 8;
}

message TextGraphQueryEdgesRequest {
  string graph_name  = 1;
  uint64 node_id     = 2;
  string edge_type   = 3; // empty = all types
  uint64 time_start  = 4; // 0 = no lower bound
  uint64 time_end    = 5; // 0 = no upper bound
  bool   reverse     = 6; // true = incoming edges
  // Conflict-as-data per-request opt-in (issue #813 / #783, Phase 3b). When set
  // to a value other than UNSPECIFIED, the gateway runs a read-time conflict
  // re-order over the returned edge set: it scans `contradicts` edges among the
  // edge endpoints, groups contradictory claims into conflict sets, arbitrates
  // each set with this policy (PR #711), and re-orders the edges so winner edges
  // precede loser edges (edges have no distance, so demotion is ordering, not
  // re-scoring — losers are never removed). UNSPECIFIED = use the graph/env
  // default (off unless configured) → byte-identical to pre-#813 behaviour.
  ConflictPolicy conflict_policy = 7;
  // When true, `conflict_resolutions_json` includes the full per-loser detail
  // (reason) for every demoted edge endpoint. When false, the JSON is still
  // emitted for any resolved set but with the loser list collapsed to node ids
  // only. No effect unless `conflict_policy` fires.
  bool include_dissent = 8;
}

message TextGraphQueryEdgesResponse {
  repeated GraphEdge edges = 1;
  // Conflict-as-data resolution log (issue #813 / #783, Phase 3b). UTF-8 JSON
  // array of ResolvedClaim objects (#781 shape), one per arbitrated conflict
  // set: `{ winner, losers:[{node_id, reason}], policy, set_id }`. Empty string
  // when `conflict_policy` was UNSPECIFIED / the default was off, the graph was
  // conflict-cold, or no conflict set was found.
  string conflict_resolutions_json = 2;
}

message TextGraphGetNodeRequest {
  string graph_name = 1;
  uint64 node_id    = 2;
}

message TextGraphGetNodeResponse {
  bool  found      = 1;
  bytes properties = 2;
}

// ── Embed: pure text→vector (gateway-only) ───────────────────────────────────

message EmbedRequest {
  repeated string texts = 1; // texts to embed; response vectors match this order
  // Embed as a search query (true) vs a document/passage (false, default).
  // Asymmetric models (e5/gte with query/doc prefixes) score better when the
  // side matches; for a symmetric model the two paths are identical.
  bool is_query = 2;
}

// One embedding vector. Wrapper because proto3 forbids a directly-nested
// `repeated repeated float`.
message EmbedVector {
  repeated float values = 1;
}

message EmbedResponse {
  repeated EmbedVector vectors = 1; // one per input text, in request order
  uint32 dim = 2;                   // embedding dimension (0 if no model loaded)
}

// ── Conflict-as-data: read-time authority resolution (gateway-only) ──────────

// Which policy arbitrates a conflict set at read time. 1:1 with the policy
// core (PR #711, `ResolutionPolicy`). `UNSPECIFIED` means "use the graph/env
// default", which is itself off unless an `STATELET_CONFLICT_POLICY` default is
// configured — so a request that leaves this unset gets the pre-feature behaviour.
enum ConflictPolicy {
  CONFLICT_POLICY_UNSPECIFIED = 0;
  RECENCY                     = 1;
  TRUST                       = 2;
  CONFIDENCE                  = 3;
}

message ResolveConflictRequest {
  string graph_name = 1;
  uint64 node_id    = 2;            // any member of the conflict set
  string policy     = 3;            // "" = graph default; trust|recency|confidence|quorum
  uint64 as_of      = 4;            // bitemporal basis (ms); 0 = now
}

message ResolveConflictResponse {
  bool   found         = 1;         // false when node has no props row
  uint64 authoritative = 2;         // winning claim node id
  repeated uint64 dissenting = 3;   // every other claim, live + retired (never dropped)
  string policy        = 4;         // policy actually applied (resolved default)
  float  score         = 5;         // authoritative claim's policy score
  string rationale     = 6;         // human-readable, e.g. trust(author=admin)=0.90 ...
  bool   truncated     = 7;         // conflict set exceeded the per-set cap
  // One Vote per normalised value; populated only for the quorum policy.
  repeated ConflictVote votes = 8;
  // Normalized candidate hypotheses for semantic replay/forking. Probabilities
  // sum to 1.0 when non-empty; existing winner/dissent fields remain authoritative.
  repeated ConflictCandidate candidates = 9;
}

message ConflictVote {
  string value          = 1;
  float  weight         = 2;
  repeated uint64 supporters = 3;
}

message ConflictCandidate {
  string value          = 1;
  uint64 representative = 2;
  float  confidence     = 3;
  float  weight         = 4;
  float  probability    = 5;
  repeated uint64 supporters = 6;
}

// ─── ResolveEntities (#828 LongMemEval Phase 5b entity-resolution) ──────────
message ResolveEntitiesRequest {
  string graph_name = 1;
  // Optional surface forms / query terms to resolve. When empty, the resolver
  // scans the entity-mention index for the whole graph and clusters all surfaces.
  repeated string queries = 2;
  // ANN neighbors fetched per query surface (blocking fan-out). 0 = default.
  uint32 k = 3;
  // Similarity threshold override (0 = STATELET_ENTITY_SIM_THRESHOLD / default).
  float threshold = 4;
}

message ResolveEntitiesResponse {
  repeated EntityCluster clusters = 1;
  // True when resolution ran with the embedder available; false ⇒ alias/lexical
  // fallback only (embedding/index unavailable — never an error).
  bool embedder_used = 2;
}

message EntityCluster {
  uint64 canonical_id = 1;   // stable hash of the canonical surface form
  string canonical    = 2;   // display canonical surface
  repeated EntityClusterMember members = 3;
}

message EntityClusterMember {
  string surface = 1;
  string method  = 2;   // alias_rule | vector_nn | lexical
  float  score   = 3;
}

// ─── Triple store (epic #1432, Phase 1) ──────────────────────────────────────

// The object position of a triple — either a resource (another interned term,
// distinguished on disk by a clear high tag bit) or a literal value carrying a
// datatype byte (distinguished by the OBJECT_LITERAL_TAG high bit).
message TripleObject {
  oneof value {
    // Resource: an interned term identical to a subject/predicate term.
    string resource = 1;
    // Literal: an opaque value (UTF-8 here) tagged with a 1-byte datatype.
    bytes  literal  = 2;
  }
  // Datatype byte for a literal object (ignored for a resource object).
  uint32 datatype = 3;
}

message TriplePutRequest {
  string graph        = 1; // triple graph name → CF `t:{graph}` (CfType::User)
  string subject      = 2; // interned through the term dictionary
  string predicate    = 3; // interned through the term dictionary
  TripleObject object = 4; // resource (interned) or literal (LIT-stored)
  // Bitemporal validity window. `valid_to == 0` is treated as open-ended
  // (encoded as u64::MAX on disk). Keys end with the inverted `valid_from` so
  // the newest version of a given (s,p,o) sorts first within its prefix.
  uint64 valid_from   = 5;
  uint64 valid_to     = 6;
  bytes  props        = 7; // opaque property bytes stored on the SPO value
}

message TriplePutResponse {
  uint64 subject_id   = 1; // interned subject term id
  uint64 predicate_id = 2; // interned predicate term id
  // Interned object id. For a resource this is its term id; for a literal it is
  // the allocated lit_id (the on-disk object id is this value, MSB-tagged).
  uint64 object_id    = 3;
  bool   object_is_literal = 4;
  uint32 cf_id        = 5; // committed cf_id of `t:{graph}`
}

// ─── Triple store (epic #1432, Phase 2) ──────────────────────────────────────

// A single bound/unbound triple pattern. Any of subject/predicate/object may be
// left empty (unbound, a variable); the gateway selects the SPO/POS/OSP index
// whose leading columns are bound and prefix-scans it.
message TripleQueryRequest {
  string graph     = 1; // triple graph name → CF `t:{graph}`
  // Leave empty/unset to make a position UNBOUND (a `?` variable). A bound
  // subject/predicate is interned through the dictionary; an unbound object is
  // signalled by leaving `object` unset.
  string subject   = 2; // bound subject term (empty ⇒ unbound)
  string predicate = 3; // bound predicate term (empty ⇒ unbound)
  TripleObject object = 4; // bound object (unset ⇒ unbound)
  // Optional point-in-time filter: keep only triples whose validity window
  // `[valid_from, valid_to)` contains `as_of`. `0` disables the filter and
  // returns the newest version of every (s,p,o).
  uint64 as_of     = 5;
  // Cap on returned triples (0 ⇒ a server default). Newest-first by valid_from.
  uint32 limit     = 6;
}

// One resolved triple — TermIds resolved back to strings through ID2T / LIT.
message ResolvedTriple {
  string subject   = 1; // resolved subject string
  string predicate = 2; // resolved predicate string
  string object    = 3; // resolved object string (resource term or literal text)
  bool   object_is_literal = 4; // true ⇒ object is a literal value
  uint32 datatype  = 5; // literal datatype byte (0 for a resource object)
  uint64 valid_from = 6;
  uint64 valid_to   = 7; // u64::MAX on disk ⇒ open-ended (returned verbatim)
  bytes  props      = 8; // opaque property bytes from the SPO value
  uint64 subject_id   = 9;  // interned subject term id
  uint64 predicate_id = 10; // interned predicate term id
  uint64 object_id    = 11; // interned object id (term id, or literal lit_id)
}

message TripleQueryResponse {
  repeated ResolvedTriple triples = 1;
  uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
}

// ─── Triple store (epic #1432, Phase 3): BGP join ────────────────────────────

// One position of a BGP triple pattern: either a bound constant term, or a
// named variable that joins across patterns. A position is treated as a
// VARIABLE iff `var` is non-empty; otherwise it is the bound constant carried by
// the position's typed field. Variables sharing the same name across patterns
// are the same join variable.
message TripleBgpTerm {
  // Variable name (e.g. "x"). Non-empty ⇒ this position is an unbound/join
  // variable and the bound fields below are ignored.
  string var = 1;
  // Bound subject/predicate constant (used when this term is in the s/p
  // position and `var` is empty).
  string term = 2;
  // Bound object constant (used when this term is in the o position and `var`
  // is empty); resource or literal.
  TripleObject object = 3;
}

// One triple pattern in the BGP. `subject`/`predicate` use the term/var fields
// of TripleBgpTerm; `object` uses the object/var fields.
message TripleBgpPattern {
  TripleBgpTerm subject   = 1;
  TripleBgpTerm predicate = 2;
  TripleBgpTerm object    = 3;
}

message TripleBgpRequest {
  string graph = 1; // triple graph name → CF `t:{graph}`
  repeated TripleBgpPattern patterns = 2; // patterns sharing variables → join
  // Optional point-in-time filter applied to every pattern's scan (see
  // TripleQueryRequest.as_of). 0 disables it (newest version of each triple).
  uint64 as_of = 3;
  // Cap on returned binding rows (0 ⇒ a server default).
  uint32 limit = 4;
  // Optional transaction-time filter applied to every pattern's scan. 0
  // disables it (current belief / legacy behavior).
  uint64 tx_as_of = 5;
}

// One join solution: a variable name → resolved value assignment.
message TripleBgpBinding {
  // var name → resolved string value (resource term or literal text).
  map<string, string> values = 1;
  // var name → interned id (term id, or literal lit_id).
  map<string, uint64> ids = 2;
  // var name → true when ids[var] is a literal id, not a graph/entity node id.
  map<string, bool> literal_vars = 3;
}

message TripleBgpResponse {
  repeated TripleBgpBinding bindings = 1;
  uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
  bool truncated = 3; // true when an internal cap may have dropped solutions
}

// ─── Triple store (epic #1432, Phase 3): vector↔triple linkage ───────────────

enum TripleLinkMode {
  // HNSW.search(query,k) → candidate node ids → SPO prefix-scan (id, P?, ?) to
  // keep only ids that have the requested triple structure, re-ranked by the
  // vector distance.
  TRIPLE_LINK_VECTOR_TO_TRIPLE = 0;
  // Single-pattern/seed ids → HNSW.search around each → similar entities NOT
  // already connected through the requested predicate (similar-but-unconnected).
  TRIPLE_LINK_TRIPLE_TO_VECTOR = 1;
  // k-hop expansion from a seed over a predicate, frontier ranked by vector
  // similarity to the query; return the top-N most semantically relevant.
  TRIPLE_LINK_VECTOR_GUIDED_KHOP = 2;
}

message TripleLinkRequest {
  string graph = 1;       // triple graph name → CF `t:{graph}`
  string index_name = 2;  // vector index to search (HNSW/SpFresh)
  TripleLinkMode mode = 3;
  repeated float query = 4; // query vector (VECTOR_TO_TRIPLE / VECTOR_GUIDED_KHOP)
  uint32 k = 5;           // ANN fan-out / candidate pool size
  uint32 ef_search = 6;   // optional HNSW ef_search override (0 ⇒ default)
  // The predicate constraining the linkage. For VECTOR_TO_TRIPLE the candidate
  // must have an outgoing (id, predicate, ?) triple; for TRIPLE_TO_VECTOR the
  // seed's existing (seed, predicate, ?) objects are excluded from results; for
  // VECTOR_GUIDED_KHOP it is the edge predicate to expand.
  string predicate = 7;
  // Seed subject term (TRIPLE_TO_VECTOR / VECTOR_GUIDED_KHOP).
  string seed = 8;
  // Hops to expand for VECTOR_GUIDED_KHOP (>=1; 0 ⇒ 1).
  uint32 hops = 9;
  // Cap on returned linked entities (0 ⇒ a server default).
  uint32 limit = 10;
  // Optional point-in-time filter for the triple-side scans (see as_of above).
  uint64 as_of = 11;
}

// One linked entity: an id in the shared term/node id space plus its resolved
// string and the vector distance that ranked it.
message TripleLinkResult {
  uint64 id = 1;          // term-id == node-id (shared id space)
  string value = 2;       // resolved string (via ID2T)
  float  distance = 3;    // vector distance (smaller ⇒ closer)
  // True if this id was confirmed to have the requested triple structure
  // (VECTOR_TO_TRIPLE) / was reached by k-hop expansion (VECTOR_GUIDED_KHOP).
  bool   connected = 4;
}

message TripleLinkResponse {
  repeated TripleLinkResult results = 1;
  uint32 cf_id = 2; // committed cf_id of `t:{graph}` (0 if the graph has none)
}