File size: 104,626 Bytes
5353ddc
 
c16ca23
5353ddc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4517e5
5353ddc
 
 
 
 
 
 
 
 
 
 
 
 
d4517e5
5353ddc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4517e5
5353ddc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# 10 — Observability and Operations

**Parent:** [Architecture hub](README.md) · **Status tags:** `IMPLEMENTED` · `VERIFIED` ·
`MEASURED` · `OPEN` · `DEFERRED` · `NOT RUN`

**Sources of truth for this chapter, all read in full before writing:**

| Source | What it establishes |
|---|---|
| `release/CURRENT_RELEASE_STATE.md` §1 (Phase-0 reconnaissance) | the **measured live health payload**, the three `completed` readings, B-02 and B-07 as captured on 2026-09-25 |
| `release/repo/docs/DEPLOYMENT.md` §2, §5, §6, §7 | the same live payload as published, the cold-start statement, the platform traps |
| `.workbuddy-ai/scratch/apply-test/main.py` (835 lines) | the **deployed orchestrator with the prepared B-07 patch applied** — `/api/health`, `_dispatch`, `_via_tunnel`, `ensure_codespace_up`, `X-SatQuery-Transport` |
| `.workbuddy-ai/scratch/apply-test/tunnel.py` (241 lines) | `TunnelHub`, its `stats` property, `DEFAULT_REQUEST_TIMEOUT_S`, `DEFAULT_POLL_WAIT_S`, `DEFAULT_AGENT_TTL_S` |
| `deploy/render/main.py` (532 lines, monorepo) | the *non-deployed* copy: no tunnel, simpler `/api/health` — kept because it documents the route's contract |
| `deploy/render/codespaces.py` (178 lines) | the GitHub Codespaces control-plane client used by the wake path |
| `app/deployment.py` (1232 lines) | `health_payload()`, `DeploymentReport.health()`, the contract-state derivation, `_effective_device`, `_LEGAL_DEVICES`, `_HUB_REASONS`, `_MISSING_REASONS` |
| `app/space_app.py` (735 lines) | the Codespace's own `/v1/health`, the `HealthStatus.model_validate` assertion, `GPU_DURATIONS`, `decorate_gpu`, the log call sites |
| `core/schemas.py` (462 lines) | `ExecutionTrace`, `TraceStep`, `ModelRef`, `HealthStatus`, `ControllerState`, `Intent`, `ConfidenceBreakdown`, `_new_id` |
| `core/controller.py` (1331 lines) | the trace's producer: `_record`, `_execute`, `_verify`, `_detect_contradiction`, `_new_run_id`, the F-19 re-snapshot, `_log` |
| `core/errors.py` (315 lines) | `SatQueryError.to_trace()`, `scrub_paths` |
| `gateway/policy.py` (937 lines) | `new_request_id()` (`req_<hex>`), `translate_error` |
| `gateway/app.py` (691 lines) | `_log.error` on transport failure, the no-retry rule, `_transport_failure_detail` |
| `deploy/codespace/launch.sh` (194 lines) | the Codespace start procedure: serve + supervised tunnel agent + the post-start verification |
| `deploy/codespace/warm_cache.py` (116 lines) | the four pinned HF models and the warm-up contract |
| `deploy/codespace/serve.py` (25 lines) | the serve entrypoint the Codespace runs |
| `DELIVERY_REPORT_2026-09-25.md` §4 | the B-07 root shape and the ≈249 s measurement |
| `docs/FINAL_DELIVERY_TODO.md` §1.4, §5, §6 | the blocker register, the status board, the evidence register |

> **The one rule that governs this whole chapter.** A claim here is only as good as the file it came
> from. Where the code and a document disagree, the code is authoritative and the disagreement is
> stated. Where neither answers, this chapter writes
> `UNKNOWN — not established from the available evidence`.

---

## 1. Scope, and where observability sits

Observability is the **weakest** subsystem in SatQuery AI, and this chapter says so on its first
screen rather than on its last. The system was built to answer questions about satellite imagery
correctly and to say when it cannot; it was **not** built to be monitored. There is no metrics
endpoint, no dashboard, no alerting rule, no cost meter and no distributed trace. What exists is
three narrow surfaces, and this chapter documents each one to the field.

### 1.1 The honest framing

Three things are true and must be stated together:

1. **There is a real, measured liveness surface.** `GET /api/health` on the Render orchestrator
   answers with a live payload that includes a tunnel-agent block and the effective configuration
   (§2). It was probed during the 2026-09-25 sprint and its exact body is recorded.
2. **There is a real, per-run execution trace.** Every `POST /v1/analyze` returns a typed
   `ExecutionTrace` alongside its result (`core/schemas.py:296`), and that trace is the system's
   primary diagnostic artifact (§3). It is not a log line — it is a contract field.
3. **Everything else an operator would expect is absent.** No APM, no distributed tracing, no cost
   accounting, no per-model latency histogram, no error-rate counter (§6). The system observes
   *what a single run did*, and *whether the transport is currently up*. It does not observe the
   fleet, the trend, or the bill.

### 1.2 The three observation surfaces

```mermaid
flowchart TD
    subgraph Surfaces["The three observation surfaces"]
      H["/api/health<br/>transport + config<br/>a SNAPSHOT"]
      T["ExecutionTrace<br/>one run, step by step<br/>a PER-RUN RECORD"]
      L["stdout logs<br/>operator diagnostics<br/>an UNSTRUCTURED STREAM"]
    end

    H --> Q1["Is the tunnel agent connected?<br/>What timeouts am I running?<br/>Which device am I on?"]
    T --> Q2["What did this run decide,<br/>which models ran,<br/>what failed, how long did it take?"]
    L --> Q3["What was the raw exception?<br/>Which URL did the proxy try?<br/>Which checkpoint was resolved?"]

    Q1 --> Who1["the operator, before a demo"]
    Q2 --> Who2["the client, the frontend, and the operator"]
    Q3 --> Who3["the operator, server-side only"]
```

| Surface | Owner | Lifetime | Audience | Contract-bound? |
|---|---|---|---|---|
| `GET /api/health` | the orchestrator | a point-in-time snapshot | operator, and any liveness probe | yes — the route is documented |
| `ExecutionTrace` | the controller | one run, returned to the caller | the **client** and the operator | yes — `extra="forbid"` (`core/schemas.py:297`) |
| server-side logs | every layer | the process lifetime | the operator only | **no** — free text |

### 1.3 What this chapter does not claim

- It does **not** claim the system is observable. It claims three specific surfaces exist and
  enumerates exactly what each carries.
- It does **not** claim any end-to-end latency number as a system property. Timings that exist are
  **per-step, per-run** (`trace.timings`, §3.6) and are the durations of the steps that actually
  ran in that run — not a benchmark.
- It does **not** claim a health payload proves the *inference* works. `/api/health` never answers
  for the Codespace; that question belongs to `/api/capabilities`, and the code says so in the route
  docstring: *"Reports its own configuration; never answers for the Codespace (that is
  /api/capabilities)"* (`.workbuddy-ai/scratch/apply-test/main.py:678-679`).

### 1.4 Status of the observability surface

| Surface | Status | Evidence |
|---|---|---|
| `/api/health` liveness route | `IMPLEMENTED` · `VERIFIED` (probed live) | measured payload, §2.1 |
| `tunnel` block in the health payload | `VERIFIED` (present in the live payload) | §2.3 |
| tunnel counters `pending` / `completed` | `IMPLEMENTED` · `MEASURED` (monotonic, three readings) | §2.3 |
| `agent_connects` counter | `IMPLEMENTED` but **dead** — never incremented | §2.3 |
| three timeouts reported in the payload | `VERIFIED` (`150.0` / `120.0` / `90.0`) | §2.4 |
| `device` field | `IMPLEMENTED` · `VERIFIED` (`cpu`) with the F-8 normalisation | §2.5 |
| `has_github_token` | `VERIFIED` (`true`) — a **boolean**, never the value | §2.6 |
| `codespace_name` trailing `\n` | **`OPEN` (cosmetic)** — B-02 | §2.7 |
| per-run `ExecutionTrace` | `IMPLEMENTED` · `VERIFIED` (present in all 24 live runs) | §3 |
| `trace.contradiction` | `IMPLEMENTED`, and observed `false` in the live runs | §3.9 |
| structured / queryable logs | **absent** | §4.4 |
| metrics, APM, distributed tracing | **absent** | §6 |
| cost accounting | **absent** | §6 |

---

## 2. The health payload

### 2.1 The measured live payload

Probed live on 2026-09-25 against `https://<backend-host>/api/health`
(`release/repo/docs/DEPLOYMENT.md` §2; `release/CURRENT_RELEASE_STATE.md` §1):

```json
{"status":"ok","service":"satquery-orchestrator",
 "tunnel":{"agent_connected":true,"agent_id":"codespaces-fd1038","pending":0,"completed":97},
 "config":{"codespace_name":"potential-space-trout-r4ppw969w45j2pvvw\n","codespace_port":8000,
           "transport_mode":"auto","tunnel_timeout_s":150.0,"wake_timeout_s":120.0,
           "upstream_timeout_s":90.0,"device":"cpu","has_github_token":true}}
```

The exact command recorded elsewhere in the project's evidence register is:

```bash
curl --noproxy '*' https://<backend-host>/api/health
```

(`docs/FINAL_DELIVERY_REPORT.md` §4, cited in `release/repo/docs/architecture/02-deployment-topology.md` §7.1.)

The payload has exactly **three top-level keys** — `status`, `service`, `tunnel` — plus `config`.
There is no `version`, no `uptime`, no `build`, no `revision` field. An operator who wants to know
*which revision is serving* cannot get that answer from this route; the revision is known only from
the deploy record (`release/CURRENT_RELEASE_STATE.md` §1).

### 2.2 Field-by-field

| Field | Type | Meaning | Live value | Source |
|---|---|---|---|---|
| `status` | string | the hub's own liveness — a literal, not a probe | `"ok"` | `apply-test/main.py:682` |
| `service` | string | the service identity | `"satquery-orchestrator"` | `apply-test/main.py:683` |
| `tunnel.agent_connected` | bool | is a tunnel agent currently polling, within the TTL? | `true` | `tunnel.py:131` |
| `tunnel.agent_id` | string \| null | which agent identity holds the poll | `"codespaces-fd1038"` | `tunnel.py:132` |
| `tunnel.pending` | int | requests parked and not yet answered | `0` | `tunnel.py:134` |
| `tunnel.completed` | int | requests successfully resolved since the hub started | `97` | `tunnel.py:135` |
| `config.codespace_name` | string | the target Codespace | `"…pvvw\n"` — **carries a trailing `\n`** | `apply-test/main.py:686` |
| `config.codespace_port` | int | the inference port | `8000` | `apply-test/main.py:687` |
| `config.transport_mode` | string | transport selection | `"auto"` | `apply-test/main.py:692` |
| `config.tunnel_timeout_s` | float | tunnel park budget | `150.0` | `apply-test/main.py:693` |
| `config.wake_timeout_s` | float | wake poll budget | `120.0` | `apply-test/main.py:694` |
| `config.upstream_timeout_s` | float | proxy request timeout | `90.0` | `apply-test/main.py:695` |
| `config.device` | string | declared device (raw env value) | `"cpu"` | `apply-test/main.py:696` |
| `config.has_github_token` | bool | is a GitHub token configured? | `true` | `apply-test/main.py:688` |

`status` is a **literal `"ok"`**, not the result of a check:

```python
@app.get("/api/health")
async def health() -> dict[str, Any]:
    """Orchestrator liveness. Reports its own configuration; never answers
    for the Codespace (that is /api/capabilities)."""
    hub = get_hub()
    return {
        "status": "ok",
        "service": "satquery-orchestrator",
        "tunnel": hub.stats,
        ...
```

(`.workbuddy-ai/scratch/apply-test/main.py:676-684`)

This is worth naming precisely, because it is a design decision with a consequence. The route
answers `"ok"` whenever the **orchestrator process** is alive — which, on Render, is whenever the
service has not crashed. It does not answer `"degraded"` when the tunnel agent is gone, and it does
not answer `"error"` when the Codespace is stopped. An operator reading only `status` learns
nothing beyond "the web service responds". The information that matters is in `tunnel` and
`config`, and that is exactly where the reader should look.

> This is **not** a defect and is **not** to be reported as one. It is the documented split: the
> orchestrator reports *its own* state, and `/api/capabilities` is the route that answers for the
> Codespace (`apply-test/main.py:678-679`). The Render blueprint uses this route as
> `healthCheckPath` (`render.yaml:9`), which is precisely the "is the process up" question it
> answers.

### 2.3 The `tunnel` block

The `tunnel` block is `hub.stats` — the `stats` property of the process-wide `TunnelHub` singleton:

