harn-stdlib 0.10.28

Embedded Harn standard library source catalog
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
import {
  agent_monologue_actuation_config,
  agent_monologue_actuation_initial_state,
  agent_monologue_actuation_observe,
} from "std/agent/monologue_actuation"
import "std/agent/stall_config"
import {
  AgentStallConfig,
  AgentStallObservation,
  AgentStallState,
  AgentStallWarning,
} from "std/agent/stall_types"
import { __agent_stall_fold_verify_state } from "std/agent/stall_verification"
import {
  agent_emit_event,
  agent_session_inject_feedback,
  agent_session_totals,
} from "std/agent/state"
import { verification_gate_input } from "std/verification"

/**
 * Reset only the action-stream tracking (signatures, observation/error
 * streaks, ping-pong). Leaves the consecutive-trip escalation and feedback
 * counters alone. Used when an action is exempt or the turn made no tool call.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_reset_action(state: AgentStallState) -> AgentStallState {
  return state
    + {
    last_signature: "",
    streak: 0,
    last_observation_signature: "",
    last_outcome_kind: "",
    same_observation_streak: 0,
    same_error_streak: 0,
    context_window_error_streak: 0,
    ping_pong_a: "",
    ping_pong_b: "",
    ping_pong_alternations: 0,
  }
}

pub fn __agent_tool_call_name(call) -> string {
  return to_string(call?.name ?? call?.tool_name ?? "")
}

pub fn __agent_tool_call_args(call) -> dict {
  const raw = call?.arguments ?? call?.tool_args
  if type_of(raw) == "dict" {
    return raw
  }
  return {}
}

pub fn __agent_tool_call_signature(call) -> string {
  const args_text = json_stringify(__agent_tool_call_args(call))
  return __agent_tool_call_name(call) + "\n" + args_text
}

/**
 * observation classification (the polling-exemption mechanism)
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_result_ok(result) -> bool {
  if result?.ok != nil {
    return result.ok ? true : false
  }
  if result?.success != nil {
    return result.success ? true : false
  }
  const status = to_string(result?.status ?? "")
  return status == "ok" || status == "success"
}

pub fn __agent_stall_result_name(result) -> string {
  return to_string(result?.tool_name ?? result?.name ?? "")
}

/**
 * A context-window / token-limit error is its own trip condition because
 * repeating it is never productive and never recovers on its own.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_is_context_window_error(text: string) -> bool {
  const lowered = lowercase(text)
  return contains(lowered, "context window")
    || contains(lowered, "context length")
    || contains(lowered, "context_length")
    || contains(lowered, "maximum context")
    || contains(lowered, "token limit")
    || contains(lowered, "too many tokens")
    || contains(lowered, "context_length_exceeded")
    || contains(lowered, "max_tokens")
}

pub fn __agent_stall_delta_signature(current: dict) -> string {
  const signatures = current?.signatures ?? []
  const count = to_int(current?.count) ?? len(signatures)
  return "err:" + sha256(json_stringify({signatures: signatures, count: count}))
}

pub fn __agent_stall_result_error_signature(delta, result) -> string {
  if len(delta.current.signatures) > 0 {
    return __agent_stall_delta_signature(delta.current)
  }
  const fallback = to_string(result?.error ?? result?.message ?? result?.result ?? "")
  return "err:" + sha256(fallback)
}

pub fn __agent_stall_current_hashes(result) {
  if type_of(result) != "dict" {
    return nil
  }
  return result?.current_hashes
    ?? result?.currentHashes
    ?? result?.file_hashes
    ?? result?.fileHashes
}

pub fn __agent_stall_gate_input(previous, result) {
  return verification_gate_input(previous, result, __agent_stall_current_hashes(result))
}

/**
 * Outcome of a dispatched result, normalized for the detector: one of
 * "ok" / "error" / "advisory" / "context_window", plus a byte-stable
 * signature of the observation payload (for "ok") or diagnostic set.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_result_outcome(result) -> dict {
  if __agent_stall_result_ok(result) {
    const payload = json_stringify(result?.result ?? result?.output ?? result?.content)
    return {kind: "ok", signature: "ok:" + sha256(payload)}
  }
  const error_text = to_string(result?.error ?? result?.message ?? result?.result ?? "")
  if __agent_stall_is_context_window_error(error_text) {
    return {kind: "context_window", signature: "ctx"}
  }
  const gate = __agent_stall_gate_input(nil, result)
  if !gate.feedsGates {
    return {kind: "advisory", signature: "advisory:" + sha256(json_stringify(gate.current))}
  }
  return {kind: "error", signature: __agent_stall_result_error_signature(gate.delta, result)}
}

/**
 * Short, human-readable evidence snippet for a failing result (used to ground
 * the strategy-shift nudge and the terminal current-failure payload). Never
 * hashed — the byte-stable signature stays in __agent_stall_result_outcome.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_result_snippet(result) -> string {
  if __agent_stall_result_ok(result) {
    return ""
  }
  const text = to_string(result?.error ?? result?.message ?? result?.result ?? "")
  const normalized = trim(text)
  return __agent_clip_head_tail(normalized, 4000)
}

/**
 * Clip an evidence string to `cap` chars WITHOUT amputating the tail. The root
 * cause of a failure is often split across both ends — the exception/class up
 * top, the decisive line (failing assertion, last compiler error) at the
 * bottom. A blind head-only clip silently hid that tail from the model. Keep a
 * generous head + tail with an explicit elision marker so nothing is dropped
 * invisibly. `cap` is intentionally large; we are NOT trying to save tokens,
 * only to bound a pathological multi-megabyte blob.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_clip_head_tail(text, cap) {
  const total = len(text)
  if total <= cap {
    return text
  }
  const tail_len = cap / 3
  const head_len = cap - tail_len
  const omitted = total - head_len - tail_len
  const head = text.substring(0, head_len)
  const tail = text.substring(total - tail_len, total)
  return head + "\n…[" + to_string(omitted) + " chars elided — middle of output]…\n" + tail
}

/**
 * Pick the representative verification result for a turn from its dispatch:
 * the FIRST failing result (the diagnostic the agent must fix) if any,
 * otherwise the first result (a "pass" turn). Returns nil when there is no
 * dispatch result to classify (the turn made no observed tool call), so the
 * caller leaves the current-failure model untouched.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_turn_result(prev_dispatch) {
  if prev_dispatch == nil {
    return nil
  }
  const results = if type_of(prev_dispatch) == "list" {
    prev_dispatch
  } else {
    prev_dispatch?.results ?? []
  }
  let first = nil
  for result in results {
    if !__agent_stall_result_ok(result) {
      return result
    }
    if first == nil {
      first = result
    }
  }
  return first
}

pub fn __agent_stall_delta_previous(state: AgentStallState) {
  if state.last_diagnostic_class != "fail" {
    return nil
  }
  const current = state.last_diagnostic_delta_current ?? {}
  const signatures = current?.signatures ?? []
  if len(signatures) == 0 {
    return nil
  }
  return {
    diagnostics: signatures,
    diagnostic_count: to_int(current?.count) ?? len(signatures),
    feedsGates: current?.feedsGates ?? current?.feeds_gates ?? true,
    classification: {status: "bound_fresh", feedsGates: true, advisory: false},
  }
}

pub fn __agent_stall_gate_failure(gate) -> bool {
  return gate != nil && (gate?.gate_bearing_failure ?? false)
}

/**
 * __agent_stall_fold_diagnostic folds the current-failure model for one turn.
 * It is a PURE function of (state, prev_dispatch, turn_made_edit):
 *
 *  - It classifies the turn's representative verification result (the first
 *    failing dispatch result, else the first result) through
 *    std/verification's diagnostic-delta helper. That helper owns
 *    location/temp-path normalization and the stale/advisory gate contract.
 *  - On a successful workspace-mutating edit this turn it bumps `write_epoch`
 *    (which arms the post-edit re-verify mandate the loop honors).
 *  - The failure SIGNATURE is the progress signal: a productive edit changes
 *    the error (signature changes => streak resets — legitimate progress, no
 *    false positive); a flailing edit leaves the SAME error (signature
 *    identical => streak advances — real thrash). So the streak advances on the
 *    same signature REGARDLESS of intervening edits.
 *  - It advances/resets `same_diagnostic_streak`:
 *      same failure signature  => futile  (streak + 1), even across edits
 *      different signature      => reset to 1 (a different mistake, or progress)
 *      "pass"                   => clear streak, reverify_owed, edit_since_failure
 *
 * Result handling:
 *  - a FAILING result  => fold the streak as above; this is verification
 *    evidence, so any owed re-verify is satisfied (clear reverify_owed /
 *    edit_since_failure). A turn that BOTH edits and re-tests still classifies
 *    here: __agent_stall_turn_result returns the FIRST failing result, so the
 *    re-test's failure is seen past the edit's own "ok" (the edit bumps the
 *    epoch; the fail still advances the streak).
 *  - a non-failing result, NOT an edit turn => a genuine passing verification
 *    clears the current-failure model.
 *  - a non-failing result on an EDIT turn => the result is the edit's own
 *    success, NOT verification of the failure. Owe a re-verify and preserve the
 *    failure model (same as the no-result edit case).
 *  - no result (prev_dispatch nil/empty) => if this turn made an edit on top of
 *    a live failure, owe a re-verify (reverify_owed / edit_since_failure) and
 *    bump the epoch; otherwise leave the model untouched (epoch still bumps on
 *    an edit).
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_fold_diagnostic(
  state: AgentStallState,
  prev_dispatch,
  turn_made_edit: bool,
) -> AgentStallState {
  // Edit bookkeeping first: a successful workspace mutation advances the epoch,
  // which arms the post-edit re-verify mandate in the loop.
  const new_write_epoch = if turn_made_edit {
    state.write_epoch + 1
  } else {
    state.write_epoch
  }
  // Elapsed-turns floor (#burin-cutover seam 5): count EVERY folded turn since the
  // last clean verification. Every branch below carries this +1 forward EXCEPT the
  // clean-pass branch, which resets it to 0. Always maintained (cheap, and never
  // surfaced in any warning/result projection); the floor is consulted only when a
  // `turns_since_clean_verify` knob is set, so this leaves default decisions
  // byte-identical.
  const new_turns_since_clean_verify = state.turns_since_clean_verify + 1
  const result = __agent_stall_turn_result(prev_dispatch)
  const result_is_non_ok = result != nil && !__agent_stall_result_ok(result)
  const gate = if result_is_non_ok {
    __agent_stall_gate_input(__agent_stall_delta_previous(state), result)
  } else {
    nil
  }
  const delta = gate?.delta
  const result_is_gate_failure = __agent_stall_gate_failure(gate)
  if !result_is_gate_failure {
    // No FAILING verification this turn. A successful edit on top of a live
    // failure is NOT proof the failure is resolved (its "ok" is the edit
    // succeeding, not the check passing): owe a re-verify and preserve the
    // current-failure model so the next re-fail of the SAME diagnostic is
    // recognized across the edit. A genuine passing result on a NON-edit turn
    // is real verification evidence and clears the model.
    if result_is_non_ok && (delta == nil || delta.status != "cleared") {
      if turn_made_edit && state.last_diagnostic_class == "fail" {
        return state
          + {
          write_epoch: new_write_epoch,
          edit_since_failure: true,
          reverify_owed: true,
          turns_since_clean_verify: new_turns_since_clean_verify,
        }
      }
      return state
        + {
        write_epoch: new_write_epoch,
        turns_since_clean_verify: new_turns_since_clean_verify,
      }
    }
    if turn_made_edit {
      if state.last_diagnostic_class == "fail" {
        return state
          + {
          write_epoch: new_write_epoch,
          edit_since_failure: true,
          reverify_owed: true,
          turns_since_clean_verify: new_turns_since_clean_verify,
        }
      }
      return state
        + {
        write_epoch: new_write_epoch,
        turns_since_clean_verify: new_turns_since_clean_verify,
      }
    }
    if result == nil {
      // No verification evidence and no edit-on-failure: leave the model alone.
      return state
        + {
        write_epoch: new_write_epoch,
        turns_since_clean_verify: new_turns_since_clean_verify,
      }
    }
    // A passing verification clears the current-failure model.
    const pass_signature = if delta != nil {
      "ok:" + sha256(json_stringify(delta.current))
    } else {
      __agent_stall_result_outcome(result).signature
    }
    const verified_epoch = if state.write_epoch > state.verified_write_epoch {
      state.write_epoch
    } else {
      state.verified_write_epoch
    }
    return state
      + {
      last_diagnostic_class: "pass",
      last_diagnostic_signature: pass_signature,
      last_diagnostic_snippet: "",
      last_diagnostic_delta_current: {},
      last_diagnostic_delta_status: delta?.status ?? "cleared",
      last_diagnostic_delta_reason: delta?.reason ?? "current_result_is_passing",
      write_epoch: new_write_epoch,
      same_diagnostic_streak: 0,
      failing_verify_turns: 0,
      turns_since_clean_verify: 0,
      edit_since_failure: false,
      reverify_owed: false,
      verified_write_epoch: verified_epoch,
    }
  }
  // Gate-bearing failure: fold against the prior failure model. The diagnostic
  // delta status is the progress signal, so the streak advances only when the
  // std/verification helper says the normalized signature set is unchanged.
  // Advisory/stale/unbound diagnostics never reach this branch.
  const signature = __agent_stall_delta_signature(delta.current)
  const same_signature = delta.status == "unchanged" && state.last_diagnostic_class == "fail"
  const new_streak = if same_signature {
    state.same_diagnostic_streak + 1
  } else {
    1
  }
  const failing_turns = state.failing_verify_turns + 1
  // Total (non-consecutive) recurrence per signature (#burin-cutover seam 1). The
  // consecutive `same_diagnostic_streak` above RESETS whenever an off-signature
  // failure interposes, so it misses interleaved churn: a dead API / wrong error
  // re-proposed N times with different failures churning between recurrences
  // never builds a consecutive streak yet is provably stuck. This per-signature
  // TOTAL counter never resets on a signature change, so the verify-state axis
  // can trip a hard-recurrence cut on total occurrences regardless of ordering.
  // Always maintained (cheap); consulted only when a progress_signal is supplied.
  const sig_counts = state.verify_signature_counts ?? {}
  const sig_total = (to_int(sig_counts[signature] ?? 0) ?? 0) + 1
  // This is a verification result, so the post-edit re-verify mandate (if any)
  // has now been satisfied: clear reverify_owed and the edit-since marker. The
  // NEXT edit after this failure re-arms them.
  return state
    + {
    last_diagnostic_class: "fail",
    last_diagnostic_signature: signature,
    last_diagnostic_snippet: __agent_stall_result_snippet(result),
    last_diagnostic_delta_current: delta.current,
    last_diagnostic_delta_status: delta.status,
    last_diagnostic_delta_reason: delta.reason,
    write_epoch: new_write_epoch,
    same_diagnostic_streak: new_streak,
    failing_verify_turns: failing_turns,
    turns_since_clean_verify: new_turns_since_clean_verify,
    edit_since_failure: false,
    reverify_owed: false,
    verify_signature_counts: sig_counts + {[signature]: sig_total},
  }
}

/**
 * Find the dispatch result for the given tool name. The detector tracks one
 * action at a time, so the first matching result is the relevant observation.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_outcome_for(prev_dispatch, tool_name: string) {
  if prev_dispatch == nil {
    return nil
  }
  const results = if type_of(prev_dispatch) == "list" {
    prev_dispatch
  } else {
    prev_dispatch?.results ?? []
  }
  for result in results {
    if __agent_stall_result_name(result) == tool_name {
      return __agent_stall_result_outcome(result)
    }
  }
  return nil
}

pub fn __agent_stall_feedback_text(warning: AgentStallWarning) -> string {
  const pattern = warning.pattern
  const count = to_string(warning.count)
  // Evidence-aware repair loop: a strategy-shift nudge grounded in the actual
  // diagnostic, not a blind "you repeated yourself" counter. This fires when
  // the same failure has survived `stuck_same_diagnostic_after` repair turns
  // with no intervening successful edit (or with edits that did not change the
  // failure), so the right move is to change approach, not to retry harder.
  if pattern == "stuck_same_diagnostic" {
    const snippet = to_string(warning.diagnostic_snippet ?? "")
    const evidence = if snippet != "" {
      " The failing evidence is unchanged:\n" + snippet
    } else {
      ""
    }
    return "Repair diagnostic: the same failure has persisted across "
      + count
      + " repair turns without changing."
      + evidence
      + "\nStop retrying the same fix. Re-read the relevant code, form a NEW hypothesis "
      + "about the root cause, or gather different evidence before editing again. If you "
      + "cannot make progress, report this specific blocker instead of repeating."
  }
  // Delivered-fix trigger (#burin-cutover seam 3): a repair WAS delivered for
  // this exact failure and it STILL did not land — a stronger stuck signal than
  // an untouched same-failure repeat, so the nudge is blunter.
  if pattern == "delivered_fix_not_landing" {
    const snippet = to_string(warning.diagnostic_snippet ?? "")
    const evidence = if snippet != "" {
      " The failing evidence is unchanged:\n" + snippet
    } else {
      ""
    }
    return "Repair diagnostic: a fix was delivered for this failure but it did NOT land — the same failure has recurred across "
      + count
      + " turns despite the attempted remediation."
      + evidence
      + "\nThe current approach is not working. Do NOT re-apply the same fix. Re-read the"
      + " root cause from scratch, form a materially different hypothesis, or report this"
      + " specific blocker instead of repeating."
  }
  // Per-turn action-signal intake (#burin-cutover seam 4): the host flagged THIS
  // turn as a flailing action (desperation write / truncated read / oscillating
  // edit) — a strong single-turn stuck signal, so the nudge is direct.
  if pattern == "flailing_action" {
    return "Loop detector: this turn performed a flailing action (a desperation write, a"
      + " truncated/partial read, or an oscillating edit) — a strong single-turn signal that"
      + " the run is stuck. Stop repeating the current approach. Re-read the relevant code,"
      + " form a NEW hypothesis about the root cause, or report the specific blocker instead"
      + " of acting reflexively."
  }
  const core = if pattern == "repeated_same_observation" {
    "the action `" + warning.tool_name + "` produced the same observation " + count
      + " times in a row"
  } else if pattern == "repeated_error" {
    "the action `" + warning.tool_name + "` failed with the same error " + count + " times in a row"
  } else if pattern == "no_progress_monologue" {
    count + " consecutive assistant messages made no progress (no tool call)"
  } else if pattern == "errored_no_tool_call" {
    "your previous turn ended with a provider error and emitted no tool call ("
      + count
      + " in a row) — re-emit the intended tool call"
  } else if pattern == "no_net_progress_hard_cap" {
    count
      + " failing verification turns occurred without a clean passing verify"
  } else if pattern == "turns_since_clean_verify_floor" {
    count
      + " turns elapsed since the last clean verification without a passing verify"
  } else if pattern == "ping_pong" {
    "the loop is ping-ponging between two actions (" + count + " cycles)"
  } else if pattern
    == "repeated_context_window_error" {
    "the run hit a context-window error " + count + " times in a row"
  } else {
    "the last " + count + " tool calls repeated `" + warning.tool_name
      + "` with identical arguments"
  }
  if warning.hard_stop {
    return "Loop detector (hard stop): "
      + core
      + ". "
      + to_string(warning.consecutive_trips)
      + " trips with no recovery — stop and report the blocker instead of repeating."
  }
  return "Stall diagnostic: "
    + core
    + ". Use different evidence, finish, or explain why repeating is necessary before doing so again."
}

pub fn __agent_stall_warning_record(
  iteration: int,
  tool_name: string,
  args: dict,
  signature: string,
  pattern: string,
  count: int,
  consecutive_trips: int,
  hard_stop: bool,
  config: AgentStallConfig,
  threshold: int,
) -> AgentStallWarning {
  const args_text = json_stringify(args)
  let record = {
    iteration: iteration,
    tool_name: tool_name,
    repeat_count: count,
    threshold: threshold,
    arguments_digest: "sha256:" + sha256(args_text),
    signature_digest: "sha256:" + sha256(signature),
    pattern: pattern,
    signature: signature,
    count: count,
    consecutive_trips: consecutive_trips,
    hard_stop: hard_stop,
  }
  if config.include_arguments {
    record = record + {arguments: args}
  }
  return record
}

/**
 * agent_stall_inject_feedback injects bounded stall feedback.
 *
 * @effects: [agent]
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_stall_inject_feedback(
  session_id: string,
  warning: AgentStallWarning,
  config: AgentStallConfig,
  state: AgentStallState,
) -> AgentStallState {
  const wants_feedback = config.inject_feedback ?? true
  const should_feedback = wants_feedback && state.feedback_count < config.max_feedback
  if should_feedback {
    agent_session_inject_feedback(
      session_id,
      "stall_diagnostics",
      __agent_stall_feedback_text(warning),
    )
    return state + {feedback_count: state.feedback_count + 1}
  }
  return state
}

pub fn __agent_stall_maybe_emit(
  session_id: string,
  warning: AgentStallWarning,
  config: AgentStallConfig,
  state: AgentStallState,
  defer_feedback: bool,
) {
  agent_emit_event(session_id, "agent_loop_stall_warning", warning)
  if defer_feedback {
    return {state: state, warning: warning, feedback_deferred: true}
  }
  return {
    state: agent_stall_inject_feedback(session_id, warning, config, state),
    warning: warning,
    feedback_deferred: false,
  }
}

/**
 * ping-pong tracking: track the two action signatures in a candidate A/B
 * alternation and the count of alternations. Two alternations (A->B->A) is one
 * cycle. Returns the updated state; the cycle count lives in
 * `ping_pong_alternations`.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_update_ping_pong(
  state: AgentStallState,
  signature: string,
) -> AgentStallState {
  const a = state.ping_pong_a
  const b = state.ping_pong_b
  if a == "" {
    return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
  }
  if b == "" {
    if a == signature {
      // Same action twice — this is the same-action repeat path, not ping-pong.
      return state + {ping_pong_alternations: 0}
    }
    return state + {ping_pong_b: signature, ping_pong_alternations: 1}
  }
  const expected_next = if state.ping_pong_alternations % 2 == 1 {
    a
  } else {
    b
  }
  if expected_next == signature {
    return state + {ping_pong_alternations: state.ping_pong_alternations + 1}
  }
  if a == signature || b == signature {
    // Within the A/B pair but out of strict alternation: restart the pair
    // anchored on the current value.
    return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
  }
  // A third distinct action: not a two-action ping-pong.
  return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
}

/**
 * Apply a fired trip: bump the consecutive-trip escalation and decide whether
 * to flag a hard stop. Returns the state with the trip recorded.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_register_trip(
  state: AgentStallState,
  config: AgentStallConfig,
) -> AgentStallState {
  const trips = state.consecutive_trips + 1
  return state + {consecutive_trips: trips, hard_stop: trips >= config.hard_stop_after_trips}
}

/**
 * Apply a terminal hard-stop trip. Used for safety floors that should stop the
 * run immediately rather than waiting for several warning/recovery cycles.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_register_immediate_hard_stop(state: AgentStallState) -> AgentStallState {
  return state + {consecutive_trips: state.consecutive_trips + 1, hard_stop: true}
}

/**
 * A non-tripping step recovers: reset the consecutive-trip escalation so the
 * hard stop only fires on genuinely uninterrupted thrashing.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_register_recovery(state: AgentStallState) -> AgentStallState {
  return state + {consecutive_trips: 0, hard_stop: false}
}

/**
 * Build an ENABLED `AgentStallObservation` from an already-folded `state`.
 * `hard_stop` always mirrors `state.hard_stop` (the trip/recovery helpers are
 * the sole authority on it), so callers pass the post-fold state and never
 * re-derive the flag. The `config.enabled == false` short-circuit is the one
 * observation that is NOT built here — it returns the untouched incoming state
 * with `enabled: false`.
 *
 * @effects: []
 * @errors: []
 */