```python
@property
def stats(self) -> dict[str, Any]:
    return {
        "agent_connected": self.agent_connected(),
        "agent_id": self._agent_id,
        "agent_connects": self._agent_connected_count,
        "pending": len(self._pending),
        "completed": self._completed,
    }
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:128-136`)

The hub is a **single in-process object** with no persistence:

```python
_HUB: Optional[TunnelHub] = None


def get_hub() -> TunnelHub:
    """Process-wide singleton hub (one per Render instance)."""
    global _HUB
    if _HUB is None:
        _HUB = TunnelHub()
    return _HUB
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:233-241`)

Three consequences follow directly from "one in-process object, no persistence", and all three
matter operationally:

1. **The counters reset whenever the Render instance restarts.** A free-tier Render service sleeps
   when idle and restarts on wake. After a restart, `completed` is `0` again. The counter is a
   *lifetime-of-this-process* counter, not a *lifetime-of-the-deployment* counter.
2. **`pending` is the only field that reflects current load.** It is `len(self._pending)` — the
   number of parked requests. `0` means no client is currently waiting. It is **not** a queue depth
   in any persistent sense.
3. **`agent_connected` is time-windowed, not event-driven.** It is computed on read:

```python
def agent_connected(self) -> bool:
    """True if an agent has polled within the TTL window."""
    if self._last_poll_at == 0.0:
        return False
    return (time.monotonic() - self._last_poll_at) <= self.agent_ttl_s
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:113-117`)

with `DEFAULT_AGENT_TTL_S = 60.0` (`tunnel.py:63`). So an agent that stopped polling 61 seconds ago
reads `agent_connected: false` even though nothing crashed — the flag is a *freshness* statement,
not a *liveness* one. This is why §7.3's diagnostic ("is this a tunnel gap?") starts by re-reading
`/api/health` rather than trusting a single reading.

#### `completed` was measured at three different values

The tunnel counter is a **monotonic runtime counter**, not a fixed fact. Three measured readings
exist, each with its own provenance:

| Reading | Where recorded |
|---|---|
| `completed: 97` | `release/repo/docs/DEPLOYMENT.md` §2 (the live health probe) |
| `completed: 314` | `docs/FINAL_DELIVERY_TODO.md` §1.4 and §6 E-02; `docs/DEPLOYMENT_TOPOLOGY.md` §2 |
| `completed: 338` | `docs/FINAL_DELIVERY_REPORT.md` §3 P2 |

They are consistent with one another — the counter grows — and the honest statement is
**"`completed` was measured at 97, 314 and 338 at three different times on 2026-09-25."** Quoting
any one of them as *the* value would be wrong. Any of them could also be `0` right now, if the
Render instance has since slept.

#### What increments `completed` — and what does not

`completed` is incremented in exactly **one** place:

```python
def complete(self, req_id: str, status: int, body: Any) -> bool:
    """Resolve a parked request. Returns False if it is gone (timed out)."""
    req = self._pending.get(req_id)
    if req is None:
        return False
    if not req.future.done():
        req.future.set_result(TunnelResult(status=status, body=body))
    self._completed += 1
    return True
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:213-221`)

`fail()` — the sibling method the agent calls when it reports an error — does **not** touch the
counter:

```python
def fail(self, req_id: str, detail: str) -> bool:
    """Fail a parked request from the agent side (e.g. upstream error)."""
    req = self._pending.get(req_id)
    if req is None:
        return False
    if not req.future.done():
        req.future.set_exception(RuntimeError(detail))
    return True
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:223-230`)

So `completed` counts **successful deliveries only**, never attempts. A run in which every request
failed would leave `completed` unchanged. An operator must not read `completed` as "requests
handled"; it is "responses delivered back to a waiting client". A timed-out request is removed by
the `finally` clause of `submit()` (`tunnel.py:175-176`) and likewise never counted.

#### `agent_connects` is a dead counter

The `stats` property emits a fifth key, `agent_connects`, sourced from `self._agent_connected_count`.
That field is initialised to `0` and **never incremented anywhere in the file**:

```
105:        self._agent_connected_count: int = 0
133:            "agent_connects": self._agent_connected_count,
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:105,133` — the only two occurrences.)

`agent_connects` can therefore only ever read `0`. It is a counter that cannot move. This is
recorded here as an observation, not as a bug report: no code reads it, no document promises it, and
the **measured live payload does not even carry the key** (§2.9). It is a field that exists in the
source and never appears in the served body.

> **A `stats` note that matters for reading the live payload.** The live payload's `tunnel` block
> has **four** keys (`agent_connected`, `agent_id`, `pending`, `completed`). The `stats` property in
> the source copy has **five** (the extra being `agent_connects`). The two do not agree. Which one
> the currently-running process serves is
> `UNKNOWN — not established from the available evidence`; the divergence is recorded in §2.9
> alongside the `config` divergence.

### 2.4 The three timeouts

The payload reports three timeouts. They are the three budgets that decide **how long a client can
wait**, and they apply at different layers:

| Field | Live value | Layer | What it bounds | Source |
|---|---|---|---|---|
| `config.tunnel_timeout_s` | `150.0` | orchestrator, tunnel path | how long a parked request waits for an agent to pick it up and return | `tunnel.py:55`, `main.py:152-153` |
| `config.wake_timeout_s` | `120.0` | orchestrator, forward path | how long the wake loop polls `/v1/health` before giving up | `main.py:156-157`, `main.py:417-442` |
| `config.upstream_timeout_s` | `90.0` | orchestrator, HTTP client | the per-request timeout on the proxied call to the Codespace | `main.py:160-161`, `main.py:253-256` |

Each is an environment variable read at call time, not a constant:

```python
def _tunnel_timeout_s() -> float:
    return float(os.environ.get("SATQUERY_TUNNEL_TIMEOUT_S", "150"))


def _wake_timeout_s() -> float:
    return float(os.environ.get("SATQUERY_WAKE_TIMEOUT_S", "120"))


def _upstream_timeout_s() -> float:
    return float(os.environ.get("SATQUERY_UPSTREAM_TIMEOUT_S", "90"))
```

(`.workbuddy-ai/scratch/apply-test/main.py:152-161`)

The tunnel timeout's default is documented in the hub with its reasoning:

> *"How long a parked client request waits for an agent to pick it up before the hub gives up.
> Generous: a cold Codespace can take ~90s to boot, and the agent only starts polling once the
> model server is ready."* (`tunnel.py:52-55`)

#### Two further budgets that are *not* in the payload

An operator debugging latency will meet two more timeouts that `/api/health` does **not** report.
They are recorded here so they are not mistaken for missing configuration:

| Budget | Value | Layer | Source |
|---|---|---|---|
| `_WAKE_HEALTH_TIMEOUT_S` | `10.0` | orchestrator — the per-**poll** health timeout during a wake | `apply-test/main.py:107`; used at `:421` |
| `_WAKE_POLL_INTERVAL_S` | `2.0` | orchestrator — the sleep between wake polls | `apply-test/main.py:106`; used at `:437` |
| `DEFAULT_BUDGET_SECONDS` | `120.0` | **controller** — the run budget, checked *between* plan steps | `core/controller.py:121-122` |

The first two are fixed literals in the deployed source, not env vars — so a `config` block that
omitted them would be correct rather than incomplete. The third is the most interesting: the
controller's own run budget is a **fourth** timeout, enforced in a completely different layer, and
it appears in the trace rather than in the health payload (§3.8).

The controller states the honest limit of that budget in the code:

> *"Budget is checked BETWEEN steps (§5.2). A true per-step timeout needs a worker process or a
> signal handler, both of which conflict with the single-process monolith constraint more than they
> benefit. The honest position: v1 bounds total wall clock and records what it skipped."*
> (`core/controller.py:548-552`)

### 2.5 `device`

`config.device` in the health payload is the **raw environment value**:

```python
"device": os.environ.get("SATQUERY_DEVICE", ""),
```

(`.workbuddy-ai/scratch/apply-test/main.py:696`)

The live value is `"cpu"`. That is the *declared* value, unnormalised. The **served** device value —
the one the contract constrains — is a different field on a different route: `/v1/health`'s
`device`, which is normalised and validated by `_effective_device` (`app/deployment.py:1093-1197`).

The distinction matters because the two can disagree on a misconfigured deployment. `_effective_device`
implements F-8:

- it **casefolds and strips** before comparing, so `'CUDA'` and `'cuda'` cannot take different
  branches;
- it validates against the contract's closed set, `_LEGAL_DEVICES = frozenset({"cpu", "cuda", "mps"})`
  (`app/deployment.py:1204`);
- an unrecognised value returns **`None`**, not an echo and not a silent `"cpu"`.

The reasoning is in the code, and it is worth quoting because it is the pattern the whole codebase
uses for "the operator set something I do not recognise":

> *"`None` is already a legal value for the field (`"cpu" | "cuda" | "mps" | null`), so it travels
> the existing contract rather than inventing one; it is honest — 'the operator set something I do
> not recognise' is not 'this deployment uses the CPU'; defaulting to `"cpu"` would mean a typo
> silently changes which device the process is believed to use."* (`app/deployment.py:1156-1164`)

Two measured defects preceded that fix, both recorded in the same docstring:

| Defect | Symptom | Cause |
|---|---|---|
| A | `'cuda'` → `'cpu'` but `'CUDA'` → `'CUDA'` | the correction compared case-sensitively while `gpu_available` did not |
| B | `'garbage'` → `device: "garbage"` | the reader accepted any string |

(`app/deployment.py:1144-1152`)

An operator reading `config.device` from `/api/health` is reading the **raw** value and must not
expect it to be one of the four contract values. An operator reading `device` from `/v1/health` is
reading the validated value and may rely on the closed set. This is one of the two places in the
system where the same concept has two differently-normalised read sites; the other is the shared
file-size cap (`docs/architecture/08-api-contract.md` §9).

### 2.6 `has_github_token`

```python
"has_github_token": bool(os.environ.get("GITHUB_TOKEN")),
```

(`.workbuddy-ai/scratch/apply-test/main.py:688`)

The field is a **boolean**. It reports *whether* a token is configured, never the token, never its
length, never a prefix, never a hash. The live value is `true`. There is no path by which the token
value can reach this payload, and that is deliberate: the route is unauthenticated.

Operationally, `has_github_token: false` means the **wake path cannot work** — `_github_token()`
raises `OrchestratorConfigError` before any GitHub call (`apply-test/main.py:119-125`). A deployment
in that state can still serve through the tunnel (the tunnel needs no GitHub token — the agent
dials out), which is exactly why the two transports are separable. `has_github_token: false` is
therefore **not** a total-outage signal; it is a "the forward fallback is unavailable" signal.

### 2.7 `codespace_name` and the trailing newline — B-02, `OPEN` (cosmetic)

`config.codespace_name` reports `…pvvw\n`. This is **B-02**, and its status is `OPEN` **but
cosmetic**:

> *"`P2-T03` `/api/health` `codespace_name` trailing `\n` | DEFERRED (cosmetic) | Wake path is safe
> (`_codespace_name()` strips, `main.py:123,357`); only the health payload reports the raw value."*
> (`docs/FINAL_DELIVERY_REPORT.md` §6)

> *"`B-02` | `/api/health` reports `codespace_name` with a trailing `\n` | **Cosmetic** — reporting
> only; the wake path strips via `_codespace_name()` (`main.py:123,357`) | P2-T03 | none needed |
> DOWNGRADED"* (`docs/FINAL_DELIVERY_TODO.md` §5)

The mechanism is visible in the two call sites:

- `_codespace_name()` — the **wake path** — strips:

```python
def _codespace_name() -> str:
    name = os.environ.get("CODESPACE_NAME", "").strip()
    if not name:
        raise OrchestratorConfigError(
            "CODESPACE_NAME is not set; the orchestrator does not know which "
            "Codespace to target."
        )
    return name
```

(`.workbuddy-ai/scratch/apply-test/main.py:128-135`)

- the **health payload** — in the deployed revision — does not strip. The prepared patch changes
  exactly this, to `os.environ.get("CODESPACE_NAME", "").strip()`
  (`.workbuddy-ai/scratch/apply-test/main.py:686`).

The fix is known and one line — *"change line 619 to `_codespace_name()`, then Render redeploys"*
(`docs/FINAL_DELIVERY_TODO.md` §4, P2-T03) — and the row's own reasoning for deferring is that *"a
live-backend redeploy before the demo is not worth the risk."*

**The live payload still carrying the `\n` is itself the evidence that the B-07 patch is not
deployed.** `release/CURRENT_RELEASE_STATE.md` §1 states it exactly: *"Note `codespace_name` still
carries the trailing `\n` → the B-07 patch is **not** deployed."* A cosmetic field is therefore
doing double duty as the deployment-state witness — which is a small, real benefit of not having
"cleaned it up".

> **Do not upgrade this.** `B-02` is `OPEN`. It is not `RESOLVED`, and it is not `CLOSED`.

### 2.8 `transport_mode`

```python
def _transport_mode() -> str:
    """Which transport to use: ``tunnel``, ``forward``, or ``auto`` (default).

    ``auto`` prefers the tunnel and falls back to the forwarded-port proxy only
    when no agent is connected.
    """
    mode = os.environ.get("SATQUERY_TRANSPORT", "auto").strip().lower()
    return mode if mode in ("tunnel", "forward", "auto") else "auto"
```

(`.workbuddy-ai/scratch/apply-test/main.py:142-149`)

The live value is `"auto"`. The field is a **closed set** of three values, and an unrecognised
value is coerced to `"auto"` rather than echoed — the same principle as `_effective_device`, applied
one layer up.

`auto` is the mode that produces B-07, and the reason is in the dispatch function:

```python
mode = _transport_mode()
hub = get_hub()

use_tunnel = mode == "tunnel" or (mode == "auto" and hub.agent_connected())
```

(`.workbuddy-ai/scratch/apply-test/main.py:562-565`)

Under `auto`, the transport decision is made **once, at dispatch time**, from `hub.agent_connected()`.
If the agent was fresh at that instant, the request goes down the tunnel with a 150 s budget. If the
tunnel then fails to complete the request, `mode != "tunnel"`, so the code does **not** return an
error — it logs a warning and **falls through** to the forward path:

```python
except TunnelTimeout as exc:
    if mode == "tunnel":
        # ... returns upstream_timeout (504)
    _log.warning("tunnel timed out, falling back to forwarded port: %s", exc)
```

(`.workbuddy-ai/scratch/apply-test/main.py:593-610`)

That fallthrough then burns `wake_timeout_s = 120` on a 302 from GitHub's relay for a private repo.
The measured shape is **≈249 s ≈ 150 + 120** (`DELIVERY_REPORT_2026-09-25.md` §4). §7.3 and §7.4
treat this as the chapter's central operational fact.

> **Note the `mode == "tunnel"` guard.** It is present in the *patched* source. The **deployed**
> revision lacks the `ForwardUnavailable` early-exit, which is why the deployed system pays the full
> 249 s. The patch converts "504 after 249 s" into "503 early with an actionable code"
> (`DELIVERY_REPORT_2026-09-25.md` §4).

### 2.9 The monorepo copy, the scratch copy, and the deployed revision

Three copies of the orchestrator exist on disk, and they are **not** the same file. This is the
single most confusing thing about this subsystem, and it is stated here in one place.

| Copy | Path | Lines | Tunnel? | `codespace_name` | Is it the deployed source? |
|---|---|---|---|---|---|
| monorepo | `deploy/render/main.py` | **532** | **no** | raw | **no** — stale and untracked |
| scratch (patched) | `.workbuddy-ai/scratch/apply-test/main.py` | **835** | yes | `.strip()` | **no** — it is the deployed file **with the prepared patch applied** |
| deployed | `SatQuery-Backend/main.py` @ `89d80eaddec5` | **768–769** | yes | raw | **yes** — but not on this disk |

The monorepo copy declares a **simpler** `/api/health` with no `tunnel` block at all:

```python
@app.get("/api/health")
async def health() -> dict[str, Any]:
    """Orchestrator liveness. Reports its own configuration; never answers
    for the Codespace (that is /api/capabilities)."""
    return {
        "status": "ok",
        "service": "satquery-orchestrator",
        "config": {
            "codespace_name": os.environ.get("CODESPACE_NAME", ""),
            "codespace_port": _codespace_port(),
            "has_github_token": bool(os.environ.get("GITHUB_TOKEN")),
            "allowed_origins": _allowed_origins(),
            "production_origins": list(_PRODUCTION_ORIGINS),
            "dev_origins_enabled": _dev_origins_enabled(),
            "wake_timeout_s": _wake_timeout_s(),
            "upstream_timeout_s": _upstream_timeout_s(),
            "device": os.environ.get("SATQUERY_DEVICE", ""),
        },
    }
```

(`deploy/render/main.py:444-466`)

It is kept in the repository because it documents the **contract** of the route — the design
decision that `allowed_origins` reports the **effective** list, so an operator can confirm *from
outside* what the service will actually accept. The comment says the dev entries being visible
*"is how a production deployment proves it turned them off"* (`deploy/render/main.py:455-458`).

#### Why the scratch copy is believed to be "deployed + patch"

The prepared patch's own presence check pins three changes to three line numbers
(`docs/FINAL_DELIVERY_TODO.md` §6 E-12; `DELIVERY_REPORT_2026-09-25.md` §4):

| Change | Documented location | Location in the scratch copy |
|---|---|---|
| `forward_unavailable` | `main.py:326` | `.workbuddy-ai/scratch/apply-test/main.py:326` |
| `upstream_timeout` | `main.py:601` | `.workbuddy-ai/scratch/apply-test/main.py:601` |
| `codespace_name` `.strip()` | `main.py:686` | `.workbuddy-ai/scratch/apply-test/main.py:686` |

All three match exactly. That is strong evidence the scratch file is the deployed file with the
patch applied, and it is why this chapter uses it as the source for the tunnel's behaviour while
**labelling it as a scratch copy rather than the deployed revision**. Where the scratch copy and the
measured live payload disagree, the disagreement is recorded below rather than smoothed over.

#### The measured divergences

| Element | Live payload | Scratch copy | Note |
|---|---|---|---|
| `tunnel` keys | 4 (`agent_connected`, `agent_id`, `pending`, `completed`) | 5 (adds `agent_connects`) | `agent_connects` is dead anyway (§2.3) |
| `config` keys | 8 | 11 (adds `allowed_origins`, `production_origins`, `dev_origins_enabled`) | the live body omits the three origin fields |
| `codespace_name` | raw (`…pvvw\n`) | `.strip()` | expected — the patch is not deployed |
| `device` | raw env value | raw env value | agree |

The `config` divergence is the one that is genuinely unexplained. Two readings are possible — a
stale transcription, or an older deployed revision — and the evidence does not decide between them:

> `UNKNOWN — not established from the available evidence` (which of "the live payload was
> transcribed with three fields omitted" or "the live revision predates those three fields" is
> correct).

What *is* established is the **consequence**: an operator should not treat the absence of
`allowed_origins` from the live payload as proof that the deployment has no CORS allowlist. The
allowlist is enforced by `CORSMiddleware` at `create_app()` time regardless of what the payload
prints (`apply-test/main.py:667-673`), and the CORS rule is documented in
`docs/architecture/08-api-contract.md` §10.

### 2.10 The Codespace's own `/v1/health`

There is a **second** health route, one tier deeper, and it is a different shape. The orchestrator's
`/api/health` reports the *transport*; the Codespace's `/v1/health` reports the *deployment's
capabilities*.

```python
@api.get("/v1/health")
async def health() -> JSONResponse:
    """Liveness + capability states. Loads no model (requirement 4).

    The body comes from `app.deployment.health_payload()`, which is the
    single public health source per the 2026-09-22 ruling. It is therefore
    the same traversal that builds `/v1/capabilities`, so the two routes
    cannot disagree about which capabilities exist -- the property the
    STEP 7 chain tests assert.
    ...
    """
    from app.deployment import health_payload

    payload = health_payload()
    # Asserted rather than trusted: `HealthStatus` is `extra="forbid"`, so a
    # key added to the payload without a key added to the model would make
    # the response invalid against the project's own contract. Finding H-1
    # was exactly this failure in the other direction.
    from core.schemas import HealthStatus

    HealthStatus.model_validate(payload)
    return JSONResponse(payload)
```

(`app/space_app.py:521-547`)

The payload's field set is fixed by `HealthStatus`, which is `extra="forbid"`:

```python
class HealthStatus(BaseModel):
    model_config = ConfigDict(extra="forbid")

    status: Literal["ok", "degraded", "error"] = "ok"
    schema_version: str = SCHEMA_VERSION
    models: dict[str, str] = Field(default_factory=dict)
    device: str | None = None
    gpu_available: bool = False
```

(`core/schemas.py:430-437`)

The payload is built by `DeploymentReport.health()`:

```python
def health(self) -> dict[str, Any]:
    """The `GET /v1/health` payload. Exactly `HealthStatus`'s field set.
    ...
    """
    return {
        "status": self.status,
        "schema_version": _schema_version(),
        "models": self.models,
        "device": self.device,
        "gpu_available": self.gpu_available,
    }
```

(`app/deployment.py:774-788`)

#### `status` is derived, not asserted

Unlike the orchestrator's literal `"ok"`, the Codespace's `status` is computed from the capability
states, and the rule is worth quoting in full because it encodes a contract decision:

```python
@property
def status(self) -> str:
    """`HealthStatus.status`, derived rather than asserted.

    The rule the contract states (`API_CONTRACT.md` section 2.1):
    `degraded` = *"the service is up but at least one expected artifact is
    absent"*. Two things follow, and both matter:

      * a capability that is merely `not_requested` does **not** degrade the
        service -- under `lazy_load: true` that is every capability on a
        fresh process, and reporting `degraded` for a healthy idle service
        would make the field useless as a probe;
      * `unavailable` (present but broken) is a defect. It is reported as
        `error`, not `degraded`, because the two have different remedies and
        `API_CONTRACT.md` section 5.2 already renders `model_load_error` as
        *"Defect -- surface it"*.
    """
    states = {cap.state for cap in self.capabilities}
    if "unavailable" in states:
        return "error"
    if "absent" in states:
        return "degraded"
    return "ok"
```

(`app/deployment.py:703-725`)

Three states, three meanings, and they are not interchangeable:

| `status` | Trigger | What it means |
|---|---|---|
| `"ok"` | every capability is `loaded` or `not_requested` | nothing this build ships is missing |
| `"degraded"` | at least one capability is `absent` | the service is up but an expected artifact is not in this deployment |
| `"error"` | at least one capability is `unavailable` | something is **present but broken** — a defect, with a different remedy |

The `models` map is capability → **contract state**, from the adapter's translation table
(`app/deployment.py:698-701`), and the five contract states are the closed set
`{"loaded", "absent", "unavailable", "not_requested", "evicted"}` (`app/deployment.py:175-177`).
The adapter **never emits `evicted`** and says so rather than guessing, because eviction is a
statement about the running model cache and no filesystem inspection can observe it
(`app/deployment.py:70-73`). The full translation table is in
`docs/architecture/08-api-contract.md` §6.6.

#### `gpu_available` — a device probe, and a torch-free one

```python
@property
def gpu_available(self) -> bool:
    """Whether a CUDA device was *detected*, not whether one is allocated.
    ...
    """
    import os

    if os.environ.get("SATQUERY_DEVICE", "").strip().lower() == "cuda":
        return True
    visible = os.environ.get("CUDA_VISIBLE_DEVICES", "").strip()
    if visible and visible != "-1":
        return True
    # `nvidia-smi` is not probed: shelling out on a health route would be a
    # per-request subprocess, and a Space's GPU is invisible until ZeroGPU
    # allocates it, so a negative answer from here would be misleading.
    return False
```

(`app/deployment.py:727-766`)

On the live CPU deployment, `gpu_available: false` is the **expected and correct** answer, and the
frontend is instructed not to surface it as a fault (`app/deployment.py:731-734`).

The docstring records a **correction** that is itself an observability lesson:

> *"DELIBERATELY DOES NOT IMPORT TORCH, and that is a correction. The first version probed
> `torch.cuda.is_available()`, which meant the health payload pulled the entire torch stack on every
> request. That breaks requirement 1 of `docs/DEPLOYMENT_ARCHITECTURE.md` section 3.3 — 'Import
> cheaply and without torch' — and it made
> `test_the_metadata_path_does_not_import_torch` fail, which is how it was found."*
> (`app/deployment.py:736-744`)

A liveness probe that costs a full model-stack import is not a liveness probe. This is the same
principle as the orchestrator's decision to compute `agent_connected` on read rather than to keep a
heartbeat thread: **the cheap answer is the right answer on a health route.**

### 2.11 What health does NOT tell you

An operator who reads only the health routes will not learn any of the following. Each gap is
stated with where the answer actually lives.

| Question | Where the answer is |
|---|---|
| Which revision is deployed? | nowhere in a payload — only the deploy record (`release/CURRENT_RELEASE_STATE.md` §1) |
| Is a model currently resident? | nowhere — `lazy_load: true` means the process cannot know without constructing |
| How many requests have been served in total? | nowhere — `completed` is per-Render-process and counts only tunnel deliveries (§2.3) |
| What is the error rate? | nowhere — no counter exists (§6) |
| How long did the last run take? | in that run's `trace.timings` (§3.6), not in health |
| Is the Codespace warm? | partly — `tunnel.agent_connected` is a proxy, and only for the tunnel path |
| Is the GPU allocated? | nowhere — `gpu_available` is a *device presence* probe, not an allocation state |
| Is the token valid? | nowhere — `has_github_token` is a boolean, and validity is only learned by calling GitHub |

---