pub fn __agent_stall_observation(
  state: AgentStallState,
  warning,
  config: AgentStallConfig,
  feedback_deferred: bool,
) -> AgentStallObservation {
  return {
    state: state,
    enabled: true,
    warning: warning,
    feedback_deferred: feedback_deferred,
    config: config,
    hard_stop: state.hard_stop,
  }
}

/**
 * Emit one diagnostic-trip warning and fold it into the observation. Shared by
 * the three repair-overlay trips in `__agent_stall_apply_repair`
 * (`no_net_progress_hard_cap`, `delivered_fix_not_landing`,
 * `stuck_same_diagnostic`), which differ only in the already-`tripped` state,
 * the pattern/count/threshold diagnostic fields, and whether the
 * `remediation_delivered` marker is set.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_emit_diagnostic_trip(
  session_id: string,
  config: AgentStallConfig,
  tripped: AgentStallState,
  iteration: int,
  pattern: string,
  count: int,
  threshold: int,
  defer_feedback: bool,
  remediation_delivered = false,
) -> AgentStallObservation {
  let warning = __agent_stall_warning_record(
    iteration,
    "",
    {},
    tripped.last_diagnostic_signature,
    pattern,
    count,
    tripped.consecutive_trips,
    tripped.hard_stop,
    config,
    threshold,
  )
    + {
    diagnostic_class: tripped.last_diagnostic_class,
    diagnostic_signature: tripped.last_diagnostic_signature,
    diagnostic_snippet: tripped.last_diagnostic_snippet,
  }
  if remediation_delivered {
    warning = warning + {remediation_delivered: true}
  }
  const emitted = __agent_stall_maybe_emit(session_id, warning, config, tripped, defer_feedback)
  const next_state = emitted.state + {warnings: emitted.state.warnings.push(warning)}
  return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}

/**
 * Detect a no-progress monologue turn (assistant produced text but no tool
 * call). Returns the observation when the streak crosses the threshold.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_observe_no_progress(
  session_id: string,
  iteration: int,
  config: AgentStallConfig,
  state: AgentStallState,
  defer_feedback: bool,
  turn_stop_reason = "",
) -> AgentStallObservation {
  // An agent message breaks any action-repeat / ping-pong run.
  let next_state = __agent_stall_reset_action(state)
  next_state = next_state + {no_progress_streak: next_state.no_progress_streak + 1}
  if next_state.no_progress_streak < config.no_progress_messages {
    next_state = __agent_stall_register_recovery(next_state)
    return __agent_stall_observation(next_state, nil, config, false)
  }
  next_state = __agent_stall_register_trip(next_state, config)
  // A turn that ended with a provider error (stop_reason=error) but emitted no
  // tool call narrated its intent without landing an action — the cheap-model
  // eval-meter failure class. Give it cause-specific feedback instead of the
  // generic "made no progress" nag, so the model knows its turn errored and to
  // re-emit the intended tool call.
  const pattern = if to_string(turn_stop_reason) == "error" {
    "errored_no_tool_call"
  } else {
    "no_progress_monologue"
  }
  const warning = __agent_stall_warning_record(
    iteration,
    "",
    {},
    "",
    pattern,
    next_state.no_progress_streak,
    next_state.consecutive_trips,
    next_state.hard_stop,
    config,
    config.no_progress_messages,
  )
  const emitted = __agent_stall_maybe_emit(session_id, warning, config, next_state, defer_feedback)
  next_state = emitted.state + {warnings: emitted.state.warnings.push(warning)}
  return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}

/**
 * __agent_stall_process_call folds a single tool call (and the observation it
 * produced last turn) into the detector state and decides which condition, if
 * any, tripped. Extracted from `agent_stall_observe_tool_calls` to keep that
 * function within the cyclomatic-complexity budget. Returns the updated state
 * plus a `trip` dict (or nil). Trip ordering: context-window, then repeated
 * error, then repeated identical observation, then ping-pong, then the legacy
 * signature-only threshold (used only when no dispatch results are available).
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_process_call(
  state: AgentStallState,
  config: AgentStallConfig,
  call,
  tool_name: string,
  prev_dispatch,
) -> dict {
  const signature = __agent_tool_call_signature(call)
  const same_action = signature == state.last_signature
  const outcome = __agent_stall_outcome_for(prev_dispatch, tool_name)
  // Observation signature of the action that just repeated. When dispatch
  // results are unavailable, fall back to the action signature itself so the
  // legacy adjacent-repeat behavior still trips.
  const obs_signature = if outcome != nil {
    outcome.signature
  } else {
    "sig:" + sha256(signature)
  }
  const outcome_kind = if outcome != nil {
    outcome.kind
  } else {
    "ok"
  }
  // Ping-pong is independent of the same-action streak and only considers
  // distinct alternating actions; update it first.
  let next_state = __agent_stall_update_ping_pong(state, signature)
  const streak = if same_action {
    next_state.streak + 1
  } else {
    1
  }
  const repeated = if streak > 1 {
    next_state.repeated_tool_calls + 1
  } else {
    next_state.repeated_tool_calls
  }
  // The polling exemption (principled, not an allowlist): a repeated identical
  // action counts toward the same-observation condition UNLESS we have positive
  // evidence the observation changed — i.e. both this turn's and last turn's
  // observations are real (non-fallback) AND differ. A changing observation is
  // legitimate polling (status advances, watched file mutates) and resets the
  // streak. When observations are unavailable (no dispatch results threaded in)
  // the streak counts the raw signature repeat, preserving the legacy
  // adjacent-identical-call behavior so existing thresholds still trip.
  const both_known = outcome != nil && next_state.last_outcome_kind != ""
  const observation_changed = both_known && obs_signature != next_state.last_observation_signature
  const new_same_observation_streak = if !same_action || outcome_kind != "ok" {
    if outcome_kind == "ok" {
      1
    } else {
      0
    }
  } else if observation_changed {
    1
  } else {
    next_state.same_observation_streak + 1
  }
  // Same-error streak: same action producing the same (or unknown) error.
  // Distinct errors reset, because the agent is making different mistakes.
  const new_same_error_streak = if !same_action || outcome_kind != "error" {
    if outcome_kind == "error" {
      1
    } else {
      0
    }
  } else if observation_changed {
    1
  } else {
    next_state.same_error_streak + 1
  }
  const new_context_streak = if outcome_kind == "context_window" {
    next_state.context_window_error_streak + 1
  } else {
    0
  }
  next_state = next_state
    + {
    last_signature: signature,
    streak: streak,
    repeated_tool_calls: repeated,
    last_observation_signature: obs_signature,
    last_outcome_kind: if outcome != nil {
      outcome_kind
    } else {
      ""
    },
    same_observation_streak: new_same_observation_streak,
    same_error_streak: new_same_error_streak,
    context_window_error_streak: new_context_streak,
  }
  const ping_pong_cycles = next_state.ping_pong_alternations / 2
  const trip = __agent_stall_classify_trip(
    config,
    {
      context_streak: new_context_streak,
      same_error_streak: new_same_error_streak,
      same_observation_streak: new_same_observation_streak,
      outcome_kind: outcome_kind,
      ping_pong_cycles: ping_pong_cycles,
      outcome_present: outcome != nil,
      streak: streak,
    },
  )
  return {state: next_state, trip: trip}
}

/**
 * Decide which stall condition, if any, tripped for a single processed call.
 * Extracted from `__agent_stall_process_call` to keep both within the
 * cyclomatic-complexity budget. Trip ordering: context-window, repeated error,
 * repeated identical observation, ping-pong, then the legacy signature-only
 * threshold. When the evidence-aware repair loop is enabled it OWNS the
 * repeated-failure semantics (same diagnostic across repair turns) via the
 * diagnostic-grounded `stuck_same_diagnostic` pattern in the repair overlay, so
 * the legacy `repeated_error` / `repeated_same_observation` trips are suppressed
 * here (no double-fire; the richer repair nudge wins). Context-window and
 * ping-pong remain — they are orthogonal failure modes the repair model does
 * not cover.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_classify_trip(config: AgentStallConfig, s) {
  const repair_owns_repeat = config.repair_aware
  if s.context_streak >= config.repeat_context_window_error {
    return {
      pattern: "repeated_context_window_error",
      count: s.context_streak,
      threshold: config.repeat_context_window_error,
    }
  }
  if !repair_owns_repeat && s.same_error_streak >= config.repeat_same_error {
    return {
      pattern: "repeated_error",
      count: s.same_error_streak,
      threshold: config.repeat_same_error,
    }
  }
  if !repair_owns_repeat
    && s.outcome_kind == "ok"
    && s.same_observation_streak >= config.repeat_same_observation {
    return {
      pattern: "repeated_same_observation",
      count: s.same_observation_streak,
      threshold: config.repeat_same_observation,
    }
  }
  if s.ping_pong_cycles >= config.ping_pong_cycles {
    return {pattern: "ping_pong", count: s.ping_pong_cycles, threshold: config.ping_pong_cycles}
  }
  if !s.outcome_present && s.streak >= config.threshold {
    return {pattern: "repeated_same_signature", count: s.streak, threshold: config.threshold}
  }
  return nil
}

/**
 * agent_stall_observe_tool_calls detects degenerate agent loops.
 *
 * Conditions: same action -> identical observation, same action -> same
 * error, no-progress monologue, ping-pong, and repeated context-window
 * errors. `prev_dispatch` carries the previous turn's dispatch results so the
 * detector can attribute an observation signature to the repeated action;
 * pass nil to fall back to the signature-only same-action heuristic. The
 * polling exemption is principled: a repeated identical action counts toward
 * the same-observation and ping-pong conditions only when its observation is
 * byte-identical across repeats. `exempt_tools` remains a secondary escape
 * hatch.
 *
 * @effects: [agent]
 * @errors: [agent_loop]
 * @api_stability: experimental
 */
pub fn __agent_stall_observe_core(
  session_id: string,
  tool_calls: list,
  iteration: int,
  raw_config,
  state: AgentStallState,
  defer_feedback: bool,
  prev_dispatch = nil,
  turn_text = "",
  had_parse_errors = false,
  turn_stop_reason = "",
) -> AgentStallObservation {
  const config = __agent_stall_config(raw_config)
  if !config.enabled {
    return {
      state: state,
      enabled: false,
      warning: nil,
      feedback_deferred: false,
      config: config,
      hard_stop: false,
    }
  }
  // A turn whose tool calls were all dropped by the parser carries real intent
  // (it tried to act); the malformed-call path already injected purpose-built
  // parse-guidance feedback. Treat it as a neutral recovery turn so it does NOT
  // count toward the no-progress monologue streak — otherwise the loop nags the
  // model to "emit one well-formed tool call" when it already did, just with a
  // malformed body. Gated purely on the syntactic parse-error signal.
  if had_parse_errors {
    return __agent_stall_observation(
      __agent_stall_register_recovery(__agent_stall_reset_action(state)),
      nil,
      config,
      false,
    )
  }
  // Soft progress-report tools (e.g. `agent_progress`) report status without
  // advancing task state, so they are excluded from the real-action stream: a
  // turn whose only call is such a report is treated as no-progress.
  const substantive_calls = tool_calls.filter(
    { c -> !contains(SOFT_PROGRESS_TOOLS, __agent_tool_call_name(c)) },
  )
  if len(substantive_calls) == 0 {
    // No substantive tool call this turn — either none at all, or only soft
    // progress reports. With visible text or a progress report, that is a
    // no-progress monologue candidate; a fully empty turn resets the action
    // stream.
    if to_string(turn_text) != "" || len(tool_calls) > 0 {
      return __agent_stall_observe_no_progress(
        session_id,
        iteration,
        config,
        state,
        defer_feedback,
        turn_stop_reason,
      )
    }
    return __agent_stall_observation(
      __agent_stall_register_recovery(__agent_stall_reset_action(state)),
      nil,
      config,
      false,
    )
  }
  let next_state = state + {no_progress_streak: 0}
  let emitted_warning = nil
  let feedback_deferred = false
  for call in substantive_calls {
    const tool_name = __agent_tool_call_name(call)
    if tool_name == "" || contains(config.exempt_tools, tool_name) {
      next_state = __agent_stall_register_recovery(__agent_stall_reset_action(next_state))
      continue
    }
    const processed = __agent_stall_process_call(next_state, config, call, tool_name, prev_dispatch)
    next_state = processed.state
    const trip = processed.trip
    if trip != nil {
      next_state = __agent_stall_register_trip(next_state, config)
      const warning = __agent_stall_warning_record(
        iteration,
        tool_name,
        __agent_tool_call_args(call),
        __agent_tool_call_signature(call),
        trip.pattern,
        trip.count,
        next_state.consecutive_trips,
        next_state.hard_stop,
        config,
        trip.threshold,
      )
      const emitted = __agent_stall_maybe_emit(
        session_id,
        warning,
        config,
        next_state,
        defer_feedback,
      )
      next_state = emitted.state
      if emitted_warning == nil {
        emitted_warning = warning
      }
      feedback_deferred = feedback_deferred || (emitted.feedback_deferred ?? false)
      next_state = next_state + {warnings: next_state.warnings.push(warning)}
    } else {
      next_state = __agent_stall_register_recovery(next_state)
    }
  }
  return __agent_stall_observation(next_state, emitted_warning, config, feedback_deferred)
}