## 3. The per-run `ExecutionTrace`

### 3.1 Why the trace exists

The trace is the system's primary diagnostic artifact, and it exists because the *result* alone
cannot answer "why". A `SpecialistResult` says what was found; the `ExecutionTrace` says **which
route the system took to find it** — which task was chosen, which models ran, what failed, what was
skipped, and how long each step took.

Its defining constraint is stated in the module banner and repeated at the producer:

> *"Execution trace (observable facts only — never chain-of-thought)"* (`core/schemas.py:277`)

> *"Append one `TraceStep`. Observable facts only — no chain-of-thought."*
> (`core/controller.py:1194`)

This is a hard line, not a style preference. The trace is **returned to the client** — it is a field
of `ResultEnvelope`, which `POST /v1/analyze` returns verbatim — and v1 has no authentication. So
anything placed in the trace is public. The trace therefore carries *what happened*, never *what the
model was thinking*, and never a server-side path (§3.11).

### 3.2 The field table

`ExecutionTrace` is `extra="forbid"` (`core/schemas.py:297`), so this field set is the contract.

| Field | Type | Populated by | Meaning |
|---|---|---|---|
| `run_id` | str | default `_new_id("run")`, or `request.run_id` | the run identifier (§5.1) |
| `schema_version` | str | `SCHEMA_VERSION` = `"1.0"` | the contract version |
| `task` | `Task \| null` | `prediction.intent.task`, else `request.force_task` | the task actually chosen |
| `query` | str \| null | `request.query` | the natural-language question |
| `inputs` | list[str] | `[_asset_label(a) for a in request.assets]` | **basenames** of the inputs (F-13) |
| `modalities` | list[`Modality`] | `self._modalities(resolved)` | the resolved modalities |
| `intent` | `Intent \| null` | `prediction.intent` | the router's reading, or null |
| `validation` | dict | default `{}` | reserved; not populated on the serving path |
| `workflow` | list[str] | `[step.step_id for step in plan.steps]` | the planned step ids |
| `steps` | list[`TraceStep`] | `self._record(...)` | the step records (§3.3) |
| `selected_models` | list[`ModelRef`] | `self._selected_models(outcomes)` | the models that ran (§3.5) |
| `parameters` | dict | `plan.to_trace()` + `budget_seconds` + `registry` | the plan's parameters and the registry snapshot |
| `outputs` | list[str] | `[e.evidence_id for e in result.evidence]` | the evidence ids produced |
| `confidence` | `ConfidenceBreakdown \| null` | `result.confidence` | the confidence breakdown |
| `timings` | dict[str, float] | `trace.timings[step.step_id] = ...` | per-step durations (§3.6) |
| `fallbacks` | list[str] | `plan.notes`, `_verify`, and others | what degraded (§3.7) |
| `errors` | list[dict] | `_execute`, `_verify` | what failed (§3.8) |
| `contradiction` | bool | `_detect_contradiction` | whether disjoint spatial claims were found (§3.9) |
| `config_hash` | str \| null | `self.config.hash` | the frozen config hash (§3.10) |
| `started_at` | str | `_utcnow()` default | ISO-8601 UTC |
| `finished_at` | str \| null | `trace.finished_at = self._now()` | ISO-8601 UTC |

The trace is assembled **once**, in `AnalysisController.run()`:

```python
started = time.perf_counter()
run_id = request.run_id or self._new_run_id()
trace = ExecutionTrace(
    run_id=run_id,
    query=request.query,
    ...
    inputs=[_asset_label(a) for a in request.assets],
    config_hash=self.config.hash if self.config is not None else None,
)
self._record(trace, ControllerState.RECEIVE, {"asset_count": len(request.assets)})
```

(`core/controller.py:258-279`)

and it is attached to the result at the very end:

```python
result.execution_trace = trace
trace.outputs = [e.evidence_id for e in result.evidence]
trace.confidence = result.confidence
self._record(trace, ControllerState.RESPOND, {"answer_length": len(result.answer)})
trace.finished_at = self._now()

return ResultEnvelope(run_id=run_id, result=result, trace=trace)
```

(`core/controller.py:388-394`)

Note that `trace.confidence` is **the same object** as `result.confidence` — not a copy, not a
recomputation. There is one confidence computation per run, and the trace references it. This is why
the two can never disagree (`docs/architecture/06-evidence-and-confidence.md`).

### 3.3 `TraceStep`

```python
class TraceStep(BaseModel):
    model_config = ConfigDict(extra="forbid")

    state: ControllerState
    started_at: str = Field(default_factory=_utcnow)
    duration_ms: float | None = None
    detail: dict[str, Any] = Field(default_factory=dict)
```

(`core/schemas.py:279-285`)

A step is created by exactly one method:

```python
def _record(
    self,
    trace: ExecutionTrace,
    state: ControllerState,
    detail: dict[str, Any] | None = None,
) -> None:
    """Append one `TraceStep`. Observable facts only — no chain-of-thought."""
    trace.steps.append(
        TraceStep(state=state, detail=self._jsonable(detail or {}))
    )
```

(`core/controller.py:1188-1197`)

`_jsonable` makes the detail dict serialisable *without dropping information*
(`core/controller.py:1199-1202`) — the honest-encoding rule applied to the trace.

Two properties of `TraceStep` are worth naming:

- **`duration_ms` is `None` on every step `_record` creates.** `_record` does not measure. The
  per-step durations live in `trace.timings`, keyed by `step_id`, and come from the *execution*
  path (§3.6). A reader must not expect `steps[i].duration_ms` to be populated.
- **`detail` is free-form.** Each call site chooses its keys. The keys observed in the code are
  enumerated in §3.4.

### 3.4 The nine-state spine

`ControllerState` is a nine-value enum:

```python
class ControllerState(str, Enum):
    RECEIVE = "RECEIVE"
    PARSE = "PARSE"
    VALIDATE = "VALIDATE"
    PLAN = "PLAN"
    PREPROCESS = "PREPROCESS"
    EXECUTE = "EXECUTE"
    AGGREGATE = "AGGREGATE"
    VERIFY = "VERIFY"
    RESPOND = "RESPOND"
```

(`core/schemas.py:79-88`)

The controller records against a subset of these. Reading `run()` end to end
(`core/controller.py:258-394`), the states that actually produce a `TraceStep` are:

| State | Recorded when | `detail` keys observed in the code |
|---|---|---|
| `RECEIVE` | always | `asset_count` |
| `PARSE` | always | `inputs`, `modalities`, `query_length` |
| `VALIDATE` | always | `assets`, `force_task`, `router` |
| `PLAN` | always | `plan` |
| `EXECUTE` | once per plan step | the `StepOutcome.to_trace()` dict (§3.6) |
| `AGGREGATE` | always | `evidence` (a summary), `degraded` |
| `VERIFY` | always | `contradiction` |
| `RESPOND` | always | `answer_length` |

`PREPROCESS` is **not** recorded by the controller. The state exists in the enum and in the frontend's
trace spine, but `run()` never calls `_record(..., ControllerState.PREPROCESS, ...)`. This is the
source of the trace bar's 94.4444 % fill (§3.12 and
`docs/architecture/09-frontend.md` §6) — the frontend's spine has nine states and one of them is
never reached on the serving path.

The refusal path records a different subset: `_finish_refusal` returns early after `PLAN`, so a
refused query's trace has no `EXECUTE`, `AGGREGATE`, `VERIFY` or `RESPOND` step
(`core/controller.py:344-345`).

### 3.5 `ModelRef` and `selected_models`

```python
class ModelRef(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str
    revision: str | None = None
    role: str | None = None
```

(`core/schemas.py:288-293`)

`selected_models` is built from each specialist that ran, deduplicated on `name:role`:

```python
def _selected_models(self, outcomes: Iterable[StepOutcome]) -> list[ModelRef]:
    """Model identities for the trace, from each specialist that ran.

    Built as `ModelRef` rather than raw dicts: `ExecutionTrace.selected_models`
    is typed `list[ModelRef]`, and pydantic would coerce a dict only by
    emitting a serialization warning. Constructing the type explicitly keeps
    the contract honest instead of relying on coercion.
    """
    refs: list[ModelRef] = []
    seen: set[str] = set()
    for outcome in outcomes:
        specialist = (
            outcome.registry_entry.specialist if outcome.registry_entry else None
        )
        if specialist is None:
            continue
        for ref in specialist.model_refs():
            key = f"{ref.get('name')}:{ref.get('role')}"
            if key in seen:
                continue
            seen.add(key)
            refs.append(
                ModelRef(
                    name=str(ref.get("name", "unknown")),
                    revision=ref.get("revision"),
                    role=ref.get("role"),
                )
            )
    return refs
```

(`core/controller.py:721-749`)

Three facts follow, all of which matter when reading a trace:

1. **`selected_models` is populated from the *specialist that ran*, not from the registry.** A
   capability that was planned but could not be constructed contributes nothing, because
   `registry_entry.specialist` is `None` (`core/controller.py:612-618`).
2. **Deduplication is on `name:role`,** so the same model appearing under two roles appears twice.
3. **`revision` may be `None`.** The field is optional in both `ModelRef` and the source dicts.

The `registry` snapshot in `trace.parameters["registry"]` is **re-snapshotted after execution**, and
the reason is a measured defect:

> *"F-19 (owner ruling 2026-09-23): re-snapshot the registry AFTER execution.
> `registry.describe()` reports the registry's MEMOISED entries, so the snapshot taken in PLAN above
> reports the PREVIOUS request's builds -- while `trace.selected_models`, populated inside
> `_execute`, reports THIS one. That made a single trace carry two clocks: on a cold process the
> first request's trace said `registry.built == {}` while its own run had built `optical_sar`.
> Measured in pass 13 with two identical payloads on one controller: request #1 body
> `built == []`, request #2 body `built == ['optical_sar']`."* (`core/controller.py:350-359`)

So `trace.parameters["registry"]` and `trace.selected_models` are now consistent with each other by
construction. A reader can use either, and they will agree.

### 3.6 `timings`

Per-step durations are written in the execution loop:

```python
outcome = self._execute_one(step, assets, request, trace)
outcomes.append(outcome)
self._record(trace, ControllerState.EXECUTE, outcome.to_trace())
trace.timings[step.step_id] = round(outcome.duration_ms, 3)
```

(`core/controller.py:573-576`)

`duration_ms` is measured with `time.perf_counter()` around the construct-validate-execute sequence
(`core/controller.py:609-708`), so a step's timing **includes model construction** — which is
exactly what an operator wants, because on a cold process construction dominates. It is rounded to
**three decimal places** (`round(..., 3)`) at both the `timings` write and in
`StepOutcome.to_trace()` (`core/controller.py:161`).

The key is `step.step_id`, and the `StepOutcome.to_trace()` dict recorded into `steps` carries a
matching `step_id`, so the two can be joined:

```python
def to_trace(self) -> dict[str, Any]:
    return {
        "step_id": self.step.step_id,
        "capability": self.step.capability,
        "ok": self.ok,
        "skipped_reason": self.skipped_reason,
        "error_code": self.error.code if self.error else None,
        "duration_ms": round(self.duration_ms, 3),
        "registry_state": (
            self.registry_entry.state.value if self.registry_entry else None
        ),
    }
```

(`core/controller.py:154-165`)

> **What `timings` is not.** It is not a benchmark and it is not a system-level latency claim. It
> is the wall-clock duration of the steps that ran in *that one run*, on *that one host*. There is
> no aggregated latency statistic anywhere in the system, and no end-to-end benchmark exists
> (`DOCS_STYLE_GUIDE.md` §3).

### 3.7 `fallbacks`

`fallbacks` is a list of human-readable strings naming what degraded. It is appended from more than
one place, and the two observed writers are:

**From the plan's notes**, in the aggregate path:

```python
trace.fallbacks.extend(plan.notes)
```

(`core/controller.py:778`)

**From `_verify`**, when a successful specialist left no evidence:

```python
for outcome in successful:
    if outcome.result is not None and not outcome.result.evidence:
        trace.fallbacks.append(
            f"{outcome.capability} produced a result with no evidence items"
        )
```

(`core/controller.py:1143-1147`)

There is a third writer recorded in the source at `core/controller.py:1145` context and one at
`:1145`/`:1145`… the list is open-ended: any code holding the trace may append. The honest statement
is that `fallbacks` is a **free-text list of degradation notes**, not a closed vocabulary, and a
consumer must not parse it.

The distinction between `fallbacks` and `errors` is the distinction between *"something ran
differently than the ideal"* and *"something failed"*. A fallback is a degraded-but-successful
outcome; an error is a failure. Both appear in the same trace, and neither implies the other.

### 3.8 `errors`

`errors` is a list of dicts, appended from two places.

**From the execution loop, for a step that failed:**