/**
 * Evidence-aware repair overlay (#repair-diagnostics). The current-failure
 * fold is shared by post-edit verification and repair-aware nudges. When
 * `post_edit_reverify` is enabled this folds the PRIOR turn's verification
 * result + whether that turn made a successful edit (turn_made_edit), so the
 * loop can require a final verify even when repair nudges are disabled. When
 * `repair_aware` is also enabled this trips the "stuck_same_diagnostic"
 * strategy-shift nudge when the same diagnostic has survived
 * `stuck_same_diagnostic_after` repair turns.
 * The trip rides the EXISTING `agent_loop_stall_warning` event +
 * `__agent_stall_register_trip` escalation; no new event type is introduced.
 * When the core observation already produced a warning this turn we keep it
 * and only fold the model (we do not stack two warnings in one turn).
 */
/**
 * Delivered-fix intake (#burin-cutover seam 3). The `remediation_delivered`
 * callback may return either a bool (the ORIGINAL per-turn form, unchanged) or an
 * int CUMULATIVE count of remediations delivered for the active failure signature
 * (burin's `injections_now` fact — cumulative "since this signature became
 * active"). For the int form harn derives the per-turn "a fix was delivered"
 * event by diffing the returned count against the count last SEEN for that
 * signature (`state.remediation_delivered_*`), resetting the baseline to 0 on a
 * signature change. Returns `{delivered, signature, count}`; the caller folds
 * `signature`/`count` back into state so the next turn's diff has the right
 * baseline. A nil return means "no fix this turn" (delivered false, tracking
 * unchanged), matching the pre-seam falsy-return behavior. Any other return type
 * throws loudly.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_eval_remediation(state: AgentStallState, signature: string, raw) -> dict {
  if type_of(raw) == "int" {
    const same_sig = state.remediation_delivered_signature == signature
    const baseline = if same_sig {
      state.remediation_delivered_count
    } else {
      0
    }
    return {count: raw, delivered: raw > baseline, signature: signature}
  }
  if raw == nil {
    return {
      count: state.remediation_delivered_count,
      delivered: false,
      signature: state.remediation_delivered_signature,
    }
  }
  if type_of(raw) == "bool" {
    return {
      count: state.remediation_delivered_count,
      delivered: raw,
      signature: state.remediation_delivered_signature,
    }
  }
  throw "agent_loop: stall_diagnostics.remediation_delivered callback must return a bool, an int cumulative count, or nil; got "
    + type_of(
    raw,
  )
}

pub fn __agent_stall_apply_repair(
  session_id: string,
  config: AgentStallConfig,
  observation: AgentStallObservation,
  iteration: int,
  prev_dispatch,
  turn_made_edit: bool,
  defer_feedback: bool,
) -> AgentStallObservation {
  const should_fold_current_failure = config.post_edit_reverify || config.repair_aware
  // The elapsed-turns clean-verify floor (#burin-cutover seam 5) is an INDEPENDENT
  // axis: its counter is maintained inside `fold_diagnostic`, so the fold must run
  // whenever the `turns_since_clean_verify` knob is set — even with neither repair
  // mode on. Without this the knob validates at config time but the floor never
  // trips (the counter stays 0), silently dead unless post_edit_reverify or
  // repair_aware happens to also be set. Every downstream trip below is already
  // independently gated on its own config, so folding here changes nothing when only
  // the knob is set beyond maintaining the counter and arming its floor.
  const clean_verify_floor_active = config.turns_since_clean_verify != nil
  if !should_fold_current_failure && !clean_verify_floor_active {
    return observation
  }
  const folded = __agent_stall_fold_diagnostic(observation.state, prev_dispatch, turn_made_edit)
  const hard_cap_due = config.no_net_progress_extend_guard
    && folded.last_diagnostic_class == "fail"
    && folded.failing_verify_turns
    >= config
    .no_net_progress_hard_cap_after
    && observation.state.failing_verify_turns
    < config
    .no_net_progress_hard_cap_after
  if hard_cap_due {
    const tripped = __agent_stall_register_immediate_hard_stop(folded)
    return __agent_stall_emit_diagnostic_trip(
      session_id,
      config,
      tripped,
      iteration,
      "no_net_progress_hard_cap",
      tripped.failing_verify_turns,
      config.no_net_progress_hard_cap_after,
      defer_feedback,
    )
  }
  // Elapsed-turns hard floor (#burin-cutover seam 5, default OFF). Distinct from the
  // failing-verify `no_net_progress_hard_cap_after` above: this counts EVERY folded
  // turn since the last clean verify (edit/read storms included), so a slow-draining
  // run that rarely produces a FAILING verification — never building the failing-verify
  // counter — still stops. Fires ONCE on the crossing edge, forcing a terminal stuck
  // stop. Inert unless the caller sets `turns_since_clean_verify` (nil = off), so today's
  // behavior is byte-identical.
  const clean_floor_due = config.turns_since_clean_verify != nil
    && folded.turns_since_clean_verify
    >= config
    .turns_since_clean_verify
    && observation.state.turns_since_clean_verify
    < config
    .turns_since_clean_verify
  if clean_floor_due {
    const tripped = __agent_stall_register_immediate_hard_stop(folded)
    return __agent_stall_emit_diagnostic_trip(
      session_id,
      config,
      tripped,
      iteration,
      "turns_since_clean_verify_floor",
      tripped.turns_since_clean_verify,
      config.turns_since_clean_verify ?? 0,
      defer_feedback,
    )
  }
  // Delivered-fix trigger (#burin-cutover seam 3, HIGHEST precedence). When the
  // host supplies a `remediation_delivered` fact callback and reports that a
  // repair WAS delivered for the ACTIVE failure signature yet that SAME failure
  // is still landing, escalate one turn sooner than the plain-repeat stuck nudge
  // (STALL_DELIVERED_FIX_AFTER = 2 < stuck_same_diagnostic_after = 3): a fix that
  // was actually attempted and did not land is a stronger stuck signal than an
  // untouched same-failure repeat. harn owns only the TRIGGER (the distinct
  // `delivered_fix_not_landing` pattern + warning.remediation_delivered = true);
  // the host's smart-escalation actuator decides what to do with it. The
  // callback is invoked ONLY inside the guard below, so an absent callback (or a
  // turn that does not otherwise qualify) leaves behavior byte-identical. Fires
  // regardless of `repair_aware` (it keys on the fact, not the repair nudge).
  const delivered_streak_advanced = folded.same_diagnostic_streak
    > observation.state
    .same_diagnostic_streak
  const delivered_candidate = config.remediation_delivered != nil
    && folded.last_diagnostic_class
    == "fail"
    && folded.same_diagnostic_streak >= STALL_DELIVERED_FIX_AFTER
    && delivered_streak_advanced
    && observation.warning == nil
  // `current` carries the cumulative-count baseline forward from the delivered
  // intake below (byte-identical to `folded` on the bool/absent path, since the
  // tracking fields are then unchanged). Every downstream branch folds `current`
  // so a non-tripping evaluation still records the new per-signature count.
  let current = folded
  if delivered_candidate {
    const raw_delivered = config.remediation_delivered(
      {
        prev_dispatch: prev_dispatch,
        session_id: session_id,
        signature: folded.last_diagnostic_signature,
      },
    )
    const evaluated = __agent_stall_eval_remediation(
      folded,
      folded.last_diagnostic_signature,
      raw_delivered,
    )
    current = folded
      + {
      remediation_delivered_count: evaluated.count,
      remediation_delivered_signature: evaluated.signature,
    }
    if evaluated.delivered {
      const tripped = __agent_stall_register_trip(current, config)
      return __agent_stall_emit_diagnostic_trip(
        session_id,
        config,
        tripped,
        iteration,
        "delivered_fix_not_landing",
        tripped.same_diagnostic_streak,
        STALL_DELIVERED_FIX_AFTER,
        defer_feedback,
        true,
      )
    }
  }
  if !config.repair_aware {
    return observation + {state: current}
  }
  // Trip ONCE when the streak first crosses the threshold, so the strategy-shift
  // nudge fires a single time per stuck episode rather than on every subsequent
  // identical failure. Require that the streak ADVANCED this turn (a fresh
  // same-signature failure was folded) — an edit/owe turn preserves the streak
  // at the threshold without re-folding a failure, and must not re-trip. A later
  // streak reset (a different/passing signature) re-arms the trip for the next
  // genuinely-stuck episode. Also require that no other stall pattern already
  // fired this turn (avoid double-nudging).
  const streak_advanced = current.same_diagnostic_streak > observation.state.same_diagnostic_streak
  const should_trip = current.same_diagnostic_streak == config.stuck_same_diagnostic_after
    && streak_advanced
    && observation.warning == nil
  if !should_trip {
    return observation + {state: current}
  }
  const tripped = __agent_stall_register_trip(current, config)
  return __agent_stall_emit_diagnostic_trip(
    session_id,
    config,
    tripped,
    iteration,
    "stuck_same_diagnostic",
    tripped.same_diagnostic_streak,
    config.stuck_same_diagnostic_after,
    defer_feedback,
  )
}

/**
 * Per-turn action-signal intake (#burin-cutover seam 4, default OFF). When the
 * host supplies an `action_signal` fact callback and it returns truthy for THIS
 * turn's payload, a single flailing action (a desperation shell write / truncated
 * read / edit-oscillation verdict) trips IMMEDIATELY — no streak needed — riding
 * the EXISTING `agent_loop_stall_warning` event with the distinct `flailing_action`
 * pattern. Consecutive flailing turns accumulate a DEDICATED `action_flail_trips`
 * counter, escalating to a hard stop once it reaches `hard_stop_after_trips`. That
 * counter is kept SEPARATE from `consecutive_trips` on purpose: the core resets
 * `consecutive_trips` to 0 on every non-core-tripping substantive turn (an
 * action_signal-only flailing stream never trips a core detector), so reusing it
 * would cap the flail escalation at 1 and the hard stop would be unreachable. A
 * non-flailing turn resets the flail streak to 0. Only ONE warning is emitted per
 * turn: if the core/repair path already tripped, the action signal is skipped (no
 * double-nudge) and the streak is left untouched. The callback is invoked ONLY when
 * configured, and this returns the observation UNCHANGED when the callback is nil,
 * so the default (callback unset) is byte-identical.
 *
 * @effects: [agent]
 * @errors: []
 */