```python
if outcome.error is not None:
    # F-15 (owner ruling 2026-09-23): the trace is CLIENT-FACING, so
    # it carries the operator-safe message only. It previously
    # preferred `error.detail` -- the technical text, which can name
    # filesystem paths and library internals -- over `user_message`.
    # The technical detail stays available on the `StepOutcome`
    # server-side, which is where a maintainer reads it.
    trace.errors.append(
        {
            "step_id": step.step_id,
            "capability": step.capability,
            "code": outcome.error.code,
            "message": outcome.error.user_message
            or "The step failed.",
            "recoverable": outcome.error.recoverable,
        }
    )
```

(`core/controller.py:578-594`)

**From the execution loop, for a step skipped on budget:**

```python
trace.errors.append(
    {
        "step_id": step.step_id,
        "code": SpecialistTimeoutError.code,
        "message": f"skipped: run budget of "
        f"{self.budget_seconds}s exceeded",
    }
)
```

(`core/controller.py:560-567`)

**From `_verify`, when a specialist produced no evidence:**

```python
trace.errors.append(
    {
        "step_id": "*",
        "code": "evidence_loss",
        "message": f"specialists produced no evidence: {sorted(missing)}",
    }
)
```

(`core/controller.py:1135-1141`)

Three properties of the `errors` list:

1. **The keys are not uniform.** A budget-skip entry has no `capability` and no `recoverable`; an
   `evidence_loss` entry uses `step_id: "*"`. A consumer must tolerate missing keys.
2. **`message` is the operator-safe `user_message`, never `detail`.** This is F-15, and the
   reasoning is quoted in the code above: `detail` can name paths and library internals, and the
   trace is client-facing.
3. **The message is a *classification*, not the exception text.** For an unhandled failure, the
   client sees the fixed sentence *"The step failed with an unhandled error. Internal detail is
   withheld; see the server-side diagnostics."* (`core/controller.py:689-692`), while the raw
   exception goes to the log (§4.3).

`errors` being non-empty does **not** imply the run failed. A run can have a failed step and still
return a result — *"Run each step, recording outcomes. Never aborts on failure."*
(`core/controller.py:543`). The failure semantics live in the result, not in the trace.

### 3.9 `contradiction`

`contradiction` is a single boolean, set by a detector that records and never resolves:

```python
@staticmethod
def _detect_contradiction(
    trace: ExecutionTrace,
    result: SpecialistResult,
    outcomes: Sequence[StepOutcome],
) -> None:
    """Detect and record; never resolve (design §7.6).

    Two steps naming disjoint places in the same coordinate system are a
    contradiction. Both claims are kept; only the flag and a warning are
    added. Cross-system comparison is skipped because IoU across coordinate
    systems is meaningless.
    """
    claims: dict[str, list[Box]] = {}
    for outcome in outcomes:
        if not outcome.ok or outcome.result is None:
            continue
        for box in outcome.result.boxes:
            claims.setdefault(box.coordinate_system.value, []).append(box)

    for system, boxes in claims.items():
        if len(boxes) < 2:
            continue
        for i in range(len(boxes)):
            for j in range(i + 1, len(boxes)):
                if _iou(boxes[i], boxes[j]) < CONTRADICTION_IOU_FLOOR:
                    trace.contradiction = True
                    message = (
                        "specialists produced contradictory spatial claims "
                        f"in {system}; both are retained in evidence"
                    )
                    if message not in result.warnings:
                        result.warnings.append(message)
                    return
```

(`core/controller.py:1151-1184`)

The threshold is frozen and named:

```python
CONTRADICTION_IOU_FLOOR: float = 0.1
```

(`core/controller.py:119`)

Two observability consequences:

- **A `true` contradiction is visible in two places at once** — `trace.contradiction` and a string
  in `result.warnings` — and the detector deliberately keeps both claims rather than picking a
  winner. The rationale is at `core/controller.py:57-63`: *"Selecting a winner requires a precedence
  weight"*, which the system does not have.
- **The comparison is scoped per coordinate system.** A box in one CRS is never compared to a box in
  another, because *"IoU across coordinate systems is meaningless"*. So `contradiction: false` does
  **not** mean "no two claims disagree" — it means "no two claims *in the same coordinate system*
  were disjoint". A reader must not over-read the flag.

In the live validation runs, `contradiction` was `false` throughout — no run produced two disjoint
in-system spatial claims. That is the expected outcome for the eight single- and pair-task cases
(`DELIVERY_REPORT_2026-09-25.md` §3).

### 3.10 `config_hash`

```python
config_hash=self.config.hash if self.config is not None else None,
```

(`core/controller.py:277`)

On the serving path `self.config` is always set (`app/serving.py:259-265`), so `config_hash` is
always populated. Its value is the **frozen config hash `78f1e3700da15aa1`** — a sha256 over the
whole config registry, and an invariant the project treats as frozen
(`DOCS_STYLE_GUIDE.md` §3; `app/serving.py:95`; `deploy/codespace/README.md:48-52`).

The field is `str | None`, and `None` is reachable only on a controller built without a config —
which the serving composition root does not do. Its observability value is that a run's trace
**self-identifies the config that produced it**, which is what makes a stored trace re-interpretable
later. Editing `configs/base.yaml` or `configs/deploy.yaml` moves the hash, so a trace carrying a
different hash is provably from a different configuration.

### 3.11 What the trace deliberately does not contain

The trace is client-facing, and two rulings removed content from it. Both are worth recording
because they are *observability design decisions* — the system chose to be less informative to the
client in order to be safe.

**F-13 — no server-side paths in `inputs`:**

> *"F-13. `inputs` is a CLIENT-VISIBLE field -- `ResultEnvelope` is returned verbatim by
> `POST /v1/analyze`, `trace` included -- and it was echoing `request.assets` after the caller had
> turned asset *handles* into filesystem *paths*. The response therefore named the server-side
> location of every uploaded byte, which is the exact disclosure `AssetHandle.to_response()` refuses
> to make."* (`core/controller.py:263-268`)

The measured disclosure is recorded with the exact bytes:

> *"Measured 2026-09-22 end-to-end through the real Space: the client sent the handle
> `asset_d243f7f85d8c2f3c02981f0af9737f01` and received back
> `C:\Users\anish\sq_scratch\...\assets\asset_d243...7f01.tif`."* (`core/controller.py:292-295`)

**F-14 — the same disclosure again, in `steps[].detail`:**

> *"F-14. The F-13 disclosure again, one record later. `trace.steps[]` is part of the same
> client-visible `ExecutionTrace` ... and this PARSE detail was writing `list(request.assets)` --
> which by now are the PATHS the Space derived from the client's handles -- so the response named
> the server-side location of every uploaded byte in a second place."* (`core/controller.py:287-292`)

Both are now reduced to a **basename** by one shared helper, `_asset_label`, which the code
describes as *"the ONE place the basename rule lives"* (`core/controller.py:296-299`). A reader of a
trace therefore sees `asset_d243f7f85d8c2f3c02981f0af9737f01` (the handle) or a bare filename —
never a directory.

**F-15 / F-20 — no raw exception text:**

The trace's `errors[].message` is `user_message`, and the raw exception is logged server-side only
(§4.3). F-20's own account of why is the clearest statement of the split in the repository:

> *"F-20 (owner ruling 2026-09-23) — F-15 WAS ONLY HALF IMPLEMENTED. The paragraph above was true
> of the trace and false of everything else: `_warnings` and `_failure_evidence` publish
> `scrub_paths(error.detail)`, and `detail` was `f"unhandled {type(exc).__name__}: {exc}"` -- so ONE
> unhandled failure reached the client as 'Internal detail is withheld' in
> `trace.errors[].message` and as 'unhandled RuntimeError: kaboom' in `result.warnings[]` and
> `evidence[].payload["message"]`. The response contradicted itself about what it had decided to
> disclose."* (`core/controller.py:650-658`)

The repair was made **at the producer, not at each carrier** — a single `detail` that is safe to
publish makes all three carriers agree by construction (`core/controller.py:660-664`).

So the trace is not a debugging dump. It is a **curated, publishable record** of observable facts,
and the curating is done at the source.

### 3.12 The trace is client-facing — the consequence

Every field in §3.2 travels to the browser. That single fact explains most of the design:

| Consequence | Where it is enforced |
|---|---|
| No chain-of-thought | `core/schemas.py:277`; `core/controller.py:1194` |
| No server-side paths | F-13/F-14, `core/controller.py:276,304` |
| No raw exception text | F-15/F-20, `core/controller.py:585-594,660-664` |
| Errors carry a `recoverable` flag the client can act on | `core/controller.py:592` |
| A degraded run still returns a trace | `core/controller.py:543` |

And it is why the trace is **also** the frontend's animation source. The Analyze console's trace bar
is driven by the eight execution events, and the bar's measured fill is **94.4444 %** — the
arithmetic being `((8 + 0.5) / 9) * 100` over the nine-state spine, with `PREPROCESS` the state the
serving path never reaches (`docs/architecture/09-frontend.md` §6; `frontend/assets/js/mission.js:422-424`).
That figure is an **artifact metric about the frontend's own progress indicator**, not a
system-level claim, and it must never be quoted as one.

---

## 4. What is and is not logged

### 4.1 The loggers

Five loggers exist across the request path. Their names are the diagnostic namespaces an operator
will see:

| Logger name | Declared at | Layer |
|---|---|---|
| `satquery.orchestrator` | `deploy/render/main.py:74`; `apply-test/main.py:103` | the Render orchestrator |
| `satquery.space` | `app/space_app.py:463` | the Codespace FastAPI app |
| `satquery.tunnel` | `.workbuddy-ai/scratch/apply-test/tunnel.py:50` | the tunnel hub |
| `__name__` of `gateway.app` | `gateway/app.py:123` | the in-process gateway (`gateway.app`) |
| `__name__` of `core.controller` | `core/controller.py:129` | the analysis controller (`core.controller`) |
| `__name__` of `app.serving` | `app/serving.py:66` | the composition root (`app.serving`) |

No logger is configured with a level, a formatter or a handler anywhere in the code read. The
effective level and destination are therefore whatever the host process sets — uvicorn's defaults on
the Codespace, Render's defaults on the orchestrator:

> `UNKNOWN — not established from the available evidence` (the effective log level and the log
> destination on each host; no logging configuration was found in the sources read).

### 4.2 Every log call site

This is the complete set of log calls found in the sources read. There are **six**.

| # | File:line | Level | Logger | Message | Carries |
|---|---|---|---|---|---|
| 1 | `core/controller.py:676` | `error` | `core.controller` | `"unhandled failure in step %s (capability=%s): %s: %s"` + `exc_info` | step_id, capability, exception **type name**, exception **message**, traceback |
| 2 | `app/space_app.py:510` | `exception` | `satquery.space` | `"unhandled failure on %s %s"` | method, path, traceback |
| 3 | `app/space_app.py:690` | `warning` | `satquery.space` | `"invalid_request on POST /v1/analyze: %s: %s"` | exception type name, full `ValidationError` text |
| 4 | `gateway/app.py:565` | `error` | `gateway.app` | `"upstream transport failure for request_id=%s: %s: %s"` + `exc_info` | request_id, exception type, exception text, traceback |
| 5 | `deploy/render/main.py:384` | `warning` | `satquery.orchestrator` | `"upstream connection error to %s: %s"` | URL, exception |
| 6 | `deploy/render/main.py:393` | `warning` | `satquery.orchestrator` | `"upstream request error to %s: %s"` | URL, exception |
| 7 | `deploy/render/main.py:405` | `error` | `satquery.orchestrator` | `"non-JSON upstream response from %s (status %s)"` | URL, status |
| 8 | `app/serving.py:203` | `info` | `app.serving` | `"optical_sar: CROMA checkpoint resolved via %s (%s)"` | source, `present`/`absent` |
| 9 | `apply-test/main.py:610` | `warning` | `satquery.orchestrator` | `"tunnel timed out, falling back to forwarded port: %s"` | exception |
| 10 | `apply-test/main.py:623` | `warning` | `satquery.orchestrator` | `"tunnel error, falling back to forwarded port: %s"` | exception |

(Calls 5–7 are the monorepo copy; the same three exist in the scratch copy with the tunnel ones
added. `gateway/assets.py` and `gateway/policy.py` contain **no** log calls at all — verified by
grep.)

Two of these are the whole reason the operator-facing diagnostic story works:

**Call 1 — the unhandled specialist failure.** This is F-20's entire point. The client receives a
sanitized message; the operator receives the exception *and its traceback*:

```python
_log.error(
    "unhandled failure in step %s (capability=%s): %s: %s",
    step.step_id,
    step.capability,
    type(exc).__name__,
    exc,
    exc_info=exc,
)
```

(`core/controller.py:676-683`)

The docstring above it states the rule: *"Both are preserved for the operator: `context` is never
serialized (`StepOutcome.to_trace()` publishes `error_code` only), and the traceback goes to the
log."* (`core/controller.py:673-675`).

**Call 2 — the unwrapped failure.** The F-12b handler logs the request and then answers with the
contract envelope:

```python
_log.exception(
    "unhandled failure on %s %s", request.method, request.url.path
)
status, body = translate_error(
    "satquery_error",
    "An internal error occurred.",
    detail="",
    recoverable=False,
)
return JSONResponse(status_code=status, content=body)
```

(`app/space_app.py:510-519`)

The handler's docstring is the clearest statement of the disclosure split in the codebase:

> *"**F-15 is enforced here as well as in the handlers.** The client is told the code and a fixed,
> operator-safe message; the exception's own text is recorded SERVER-SIDE only. That split is the
> ruling: 'sanitize all client-facing exception messages; retain full exception details only in
> server-side diagnostics'. A traceback in `detail` would disclose internal paths and library
> versions to an unauthenticated caller."* (`app/space_app.py:503-508`)

### 4.3 The client/log split

One rule governs every one of the six-plus call sites, and it is worth stating as a table because it
is the single most important thing to understand about this system's diagnostics.

| Carrier | Audience | Content |
|---|---|---|
| `trace.errors[].message` | the client | `user_message` — a fixed, operator-safe sentence |
| `trace.errors[].code` | the client | the machine code (a classification) |
| `trace.errors[].recoverable` | the client | a boolean |
| `result.warnings[]` | the client | `scrub_paths(error.detail)` — path-scrubbed |
| `evidence[].payload["message"]` | the client | `scrub_paths(error.detail)` — path-scrubbed |
| **the log** | **the operator** | the raw exception, its type name, its text, and its traceback |

The path-scrubbing is done by a single function:

```python
def scrub_paths(text: str | None) -> str | None:
```

(`core/errors.py:39`)

which the F-20 account identifies as the thing that made the *published* carriers safe while the
*unpublished* `detail` stayed unsafe (`core/controller.py:652-654`).

The consequence for an operator is direct: **the client's error message is never enough to debug
with, and the log is the only place the real cause lives.** An operator who has only a client-side
report has a code and a classification; an operator with log access has the exception.

### 4.4 What is not logged

The following are **not** logged anywhere in the sources read, and their absence is deliberate or
at least consequential:

| Not logged | Consequence |
|---|---|
| request bodies | a malformed `POST /v1/analyze` is logged as a `ValidationError` *summary* (call 3), not as the body |
| uploaded file bytes | never logged; the upload path has no log calls at all |
| client IP addresses | never logged; no access log exists |
| token values | **never** — `has_github_token` is a boolean and no call site prints a token |
| per-request access lines | no access logging was found in any layer |
| successful analysis outcomes | a successful run produces **no** log line at all |
| health-check hits | `/api/health` logs nothing |
| the tunnel agent's polls | the hub logs nothing on a poll; the agent's own logging lives in the private Inference repo |
| the run's `run_id` | **no log call site includes a run id** |

That last row is the most operationally significant. A client that reports a failure with its
`run_id` gives the operator an identifier that **cannot be looked up in the logs**, because no log
line carries one. The `run_id` correlates the *client's* record with the *trace* the client already
has; it does not correlate anything server-side. This is a real observability gap and is recorded as
such in §6.

> **The one partial exception.** Call 4 (the gateway's transport failure) logs a `request_id` — the
> `req_<hex>` id from `gateway/policy.py` (§5.2). That is the closest thing to a correlation id in
> the logs, and it is a *gateway* id, not a *run* id. The two are different namespaces (§5.4).

### 4.5 Where the logs go

Not established. No `logging.basicConfig`, no `dictConfig`, no handler, no formatter and no log
level was found in any source read. The two hosts differ:

- **the Codespace** runs `serve.py` under uvicorn, with `setsid nohup ... > "$SERVE_LOG" 2>&1`
  redirecting stdout/stderr to `/tmp/satquery-serve.log` (`deploy/codespace/launch.sh:63,133`), and
  the tunnel agent's output to `/tmp/satquery-tunnel.log` (`launch.sh:64,163`). Those files are on
  the **ephemeral Codespace filesystem** and vanish with it.
- **Render** captures the service's stdout/stderr in its own log view. There is no in-code
  configuration and no log shipping.

So: `UNKNOWN — not established from the available evidence` for the effective level; and for
retention, the Codespace's logs are demonstrably **ephemeral** (`launch.sh:44-47` states the
Codespace filesystem is ephemeral) while Render's retention is a platform property not observable
from the code.

---

## 5. Identifiers

The system generates identifiers with three distinct conventions, in three layers. They do not
interoperate, and knowing which is which prevents a wasted search.

### 5.1 `run_<12 hex>`

The run id is generated by the controller, and by the schema default when a run id is not supplied:

```python
@staticmethod
def _new_run_id() -> str:
    import uuid

    return f"run_{uuid.uuid4().hex[:12]}"
```

(`core/controller.py:1210-1214`)

```python
def _new_id(prefix: str) -> str:
    return f"{prefix}_{uuid.uuid4().hex[:12]}"
```

(`core/schemas.py:28-29`)

The controller prefers a **caller-supplied** id:

```python
run_id = request.run_id or self._new_run_id()
```

(`core/controller.py:259`)

So `AnalysisRequest.run_id` — an optional field (`core/schemas.py:418`) — lets a caller choose the
id, and it is used verbatim if present. The format is **`run_` followed by twelve lowercase hex
characters**. Real measured examples, all from the 2026-09-25 live validation
(`DELIVERY_REPORT_2026-09-25.md` §3):

```
run_0843db184e32   run_5b766f2d7df7   run_ea590b6fd70f   run_65a4b2f9d912
run_efe24b98d217   run_6375b80dcb8e   run_2a07dcdbae96   run_9134f40a258c
```

Twenty-four such ids exist across three passes, and **no run id is shared between passes**
(`release/CURRENT_RELEASE_STATE.md` §5) — which is itself the evidence that each pass was a genuine
independent execution rather than a replay.

The same id is propagated to each specialist:

```python
specialist_request = SpecialistRequest(
    assets=list(assets),
    query=request.query,
    params=dict(step.params),
    run_id=trace.run_id,
)
```

(`core/controller.py:621-626`)

so a run id is the one identifier that is consistent across the controller and every specialist
invocation within a run.

### 5.2 `req_<24 hex>`

The gateway generates its own correlation id, and it is a **deterministic function of a seed**:

```python
def new_request_id(seed: str | None = None) -> str:
    """Generate a correlation id.

    Format `req_<hex>`. When `seed` is given the id is a deterministic function
    of it, which lets tests assert on ids without patching a clock or an RNG --
    the same technique the rest of this repository uses for reproducibility.

    An inbound `X-Request-Id` is NOT trusted verbatim: see
    :meth:`GatewayPolicy.accept_request_id`.
    """
    if seed is None:
        seed = f"{time.time_ns()}"
    digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:24]
    return f"req_{digest}"
```

(`gateway/policy.py:181-194`)

Three properties:

1. **Format `req_` + 24 hex characters** — the first 24 hex of a sha256, so 96 bits.
2. **Deterministic when seeded**, which is what makes it testable without patching a clock or an RNG.
3. **An inbound `X-Request-Id` is not trusted verbatim** — `accept_request_id` validates it against
   `_REQUEST_ID_RE = re.compile(r"^[A-Za-z0-9_\-]{8,64}$")` (`gateway/policy.py:178`), so a caller
   cannot inject an arbitrary string into the logs.

This id is what appears in log call 4 (`gateway/app.py:565-571`) and in the error envelope the
gateway returns (`gateway/app.py:577`, `request_id=decision.request_id`).

### 5.3 The other prefixes

`_new_id` is used with five prefixes across `core/schemas.py`:

| Prefix | Field | Source |
|---|---|---|
| `asset_` | `AssetMetadata.asset_id` | `core/schemas.py:156` |
| `region_` | `Region.region_id` | `core/schemas.py:194` |
| `chg_` | `ChangeRegion.region_id` | `core/schemas.py:205` |
| `ev_` | `Evidence.evidence_id` | `core/schemas.py:219` |
| `run_` | `ExecutionTrace.run_id` | `core/schemas.py:299` |

Note that `region_` and `chg_` are **different prefixes for the same conceptual thing** — a change
region uses `chg_`, a general region uses `region_`. A consumer that assumes all regions are
`region_*` will miss the change regions. The `Evidence` validator requires unique `evidence_id`
values within a result (`core/schemas.py:345-351`), which is the one place an identifier collision
is actively refused.

The tunnel hub uses a **different** convention again — a bare `uuid4().hex` with no prefix:

```python
req = PendingRequest(
    id=uuid.uuid4().hex,
    ...
)
```

(`.workbuddy-ai/scratch/apply-test/tunnel.py:155-162`)

So a tunnel request id is 32 hex characters with no prefix — neither `run_` nor `req_`.

### 5.4 The two id spaces do not join

This is the observability gap that follows from the identifier conventions, and it deserves its own
subsection.

```mermaid
flowchart LR
    B["browser"] -->|"POST /api/infer"| R["Render orchestrator"]
    R -->|"hub.submit<br/>id = uuid4().hex"| T["TunnelHub"]
    T -->|"long-poll"| A["Codespace agent"]
    A -->|"POST /v1/analyze"| S["Codespace app"]
    S -->|"AnalysisController.run()"| C["controller"]
    C -->|"run_id = run_&lt;12hex&gt;"| E["ResultEnvelope"]

    R -.->|"logs request_id<br/>= req_&lt;24hex&gt;"| L["logs"]
    C -.->|"no log line carries run_id"| L
```

| Id | Format | Owner | Appears in a log? | Appears in the response? |
|---|---|---|---|---|
| `req_<24hex>` | sha256 prefix | the gateway | **yes** (call 4) | yes, in gateway error envelopes |
| tunnel `uuid4().hex` | bare 32 hex | the tunnel hub | **no** | no |
| `run_<12hex>` | uuid4 prefix | the controller | **no** | **yes** — in the envelope and the trace |

The three do not reference each other anywhere in the code. A client that reports a failure with
its `run_id` cannot be matched to a gateway `request_id` or a tunnel id from the logs. The only way
to correlate a client report with server-side diagnostics is by **timestamp and query text**.

> This is recorded as a **gap**, not as a defect with a workaround. No correlation id is propagated
> across the tier boundary, and no document claims otherwise.

---

## 6. What is not observed

This section is the honest inventory. Everything in it is a **capability the system does not have**,
and none of it is aspirational — the list exists so that a reader does not assume a monitoring
facility that was never built.

| Capability | Present? | Evidence |
|---|---|---|
| **APM** (application performance monitoring) | **no** | no APM client, SDK or agent in any source read |
| **Distributed tracing** | **no** | no trace-context propagation; the three id namespaces do not join (§5.4) |
| **Cost accounting** | **no** | `GPU_DURATIONS` declares ZeroGPU durations (`app/space_app.py:109-116`) but nothing meters or reports consumption |
| **Metrics endpoint** (Prometheus / OpenMetrics) | **no** | no `/metrics` route in any app factory |
| **Per-model latency histogram** | **no** | `trace.timings` is per-run, per-step, and is not aggregated anywhere |
| **Error-rate counter** | **no** | no counter exists; `trace.errors` is per-run only |
| **Request counter** | **no** | the tunnel's `completed` counts only tunnel deliveries, per Render process (§2.3) |
| **Uptime / restart tracking** | **no** | no uptime field; the hub's counters reset on restart |
| **Alerting** | **no** | no alerting rule, webhook or threshold anywhere |
| **Structured / JSON logs** | **no** | all log calls use `%s`-style free text |
| **Log shipping / aggregation** | **no** | logs are per-host; the Codespace's are on an ephemeral filesystem (§4.5) |
| **Dashboards** | **no** | none exists |
| **SLO / SLA definition** | **no** | none exists |
| **A system-level end-to-end benchmark** | **no** | `DOCS_STYLE_GUIDE.md` §3: *"does not exist; no system-level accuracy is claimed"* |

### 6.1 Cost accounting, specifically

Cost is the one absence that has a code artifact, which makes it worth a subsection rather than a
table row.

`GPU_DURATIONS` declares a per-task ZeroGPU duration, transcribed from the frozen config:

```python
#: ZeroGPU duration per task, from `configs/deploy.yaml` -- the FROZEN values,
#: not new guesses. `change_vqa` has no key of its own and reuses `change`,
#: because adding a key would move `Config.hash` off `78f1e3700da15aa1`
#: (`docs/DEPLOYMENT_ARCHITECTURE.md` section 3.4).
GPU_DURATIONS: dict[str, int] = {
    "vqa": 20,
    "caption": 20,
    "grounding": 45,
    "change": 30,
    "optical_sar": 45,
    "change_vqa": 30,
}
```

(`app/space_app.py:105-116`)

These values are used **only** to decorate a handler with a ZeroGPU reservation:

```python
def decorate_gpu(task: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Return the ZeroGPU decoration for a task, or an identity decorator.
    ...
    """
    if task not in GPU_DURATIONS:
        raise KeyError(...)
    duration = GPU_DURATIONS[task]
    spaces = _spaces_module()
    if spaces is None or not hasattr(spaces, "GPU"):
        def _identity(fn: Callable[..., Any]) -> Callable[..., Any]:
            return fn

        return _identity
    return spaces.GPU(duration=duration)
```

(`app/space_app.py:143-166`)

Nothing reads `GPU_DURATIONS` back out to compute, record or report consumption. There is no
per-run GPU-second field, no cumulative counter, and no budget-exhaustion signal. On the live
**CPU** deployment the decoration is a no-op anyway — `_spaces_module()` returns `None` when the
`spaces` package is absent, so `decorate_gpu` returns the identity decorator
(`app/space_app.py:160-164`).

The declared durations are therefore **frozen paperwork on the live path**, exactly like
`configs/deploy.yaml` itself (`release/repo/docs/DEPLOYMENT.md` §9). They are an input to a
mechanism that does not run in the deployed configuration, and no cost is observed.

> **Do not present `GPU_DURATIONS` as a cost model.** It is a declaration of intended reservation,
> and on the CPU deployment it reserves nothing.

---

## 7. Operational runbook

This section is the part of the chapter an operator uses. Every step below is grounded in a file.

### 7.1 Warm the stack before a demo

The stack has three things that can be cold, and all three are warmed differently.

| What is cold | How it warms | How long |
|---|---|---|
| the Render orchestrator | the first request to any `/api/*` route | Render free tier: a sleep/wake cycle |
| the Codespace | `ensure_codespace_up()` starts it and polls | bounded by `wake_timeout_s = 120` |
| the HF model cache | `warm_cache.py`, or the first model-touching request | a download per model |

**Step 1 — confirm the tunnel agent is connected.** This is the check that matters most, because
the tunnel is the live transport and the forwarded-port path is dead for a private repo
(`release/CURRENT_RELEASE_STATE.md` §2: *"The forwarded-port path is **dead** (302 for a private
repo); the tunnel is the live transport."*).

```bash
curl --noproxy '*' https://<backend-host>/api/health
```

Read `tunnel.agent_connected`. `true` means an agent has polled within the last 60 seconds
(`tunnel.py:63,113-117`). `false` means **no agent has polled recently** — and under `transport_mode:
auto`, a request will then take the forward path, which for a private repo will fail after burning
the wake timeout.

**Step 2 — if `agent_connected` is false, start the Codespace.** The agent is launched by the
devcontainer's `postStartCommand`, which runs `launch.sh`:

```json
"postStartCommand": "bash deploy/codespace/launch.sh"
```

(`.devcontainer/devcontainer.json:18`)

`launch.sh` is defensive by design, and its own header explains why:

> *"``setsid`` alone is NOT enough in Codespaces. The lifecycle shell that runs postStartCommand can
> still reap the process group, which showed up in production as 'the agent announced once, then
> vanished' — the hub then reported agent_connected=false and /api/infer fell back to the dead
> forwarded-port path (401 -> wake_timeout)."* (`deploy/codespace/launch.sh:15-19`)

The script therefore uses `setsid + nohup + </dev/null` plus a **supervising wrapper** that restarts
the agent if it exits (`launch.sh:20-23,160-168`), and then **verifies** the agent came up:

```bash
sleep 4

if ! pgrep -f "deploy/codespace/tunnel_agent.py" > /dev/null 2>&1; then
  echo "WARNING: the tunnel agent is not running. Last log lines:" >&2
  ...
else
  echo "tunnel agent process is up (pid $(pgrep -f 'deploy/codespace/tunnel_agent.py' | head -1))"
  if grep -q "announced to hub" "$TUNNEL_LOG" 2>/dev/null; then
    echo "tunnel agent announced to the hub successfully"
  ...
```

(`deploy/codespace/launch.sh:171-191`)

The verification exists because of a specific failure:

> *"Backgrounding with all output discarded means a crashing agent is completely invisible — that is
> exactly how a missing `httpx` hid itself."* (`launch.sh:174-176`)

**Step 3 — warm the HF cache, if the Codespace was rebuilt.** `warm_cache.py` pre-downloads the four
pinned models:

```bash
python deploy/codespace/warm_cache.py
```

It reports per-model `OK` / `SKIPPED` / `FAILED`, never aborts on a single miss, and **always exits
0** so a cache miss cannot fail a build (`deploy/codespace/warm_cache.py:8-10,110-112`). The four
models and their pinned revisions are transcribed verbatim from `configs/base.yaml`
(`warm_cache.py:12-16,35-60`):

| key | repo | revision | file |
|---|---|---|---|
| `vlm` | `HuggingFaceTB/SmolVLM-500M-Instruct` | `a7da5b986cb5` | (snapshot) |
| `grounding` | `chendelong/RemoteCLIP` | `bf1d8a3ccf2d` | `RemoteCLIP-ViT-B-32.pt` |
| `router` | `sentence-transformers/all-MiniLM-L6-v2` | `1110a243fdf4` | (snapshot) |
| `croma` | `antofuller/CROMA` | `0dd28e3d633b` | `CROMA_base.pt` |

> *"Running this before the first request means the initial `/v1/analyze` does not pay a cold
> download. It is idempotent and safe to re-run."* (`warm_cache.py:3-6`)

**Step 4 — run one throwaway analysis.** A single cheap query confirms the whole chain end to end.
Do this **before** the demo, not during it.

### 7.2 The cold-start shape

The cold start is **blocking and documented**, not hidden. The orchestrator's module docstring
states it plainly:

> *"If the Codespace is stopped, this service starts it via the GitHub Codespaces REST API, polls
> its ``/v1/health`` until it answers 200, then proxies the request. The client simply **waits**
> (blocking, wake-then-proxy) and receives the result; the frontend independently shows 'Waking
> inference engine...' on slow responses. We optionally tag the response with
> ``X-SatQuery-State: waking`` so the frontend can confirm the delay was a cold start."*
> (`.workbuddy-ai/scratch/apply-test/main.py:13-20`)

The wake loop is:

```python
deadline = time.monotonic() + _wake_timeout_s()
last_err: str | None = None
while time.monotonic() < deadline:
    try:
        async with httpx.AsyncClient(timeout=_WAKE_HEALTH_TIMEOUT_S) as client:
            health = await client.get(f"{base}/v1/health")
        if health.status_code == 200:
            return base, woke
        if _anonymous_access_blocked(health.status_code):
            location = health.headers.get("location", "")
            raise ForwardUnavailable(...)
        last_err = f"health status {health.status_code}"
    except httpx.HTTPError as exc:
        last_err = str(exc)
    await asyncio.sleep(_WAKE_POLL_INTERVAL_S)

raise WakeTimeout(
    f"Timed out after {_wake_timeout_s():.0f}s waiting for /v1/health. "
    f"Last probe error: {last_err or 'unknown'}."
)
```

(`.workbuddy-ai/scratch/apply-test/main.py:417-442`)

The shape an operator should expect:

| Phase | What happens | Bound |
|---|---|---|
| 0 | the client sends `POST /api/infer` and **waits** | — |
| 1 | `GET` the Codespace via the GitHub API; `POST .../start` if not `available` | one API round trip |
| 2 | poll `GET {base}/v1/health` every 2 s, each with a 10 s timeout | ≤ `wake_timeout_s = 120` |
| 3 | on the first `200`, proxy the original request | ≤ `upstream_timeout_s = 90` |
| 4 | the response carries `X-SatQuery-State: waking` | — |

`_anonymous_access_blocked` is the one branch that stops the loop early:

```python
def _anonymous_access_blocked(status_code: int) -> bool:
    """Whether a ``/v1/health`` status means the forwarded port is *not*
    anonymously reachable, as opposed to merely not ready yet.
    ...
    """
    return status_code in (401, 403) or 300 <= status_code < 400
```

(`.workbuddy-ai/scratch/apply-test/main.py:360-371`)

A redirect, `401` or `403` **can never become a 200 by waiting**, so the loop raises
`ForwardUnavailable` immediately rather than burning the timeout. A connection error, `404` or `5xx`
can still be a cold start, so those keep polling.

> **Note this branch is in the patched source.** The deployed revision lacks it, which is why the
> deployed system pays the full timeout instead of failing fast (§7.4).

**The frontend's side of the cold start.** The frontend shows *"Waking inference engine…"* while
Render starts the Codespace (`release/repo/docs/DEPLOYMENT.md` §5). The cold start is
**tens of seconds** and is documented rather than papered over (`DEPLOYMENT.md` §5;
`release/repo/docs/LIMITATIONS.md:52`).

> **Do not quote a single cold-start number.** The documented statement is *"tens of seconds"*
> (`DEPLOYMENT.md` §5), and the bounded worst case is `wake_timeout_s = 120` before a `504`. No
> measured cold-start distribution exists:
> `UNKNOWN — not established from the available evidence`.

### 7.3 Distinguishing a tunnel gap from a real failure

This is the runbook's most useful procedure, and it is grounded in a measured timing.

**The signature.** A request that hangs for **≈249 seconds** and then returns **`504`** is the
tunnel-gap signature, not a broken model. The arithmetic is exact:

```
tunnel_timeout_s (150) + wake_timeout_s (120) = 270 s  (nominal)
measured                                            ≈ 249 s
```

> *"B-07 root shape: in `auto` mode a tunnel timeout **falls through** to the forward path
> (`main.py:546`), burning `wake_timeout_s = 120` on a `302` (~249 s ≈ 150 + 120)."*
> (`DELIVERY_REPORT_2026-09-25.md` §4; `release/repo/docs/DEPLOYMENT.md` §6)

**The decision tree.**

```mermaid
flowchart TD
    A["a request hung, or returned 5xx"] --> B{"re-read /api/health<br/>tunnel.agent_connected?"}
    B -->|"true"| C{"was the wait ≈249 s?"}
    B -->|"false"| D["TUNNEL GAP<br/>the agent is not polling.<br/>Start the Codespace (§7.1)."]
    C -->|"yes, 504"| E["TUNNEL GAP<br/>agent went stale mid-request,<br/>or the Codespace stopped.<br/>B-07. Mitigate operationally."]
    C -->|"no"| F{"what was the code?"}
    F -->|"invalid_request 422"| G["a CLIENT bug — the body<br/>did not match AnalysisRequest"]
    F -->|"model_load_error / model_unavailable"| H["an ARTIFACT defect —<br/>surface it, do not retry blindly"]
    F -->|"upstream_timeout 504"| I["tunnel healthy but slow —<br/>the agent did not complete in 150 s"]
    F -->|"other"| J["read the code and<br/>trace.errors[] — §3.8"]
```

**The three signals to read, in order:**

1. **`tunnel.agent_connected`** on a **fresh** `/api/health`. If `false`, it is a tunnel gap and the
   remedy is §7.1 step 2. Do not trust a single reading — the flag is a 60-second freshness window
   (§2.3), so re-read.
2. **The elapsed time.** ≈249 s is the B-07 signature. A fast failure is something else.
3. **The machine code.** The codes are disjoint and each implies a different action:

| Code | Status | Meaning | Action |
|---|---|---|---|
| `tunnel_offline` | 503 | no agent is connected and `mode == "tunnel"` | start the Codespace |
| `forward_unavailable` | 503 | the forwarded port is not anonymously reachable | start the tunnel agent (patch only) |
| `wake_timeout` | 504 | the wake loop exhausted `wake_timeout_s` | the Codespace is not coming up |
| `upstream_timeout` | 504 | the tunnel was healthy but no agent completed in `tunnel_timeout_s` | retry; check the agent (patch only) |
| `upstream_unreachable` | 502 | a connection error to the Codespace | check the transport |
| `invalid_request` | 422 | the body was not a valid `AnalysisRequest` | fix the client |
| `model_unavailable` | 502 | the gateway could not reach the analysis service | retry |

(`.workbuddy-ai/scratch/apply-test/main.py:308-332,570-622`; the full taxonomy is in
`docs/architecture/08-api-contract.md` §12.)

The two codes marked "patch only" — `forward_unavailable` and `upstream_timeout` — **do not exist in
the deployed revision** (§7.4). An operator on the deployed system will not see them; they will see
`wake_timeout` after ≈249 s instead.

### 7.4 The B-07 operational note

**B-07 is `OPEN`.** The patch is prepared and **not deployed**. This is the single most important
operational fact in this chapter.

> *"B-07 | Transient tunnel-agent gaps → a request can hang or return 504. Patch prepared, **NOT
> deployed**. | **OPEN**"* (`release/repo/docs/DEPLOYMENT.md` §6)

**What the patch does** (`.workbuddy-ai/scratch/apply-test/main.py`):