pub fn __agent_stall_apply_action_signal(
  session_id: string,
  config: AgentStallConfig,
  observation: AgentStallObservation,
  iteration: int,
  payload,
  defer_feedback: bool,
) -> AgentStallObservation {
  if config.action_signal == nil {
    return observation
  }
  if observation.warning != nil {
    // A core/repair detector already owns this turn's single warning; the
    // action-flail streak is a distinct axis and is left untouched.
    return observation
  }
  const flailing = config.action_signal(payload)
  if !flailing {
    // A non-flailing substantive turn breaks the consecutive-flail streak.
    if observation.state.action_flail_trips == 0 {
      return observation
    }
    return observation + {state: observation.state + {action_flail_trips: 0}}
  }
  // Dedicated accumulator, decoupled from `consecutive_trips` (which the core resets
  // every non-core-tripping turn): repeated flailing escalates to a hard stop at
  // `hard_stop_after_trips`. The flail count rides the warning's `count` field
  // (passed below); we deliberately do NOT write it into `consecutive_trips`, so the
  // core escalation axis stays uncontaminated — otherwise a later core trip at a
  // flail->core-trip boundary would escalate one step early.
  const trips = observation.state.action_flail_trips + 1
  const hard_stop = trips >= config.hard_stop_after_trips
  const tripped = observation.state + {action_flail_trips: trips, hard_stop: hard_stop}
  return __agent_stall_emit_diagnostic_trip(
    session_id,
    config,
    tripped,
    iteration,
    "flailing_action",
    trips,
    config.hard_stop_after_trips,
    defer_feedback,
  )
}

pub fn __agent_stall_observe_monologue_actuation(
  observation: AgentStallObservation,
) -> AgentStallObservation {
  return observation
    + {
    state: observation.state
      + {
      monologue_actuation: agent_monologue_actuation_observe(
        observation.state.monologue_actuation,
        observation.config.monologue_actuation,
        to_string(observation.warning?.pattern ?? ""),
      ),
    },
  }
}

/**
 * Fold one turn through stall, repair, action, verification, and optional
 * actuation detectors. `turn_made_edit` describes the prior dispatch;
 * `payload` reaches configured host fact callbacks only. Completed-dispatch
 * actuation credit is owned by the loop after tool results exist.
 *
 * @effects: [agent]
 * @errors: [agent_loop]
 * @api_stability: experimental
 */
pub fn agent_stall_observe_tool_calls(
  session_id: string,
  tool_calls: list,
  iteration: int,
  raw_config,
  state: AgentStallState,
  defer_feedback: bool,
  prev_dispatch = nil,
  turn_text = "",
  had_parse_errors = false,
  turn_made_edit = false,
  turn_stop_reason = "",
  payload = nil,
) -> AgentStallObservation {
  const observation = __agent_stall_observe_core(
    session_id,
    tool_calls,
    iteration,
    raw_config,
    state,
    defer_feedback,
    prev_dispatch,
    turn_text,
    had_parse_errors,
    turn_stop_reason,
  )
  if !observation.enabled {
    return observation
  }
  const repaired = __agent_stall_apply_repair(
    session_id,
    observation.config,
    observation,
    iteration,
    prev_dispatch,
    turn_made_edit,
    defer_feedback,
  )
  // Per-turn action-signal intake (#burin-cutover seam 4). Inert (returns `repaired`
  // unchanged) unless an `action_signal` callback is configured, so the default is
  // byte-identical. Applied after the repair overlay so it respects the one-warning-
  // per-turn discipline.
  const signaled = __agent_stall_apply_action_signal(
    session_id,
    observation.config,
    repaired,
    iteration,
    payload,
    defer_feedback,
  )
  let resolved = signaled
  if observation.config.progress_signal != nil {
    resolved = resolved
      + {
      state: __agent_stall_fold_verify_state(
        resolved.state,
        observation.config,
        payload,
        turn_made_edit,
      ),
    }
  }
  return __agent_stall_observe_monologue_actuation(resolved)
}