| Change | Code | Effect |
|---|---|---|
| A | `forward_unavailable` (`503`, `recoverable: true`) on a **terminal** 302/401/403 | converts a 504-after-249 s into a 503-early with an actionable code |
| B | `upstream_timeout` (`504`) for "tunnel healthy but slow" | distinguishes a slow agent from a dead forward path |
| C | `/api/health` `codespace_name` `.strip()` | fixes B-02 |

**The patch's own honesty note, recorded because the honesty matters:**

> *"The report records that an earlier claim that the patch 'would not have prevented' the observed
> 504 'was wrong and was retracted'. The corrected position: 'Change A is genuinely **on the
> failing path** — it converts a 504-after-249 s into a 503-early with an actionable code.'"*
> (`DELIVERY_REPORT_2026-09-25.md` §4, quoted in `docs/architecture/02-deployment-topology.md` §6.4)

**Why it is not deployed:**

> *"the patch is not needed for the demo and touches the live backend. The residual is better
> mitigated operationally (keep the Codespace warm, raise the idle timeout)."*
> (`DELIVERY_REPORT_2026-09-25.md` §4)

**The operational mitigation, therefore, is:**

1. Keep the Codespace **warm** before and during any demo (§7.1 step 1).
2. **Raise the Codespace idle timeout** so it does not stop mid-session.
3. Re-read `/api/health` before a run, and treat `agent_connected: false` as "warm the stack now".
4. Expect a ≈249 s hang followed by a `504` if the agent goes stale mid-request — and **retry**,
   because the residual is transient.

> **A retracted claim, recorded.** An earlier statement that the patch *"would not have prevented"*
> the observed 504 was **wrong and was retracted**. Change A is on the failing path. Do not repeat
> the retracted version.

> **Do not upgrade B-07.** It is `OPEN`. It is not `RESOLVED`, and the patch is not deployed — the
> live payload's `codespace_name` trailing `\n` is the witness (§2.7).

### 7.5 Platform traps that bite operators

These are recorded in `release/repo/docs/DEPLOYMENT.md` §7 and each cost real debugging time.

| Trap | Consequence | Where recorded |
|---|---|---|
| **Cloudflare `_headers` rules CONCATENATE, they do not override** | a later rule cannot "fix" an earlier one; Chromium takes the **first** `max-age` | `DEPLOYMENT.md` §7 |
| **Cloudflare 308-redirects `X.html` → `/X`** | reference the extensionless path | `DEPLOYMENT.md` §7 |
| **A forwarded Codespace port returns `302` for a private repo** | this is *why* the tunnel exists | `DEPLOYMENT.md` §7 |
| **The tunnel agent must be started by the devcontainer `postStartCommand`** | a restarted Codespace comes up with `agent_connected: false` | `DEPLOYMENT.md` §7 |
| **Never retry `POST /api/infer` at the gateway** | a retry consumes inference twice | `DEPLOYMENT.md` §7 |
| **The monorepo `deploy/` is stale and untracked** | it is **not** the deployed source | `DEPLOYMENT.md` §1; `release/CURRENT_RELEASE_STATE.md` §6 |
| **The local `SatQuery-Backend` copy is stale** | always fetch the deployed `main.py` first | `DELIVERY_REPORT_2026-09-25.md` §4 |
| **The Codespace filesystem is ephemeral** | uploaded assets and logs vanish with the Codespace | `launch.sh:44-47` |
| **A stale serve process is worse than no process** | it answers `/v1/health` from OLD code | `launch.sh:111-113` |

The last one has its own mitigation in `launch.sh` — a **stamp** recording the revision and the
asset configuration, so a running server that does not match the checkout is restarted:

```bash
_current_stamp() {
  printf 'rev=%s asset_enabled=%s asset_dir=%s\n' \
    "$(git rev-parse HEAD 2>/dev/null || echo nogit)" \
    "${SATQUERY_ASSET_ENABLED:-}" \
    "${SATQUERY_ASSET_DIR:-}"
}
```

(`deploy/codespace/launch.sh:103-108`)

> *"A stale serve process is worse than no process: it answers /v1/health and /v1/capabilities from
> OLD code, so the deployment looks alive while reporting the previous revision's capabilities."*
> (`launch.sh:111-113`)

This is the closest thing in the system to a **deployment-identity check**, and it is local to the
Codespace — it is not exposed on any HTTP route. An operator on the orchestrator side cannot see it.

### 7.6 The diagnostic decision tree

One consolidated tree, for the case where something is wrong and the operator has only the
client-side report.

```mermaid
flowchart TD
    A["something is wrong"] --> B["GET /api/health"]
    B --> C{"tunnel.agent_connected?"}
    C -->|"false"| D["warm the stack<br/>§7.1 steps 1-2"]
    C -->|"true"| E{"what did the client see?"}

    E -->|"nothing, request hung"| F{"waited ≈249 s?"}
    F -->|"yes"| G["B-07 tunnel gap<br/>retry + warm<br/>§7.4"]
    F -->|"no"| H["still waiting —<br/>within budget"]

    E -->|"a 5xx"| I["read the error code<br/>§7.3 table"]
    E -->|"a 4xx"| J["a client bug —<br/>the body did not match the contract"]

    I --> K{"code present in the<br/>DEPLOYED revision?"}
    K -->|"no"| L["the patch is not deployed<br/>§7.4"]
    K -->|"yes"| M["act on the code"]

    E -->|"a result, but wrong"| N["read trace:<br/>intent, task, selected_models,<br/>errors, fallbacks<br/>§3"]
```

The last branch is the one that uses the trace rather than health: a **wrong but successful** answer
is diagnosed from the `ExecutionTrace`, by reading `trace.intent` (what the router thought),
`trace.task` (what was dispatched), `trace.selected_models` (what ran), and `trace.errors` /
`trace.fallbacks` (what degraded). That is the whole purpose of the trace, and it is the only
diagnostic surface for a wrong answer — no log line records a successful run (§4.4).

---

## 8. Status, gaps, and evidence

### 8.1 `NOT RUN` / `OPEN` / `BLOCKED` / `DEFERRED` for this topic

| # | Item | Status |
|---|---|---|
| 1 | B-07 — tunnel gaps; patch prepared, **not deployed** | **`OPEN`** |
| 2 | B-02 — `/api/health` `codespace_name` trailing `\n` | **`OPEN` (cosmetic)** |
| 3 | Effective log level and log destination on each host | **`UNKNOWN`** — no logging configuration found (§4.5) |
| 4 | Whether the live payload's `config` block omits three origin fields, or the deployed revision predates them | **`UNKNOWN`** (§2.9) |
| 5 | Whether the live `tunnel` block carries `agent_connects` | **`UNKNOWN`** (§2.3, §2.9) |
| 6 | A measured cold-start distribution | **`NOT RUN`** — only "tens of seconds" is documented (§7.2) |
| 7 | A system-level end-to-end benchmark | **`NOT RUN`** — none exists |
| 8 | Log retention on Render | **`UNKNOWN`** — a platform property, not observable from the code |
| 9 | Any APM / metrics / distributed tracing | **not implemented** — see §6 |
| 10 | Cost accounting | **not implemented** — `GPU_DURATIONS` is declared, not metered (§6.1) |
| 11 | `agent_connects` counter | **`IMPLEMENTED` but dead** — never incremented (§2.3) |
| 12 | Correlation of a client `run_id` to a server log line | **not implemented** — no log call site carries a run id (§5.4) |

### 8.2 The `UNKNOWN` list, in one place

Four things in this chapter are genuinely not established from the available evidence, and each is
written here so it is not silently filled in later:

1. **The effective log level and destination on each host.**
   `UNKNOWN — not established from the available evidence` (§4.5).
2. **Which `config` field set the live process serves.** The measured payload and the source copy
   disagree by three fields, and the evidence does not decide between a stale transcription and an
   older revision. `UNKNOWN — not established from the available evidence` (§2.9).
3. **Whether the live `tunnel` block carries `agent_connects`.** The measured payload has four keys;
   the source `stats` has five. `UNKNOWN — not established from the available evidence` (§2.3).
4. **The cold-start duration as a measured distribution.** Only the phrase *"tens of seconds"* is
   documented, and the bounded worst case is `wake_timeout_s = 120`.
   `UNKNOWN — not established from the available evidence` for any distribution (§7.2).

### 8.3 Evidence table

| Evidence | Location | Establishes |
|---|---|---|
| the measured live health payload | `release/CURRENT_RELEASE_STATE.md` §1; `release/repo/docs/DEPLOYMENT.md` §2 | §2.1, §2.2 |
| the three `completed` readings | `DEPLOYMENT.md` §2; `docs/FINAL_DELIVERY_TODO.md` §1.4/§6 E-02; `docs/FINAL_DELIVERY_REPORT.md` §3 P2 | §2.3 |
| B-02's status and fix | `docs/FINAL_DELIVERY_REPORT.md` §6; `docs/FINAL_DELIVERY_TODO.md` §4 P2-T03, §5 | §2.7 |
| the B-07 root shape and ≈249 s | `DELIVERY_REPORT_2026-09-25.md` §4 | §2.8, §7.3, §7.4 |
| the patch's three presence checks | `docs/FINAL_DELIVERY_TODO.md` §6 E-12; `DELIVERY_REPORT_2026-09-25.md` §4 | §2.9 |
| `TunnelHub` and `stats` | `.workbuddy-ai/scratch/apply-test/tunnel.py:113-136,213-230` | §2.3 |
| the deployed orchestrator (patched) | `.workbuddy-ai/scratch/apply-test/main.py` | §2.4, §2.8, §7.2, §7.3 |
| the monorepo orchestrator | `deploy/render/main.py:444-466` | §2.9 |
| `/v1/health` and `HealthStatus` | `app/space_app.py:521-547`; `core/schemas.py:430-437` | §2.10 |
| `status` derivation, `gpu_available` | `app/deployment.py:703-766` | §2.10 |
| `_effective_device` and F-8 | `app/deployment.py:1093-1204` | §2.5 |
| `ExecutionTrace`, `TraceStep`, `ModelRef` | `core/schemas.py:279-319` | §3.2, §3.3, §3.5 |
| the trace's producer | `core/controller.py:258-394,535-749,1120-1214` | §3.4, §3.6, §3.7, §3.8, §3.9 |
| F-13/F-14/F-15/F-19/F-20 | `core/controller.py:263-308,350-365,578-702` | §3.11, §4.2, §4.3 |
| `scrub_paths` | `core/errors.py:39` | §4.3 |
| every log call site | §4.2 table, with `file:line` | §4.2 |
| `run_<12hex>` | `core/controller.py:1210-1214`; `core/schemas.py:28-29` | §5.1 |
| `req_<24hex>` | `gateway/policy.py:181-194` | §5.2 |
| `GPU_DURATIONS` and `decorate_gpu` | `app/space_app.py:105-116,143-166` | §6.1 |
| the Codespace start procedure | `deploy/codespace/launch.sh`; `.devcontainer/devcontainer.json:18` | §7.1, §7.5 |
| the warm-up contract | `deploy/codespace/warm_cache.py` | §7.1 |
| the platform traps | `release/repo/docs/DEPLOYMENT.md` §7 | §7.5 |
| the 24 live runs and their ids | `DELIVERY_REPORT_2026-09-25.md` §3 | §5.1, §3.9 |

### 8.4 Cross-references

| For | See |
|---|---|
| the four endpoints, the envelopes, the error taxonomy | [08 — The API Contract](08-api-contract.md) |
| the four-tier topology, the tunnel, the wake flow, `transport_mode` | [02 — Deployment Topology](02-deployment-topology.md) |
| the request lifecycle and the nine-state spine in motion | [03 — Request Lifecycle](03-request-lifecycle.md) |
| confidence, evidence, and the `ConfidenceBreakdown` the trace carries | [06 — Evidence and Confidence](06-evidence-and-confidence.md) |
| the frozen config and `Config.hash == 78f1e3700da15aa1` | [07 — Configuration and Freeze](07-configuration-freeze.md) |
| the frontend's trace bar and the 94.4444 % fill | [09 — Frontend](09-frontend.md) §6 |
| the live deployment, env vars, and the cold-start statement | [../DEPLOYMENT.md](../DEPLOYMENT.md) |
| the project's blocker register and evidence index | `docs/FINAL_DELIVERY_TODO.md` §5, §6 |

---

> **Chapter summary.** SatQuery AI observes exactly three things: whether the transport is up
> (`/api/health`, §2), what one run did (`ExecutionTrace`, §3), and what failed inside the server
> (the logs, §4). It observes nothing else — no metrics, no tracing, no cost, no trend (§6). The
> health payload is a live-measured snapshot whose most important field is `tunnel.agent_connected`,
> whose most consequential absence is any correlation id, and whose most load-bearing quirk is a
> trailing newline that doubles as the witness that the B-07 patch is not deployed. B-07 is `OPEN`;
> B-02 is `OPEN` and cosmetic; four things are genuinely
> `UNKNOWN — not established from the available evidence`.