shelly-shell 0.5.3

A Rust based Unix style shell with a typed and structured language syntax.
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
# Networking and JSON API design

Status: core implemented and being validated, 2026-10-13. The executable library surface,
initial transport profiles and compatibility changes are recorded in
[the implementation reference](networking-json-api-implementation.md). Schema, Patch,
Merge Patch, canonicalization and numeric comparison remain the later utility layer.

OpenAI is an ordinary application sample in [examples/openai.shy](../examples/openai.shy).
It is **not a standard-library module**. Multi-file Shelly modules use directory/init.shy:
networking starts at [std/net/init.shy](../std/net/init.shy), and HTTP starts at
[std/net/http/init.shy](../std/net/http/init.shy). Neither directory has a conflicting sibling
net.shy or http.shy.

Shelly should support sockets, HTTP, REST, and JSON-RPC through consistent typed values,
explicit byte streams, and libraries that can be extended in Shelly. JSON supplies one
conversion model across direct calls, pipeline boundaries, network bodies, and application
types. Marked error structs, named error unions, exceptions, and call stacks supply the shared
failure model.

This revision resolves the five blocking contracts from the [first hostile review](networking-json-api-review.md).
The [second hostile review](networking-json-api-review-2.md) records the counterexamples tried
against the revised design and corrections made. The contracts below describe the complete
target API, with the initial profiles and executed behavioral coverage in the implementation reference.

The usability criterion is simple: ordinary calls return the useful value or enter a catch.
JSON conversion protocols handle representation, named unions organize errors, and operation
leases handle pipeline cleanup. A workflow should contain substantially more useful work than
error-handling code. No checked-exception boilerplate is introduced.

## Existing foundations and required changes

The current language already supports nominal named types and unions, module privacy,
constructors, conversion factories, mutable instance receivers, closures, and source iterators.
Only the source of a pipeline expands automatically; arrays returned by later stages remain
single items. Bare $ denotes the current pipeline item.

Native process pipelines preserve bytes. Unannotated bytes entering ordinary scripted stages
currently become a String after EOF. The JSON boundary currently buffers a complete document.
The next_item protocol reserves bare () for end of iteration. Custom iteration uses a private
receiver copy, so ordinary field mutations are not a shared stream lifecycle mechanism.

Runtime diagnostics now contain the original Shelly failure location, optional Rust failure
origin, and CallFrame records ordered innermost first. Frames include a call name, Shelly call
location, native flag, and optional native dispatch boundary. This trace is accumulated as
interpreter errors unwind. Script-visible CallStack, normal-return failure capture, marked
errors, and try/catch/finally now build on that trace.

Relevant foundations:

- [Pipeline behavior]../README.md#pipelines.
- [Conversion and structural mapping]../src/language/data/formats/mapping.rs.
- [Current JSON engine]../src/language/data/formats/json/mod.rs.
- [Iteration]../src/language/interpreter/iteration.rs.
- [Call frames]../src/language/interpreter/call_stack.rs.
- [Runtime diagnostics]../src/language/interpreter/interpreter.rs.
- [Call-stack behavioral coverage]../tests/native/call_stacks.shy.

The previous JSON encoder included private struct storage and its decoder used native
numeric types. ABI 1 replaces those defaults with public structural mapping and exact
documents; ordinary materialization explicitly checks native numeric representability.

## Library boundaries

| Layer | Responsibility |
| --- | --- |
| std::io | Bytes, readers, writers, deadlines, transfer and resource leases |
| std::stream | Single-consumer scripted item readers and completion results |
| std::json | Parsing, encoding, exact documents, conversion, framing and validation |
| std::net::dns, tcp, udp, unix, tls | Addresses, sockets, listeners and secure transports |
| std::net::http | HTTP clients, requests, responses, body transport and servers |
| std::net::sse | Server-Sent Events framing over byte readers |
| std::net::rpc | JSON-RPC envelopes, correlation, transports and dispatch |
| Application modules | Endpoint models, authentication choices and service-specific policy |
| Prelude and interpreter | Error obligations, exceptions, CallStack and report_error |

JSON depends on shared I/O, not networking. HTTP and RPC consume the JSON interface.
Rust owns operating-system resources, readiness, TLS and established HTTP protocol engines.
Shelly owns public wrappers, codec policy, framing composition, pagination, RPC envelopes,
routing and application models.

Register implementation primitives as hidden and expose them locally inside their wrappers.
Existing visible declarations can reveal hidden natives from other modules: this is interface
encapsulation, not exclusive access. True owner-only native access requires a separate
module-provenance permission design.

## Marked errors and exception handling

### Error values and named groups

An error struct is an ordinary nominal struct with a failure marker. A **payload** is typed
data; a **failure occurrence** is one activation of that payload, with an origin snapshot and
responsibility token. Caught payloads can be stored as history without becoming new failures.

~~~shy
pub error struct ApiError
{
    pub status: Integer,
    pub message: String,
    pub request_id: String | (),
    pub response: std::json::Document | (),
}

pub type Error = ApiError | std::net::http::Error | std::json::Error | std::stream::Error

pub fn Client::create($request: Request): Response | Error
{
    # Return a response or a marked failure.
}
~~~

Groups organize signatures and catches; recognition follows named wrappers to concrete marked
types. They are not checked exception effects: callbacks can throw other types, and callers
need neither exhaustive catches nor a growing list of checks. One untyped workflow catch is
valid. Ordinary nominal success types retain their existing behavior.

The prelude also supplies ErrorValue, an abstract constraint accepting any concrete marked
payload without wrapping it. It cannot be constructed or structurally decoded; it supports
.type, narrowing and diagnostic inspection, not an assumed common message field. An explicit
ErrorValue scalar parameter/binding is error-inclusive capture. An untyped catch uses this
open constraint, so new library errors do not require changing the prelude signature.

<a id="unhandled-failure-obligations"></a>

### Success consumption and explicit capture

The ordinary path is deliberately short:

~~~shy
try
{
    let $response = $client.get "users/42"
    let $user = $response.require_success.read_as User
    save $user
}
catch ($stack: CallStack, $error)
{
    report_error $stack $error
}
~~~

An inferred binding consumes success. It does not retain a success/error union. Passing to an
any parameter also consumes success: any is not an error-suppression escape hatch.

| Context | Failure behavior |
| --- | --- |
| Inferred binding, success-only annotation or any | Promote an active marked failure before coercion; otherwise validate success normally |
| Ordinary argument, success-only receiver, collection item/key or struct field | Consume success before calling or mutating storage |
| Explicit scalar annotation containing marked alternatives | Capture a compatible pending occurrence |
| Function return admitting that marked alternative | Transfer the root occurrence to the caller |
| Return excluding that error, ordinary pipeline boundary or iteration | Promote before incompatible-success checking or emitting data |
| Bare call, unused temporary or explicit _ discard | Promote an active failure |
| Catch | Acknowledge the entire incoming diagnostic packet before entering the handler |
| Inspection/display of an explicitly captured occurrence | Observe without acknowledging |
| throw, or returning a caught marked payload as the root failure | Propagate/reactivate with original provenance |

~~~shy
let $reply = fetch                    # Reply, or an exception.
let $reply: Reply = fetch             # Same behavior, with a success check.
let $saved: Reply | Error = fetch     # Explicitly captures a pending scalar.
~~~

Only an explicit top-level error-inclusive scalar type captures. An optional error type
qualifies; [Reply | Error] does not. Pending occurrences cannot enter containers or captured
closure cells: insertion promotes before mutation. This avoids tracking responsibility over
arbitrary heap graphs. After catch, immutable diagnostic data can be stored and shared freely:

~~~shy
pub struct Failure
{
    pub stack: CallStack,
    pub error: any,
}

try { operation }
catch ($stack: CallStack, $error)
{
    $failures = [$failures..., Failure(stack: $stack, error: $error)]
}
~~~

Copying passive history does not reactivate it. Explicit throw, root failure return or explicit
conversion into a marked target does. Reading a Failure.error field remains passive. Provenance
is runtime metadata, separate from the user's struct fields.

### Responsibility state machine

Each pending occurrence has a monotonically increasing activation ID, one owning lexical
region, and tracked scalar slots. Regions are function activations, lexical blocks, protected
try bodies and the root submission. A producer's root return transfers into the receiving
region. VM instructions maintain the ledger; Rust Drop and Rc counts never trigger failures.

| Event | Ledger transition |
| --- | --- |
| Construct marked root payload | Create Pending(id, owner, provenance) after successful construction |
| Capture/copy into an allowed local scalar | Add a slot to the same ID; no new occurrence |
| Pass to an explicit error-inclusive parameter | Borrow; responsibility remains with the owner |
| Overwrite a slot | Remove its old reference; if it is the last owned slot, promote before writing |
| Discard or success consumption | Remove occurrence from ledger and throw, even if aliases remain |
| Owner exit | Throw outstanding occurrences owned by that region |
| Compatible root return | Put occurrence in return escrow; transfer only after cleanup succeeds |
| Catch | Acknowledge every occurrence in packet; aliases become passive history |
| Return/throw acknowledged history | Fresh activation retaining original provenance |
| Commit marked error conversion | Retire active source tokens into passive history; outcome owns replacement obligation |

Slots include locals, expression temporaries and borrowed scalar arguments. The VM applies
these transitions at statement, assignment, call and region boundaries without scanning objects.
Copies within the owner or descendants are allowed; writes into ancestor/global/module bindings
or closure-captured bindings promote before mutation. Capturing a binding already holding a
pending occurrence also promotes; later writes to captured cells use that same rule.
Fresh root-submission locals are owned scalar slots even when the REPL preserves their names.
The global-write restriction concerns bindings outside that ownership region, not a fresh
top-level explicit capture. Root catch/exit resolves it before the next submission.

A descendant can observe/borrow a parent's token but cannot transfer its responsibility by
returning it. Its return passes the same borrowed token back; only the owner's eventual root
return transfers ownership. The last owned slot's overwrite promotes even if borrowed aliases
survive. Incidental aliases cannot postpone owner exit.

On owner exit, earliest activation is primary; other tokens attach in activation order as
Unhandled relations. Remove all from ledgers before dispatch. A protected region's catch is
still active during this check. If an exception is already propagating, it stays primary and
outstanding tokens attach instead of replacing it.

A compatible return excludes its escrowed token from owner-exit checks. Local aliases expire
without duplicate promotion. Cleanup failure during that return makes the escrowed failure
primary with cleanup attached. An incompatible return consumes success and throws locally;
nominal wrapping cannot disguise it. No pending token survives a REPL submission.

A field/method whose receiver is explicitly a marked type can inspect a captured occurrence;
.type is observational as well. Access demanding a successful Reply receiver promotes.
Branch/block expression results inherit the receiving context; a root value crosses that
expression region in escrow instead of being discarded at its closing brace.

Raw storage inside a marked type's factory is not activated early. Activation occurs only after
the factory returns a validated marked payload; a factory that fails instead creates that failure.

If owner exit abandons a returned pending occurrence because another token is outstanding,
the escrowed failure is primary and the other tokens attach as Unhandled. If a successful return
is abandoned, earliest outstanding activation is primary. This closes the return-plus-alias case.

These restrictions concern explicit capture. Ordinary network/JSON code requires no pending
locals, acknowledgement calls or boilerplate checks.

### Typechecking and conversion contexts

The compiler and interpreter share four contexts:

| Context | Rule |
| --- | --- |
| ConsumeSuccess | Remove only active marked alternatives before checking expected success; wrong successful types still fail |
| Capture | Preserve compatible error-inclusive scalars and ordinary nominal assignment rules |
| Propagate | Admit declared root errors; preserve concrete payload/occurrence through named return groups |
| CatchFilter | Flatten aliases/groups to concrete marked TypeIDs; never wrap the payload in a nominal group |

An active error incompatible with a requested capture type promotes the original failure;
it does not become an ordinary type-mismatch error. Capture cannot silence an unexpected error.

Catch-filter bindings have the flattened concrete union as their static type. .type is the
concrete type and existing narrowing applies. Clause compilation pins TypeIDs, including across
REPL redefinitions; matching never uses spelling. Ordinary named-group bindings still use
nominal wrapping. This is not a global change to conversion.

ConsumeSuccess projects a callable's mixed success/error result: named wrappers whose
underlying alternatives include marked errors are transparent in that projection. Pure success
nominal types retain their identity. A call returning Result = Reply | Error therefore binds
a successful Reply as Reply; an explicit Result annotation captures/wraps instead. Copying an
already caught passive error is not a fallible call and retains its concrete type. Promotion
depends on active occurrence metadata, never merely on a stored type being marked.

Factories may return their own type (or optional form) plus marked alternatives. Wrong successful
types fail at the factory boundary. Raw construction inside the factory retains existing access
rules. JSON hooks require Document, the target type or Codec as their successful result,
respectively. Native metadata must express the same contracts.

A fallible callback fits a success-only prototype if all non-error alternatives fit; an execution
adapter promotes returned errors. A dynamic any result is checked when it arrives. This does
not claim the callback cannot throw.

### Conversions should remove glue code

Use existing from_<source> factories rather than parallel network conversion callbacks.
A Document uses from_json; ordinary hashes retain key/value and nominal identities for
source-specific selection. json::convert, HTTP read_as and typed JSON pipeline boundaries share
dispatch and strict fallback. Conversion failures enter the surrounding catch automatically.

Error conversion is **explicit**: catch matching never executes conversion code.

~~~shy
pub error struct ServiceError
{
    pub message: String,
}

pub fn ServiceError::from_status_error($error: std::net::http::StatusError): ServiceError
{
    ServiceError(message: "Service request failed: ${error.status}")
}

try { fetch_profile }
catch ($error: std::net::http::StatusError)
{
    throw ServiceError($error)
}
~~~

The normal conversion call selects from_status_error. The converted payload is a new occurrence
with the source attached as Cause automatically; no stack copying or manual cause field is
required. This follows marked source-argument provenance even after catch, including explicit
conversion outside that handler. Multiple marked arguments attach causes in parameter order;
conversion failure retains those sources as causes. Deduplicate by occurrence identity.
Ordinary diagnostic containers do not implicitly cause error conversion.

When an explicit conversion to a marked target commits its returned/escaping failure, replace
any active source obligations with that outcome's token/packet atomically. Source aliases become
passive history; the new outcome owns the obligation and retains source causes. Do not leave
both the old error and its domain replacement independently pending. Before commitment, source
borrows remain unchanged. A failed conversion performs the same replacement with its escaping
failure. Conversion to a non-error target, such as String for display, only observes the source.

Usually a union is sufficient: export ServiceError = DomainError | http::Error | json::Error
when retaining concrete lower-level failures helps callers. Convert when domain meaning changes.

### Try and catch

~~~shy
try
{
    let $response = operation
    consume $response
}
catch ($stack: CallStack, $error: ApiError)
{
    report_error $stack $error
}
catch (_, $error: std::json::Error)
{
    echo $error.message
}
catch ($error: net::Error)
{
    echo $error.message
}
finally
{
    $resource.close
}
~~~

All catches receive a CallStack and concrete error. The shorter forms omit stack bindings;
an untyped error binding catches all exceptions. Match clauses in source order. Groups match
only actual marked exceptions even if their declarations contain non-error alternatives.

A handler acknowledges the packet before executing. A new handler failure attaches the caught
packet as Handling context unless already a cause. Sibling clauses do not catch handler/finally
failures. Rethrow of the same error preserves origin and adds propagation without a self-cause.

try initially is a statement. A compatible explicit return transfers through finally without
entering its local catch; throw enters catch immediately. Parse/compile failures are reported
at the submission boundary and cannot be caught inside the rejected submission.

### Finally and cleanup

Each region leaves with one completion record: Normal, Return(value/escrow), Break(target),
Continue(target), Throw(packet) or Exit(status). Finally runs after the selected catch and
before that record leaves the try. Pipeline/reader unwinding releases operation leases before
the enclosing catch; user finally handles caller-owned persistent resources.

| Original completion | Cleanup | Result |
| --- | --- | --- |
| Normal, successful return or loop exit | Success | Resume original completion |
| Normal or successful return | Failure | Cleanup primary; cancel successful return |
| Throw or returned pending failure in escrow | Failure | Original primary; attach cleanup with its own stack |
| Any | Multiple failures | Preserve first applicable primary; attach others in cleanup order |

Automatic releases run in reverse acquisition order, at most once each including failed release.
Finally is entered at most once and is not restarted after interruption. Output, mutations and
remote effects are not rolled back.

A cleanup guard forbids return/break/continue/exit escaping that guard, including exit inside
a helper. Helpers may return normally and use local loops. Direct escaping syntax fails
compilation; indirect escaping exit raises CleanupControl. Throw is allowed.

Normal and exceptional cleanup share a cumulative **5,000 ms / 1,000,000 VM instruction**
budget per leaving operation, whichever expires first. The outermost unwind/operation-release
creates the budget; nested finally, helper calls and related releases inherit it. Reaching a
catch ends that unwind. Its handler is ordinary code; later normal finally creates a cleanup
budget if no parent cleanup is active. Nested cleanup cannot reset an active budget.
The initial release fixes these budgets; configurable larger normal budgets are deferred.
Native waits share the remaining deadline; back-edge/instruction checks stop scripted loops.
Budget exhaustion skips remaining script cleanup, attaches CleanupTimeout and performs mandatory
native releases. A previously successful completion then becomes a cleanup failure.

First Ctrl+C cancels waits and starts Interrupted unwinding. Cleanup masks only that consumed
cancellation generation, allowing local release without resetting an expired network deadline.
A new Ctrl+C during this unwind produces CleanupAbort, bypasses script catches until root,
skips remaining script cleanup and runs native releases. Original interruption stays primary;
the REPL reports and recovers. First Ctrl+C during otherwise normal finally cancels that
finally; remaining outer cleanup runs in interrupted mode.

close is local release, not TLS close-notify, TCP drain or remote acknowledgement. Graceful
shutdown is a separate ordinary deadline-bound operation. Native releases must not wait for
peers or execute script. Workers dispose of late completions without interpreter access.
Cleanup cannot be guaranteed after SIGKILL or a nonreturning kernel operation; supported waits
must not run uncancellable blocking work on the interpreter thread.

### Call stacks and reporting

Expose immutable CallStack snapshots:

| Field | Meaning |
| --- | --- |
| error_type | Concrete TypeID associated with this diagnostic |
| origin | Original Shelly failure location |
| native_origin | Optional Rust failure location |
| frames | Innermost-first CallFrames, including distinct native_boundary sites |
| propagation | Later throw/conversion sites without rewriting origin frames |
| related | Ordered RelatedFailure records |
| truncated | Whether bounded capture omitted information |

RelatedFailure contains relation (Cause, Cleanup, Unhandled, Handling or Reporting),
error: ErrorSnapshot, and stack: CallStack. ErrorSnapshot exposes original_type: Type,
public_fields: bounded DiagnosticValues, optional formatted preview, and truncated. DiagnosticValue
represents scalars, container previews and opaque resource identities. It is explicitly a
projection, not an invalid partially copied instance of the original struct. Private fields are
not exposed. A snapshot never owns a socket or retains an arbitrarily large source document.

The handler's primary $error remains the actual passive concrete payload. Related originals
are projected when attached while their values are still available; their pinned formatters
may supply bounded previews under the shared formatter budget. Two reporter arguments therefore
provide all diagnostics without ambient "current exception" lookup or retaining whole related
objects. Exact related payloads requiring application retention should be recorded explicitly
while caught; diagnostic previews are not serialization or replay data.

Capture logical task frames at **first activation, before the producing call returns**, including
constructors, closures, accessors, conversions and callbacks. Native failures preserve the actual
Rust origin and separate dispatch boundary of the landed implementation. Scripted errors have
no fabricated Rust origin. Suspended calls retain their invocation frames. Internal invocation
IDs deduplicate capture/unwind merging while preserving recursion with identical names.

Rethrow preserves origin and adds propagation records. Relations form an immutable acyclic
diagnostic tree/DAG: prevent self/ancestor links and deduplicate occurrences. Capture at most
16 related occurrences, depth 4, 128 frames per occurrence and 64 KiB total diagnostic payload
preview. Truncation never discards the primary.

The prelude exports report_error($stack: CallStack, $error: ErrorValue): (). Its explicit
error-inclusive parameter borrows pending occurrences for observation instead of consuming
success like an any parameter would. Overrides must retain this parameter contract.
At activation, pin the creation site's visible
String::from_<concrete_type> factory by TypeID, method version and defining module environment;
do not capture producer-local bindings. Later imports/redefinitions do not change old failures'
default display. Explicit String conversion inside catch uses normal handler lookup.

~~~shy
pub fn String::from_api_error($error: ApiError): String
{
    "OpenAI HTTP ${error.status}: ${error.message}"
}
~~~

The prelude uses a guarded display intrinsic to invoke the pinned factory through its original
module authority. That capability is attached to CallStack, not reconstructed by name lookup
from the prelude. If error_type disagrees with the supplied payload, use structural fallback
rather than invoking an inapplicable factory.

The default reporter invokes that factory, otherwise decomposes public stored fields without
automatically running getters or exposing private fields. Formatting/snapshot work shares a
cumulative 100 ms / 50,000 VM instruction budget per packet, prohibits external commands,
resource operations and module writes, and discards incidental stdout. Exceptions, timeouts and wrong results trigger bounded structural
fallback while preserving the original error.

Print origin, native origin/boundaries, frames, propagation and related stacks to stderr.
Reporting calls never enter original frames. Limit output to 64 KiB with a 200 ms wait budget and one bounded native stderr worker. A failed reporter uses minimal native fallback without recursion. An overridden
root-visible report_error has the same computation guard, plus one explicit bounded stderr-write
capability; it cannot reopen files or call an external printer. Explicit successful reporting
returns () and observes rather than acknowledges a pending occurrence.

Uncaught failures unwind, clean up, report once and halt nonzero. Each REPL submission has an
implicit catch-all, reports, and leaves the REPL usable. Libraries do not print automatically.

### Public error groups

Export concrete marked structs and convenient named groups. The initial taxonomy is:

| Group | Expected members |
| --- | --- |
| io::Error | ReadError, WriteError, OpenError, ClosedError, TimeoutError, CancelledError, LimitError, DecodeError, LeaseError, ShortReadError |
| net::Error | Transport, HTTP, SSE, RPC and framing groups, preserving their concrete members |
| http::Error | StatusError, ProtocolError, TransportError, io::Error, json::Error |
| json::Error | SyntaxError, EncodingError, MappingError, PathError, LimitError, ValidationError |
| sse::Error | FrameError, io::Error, json::Error, http::Error |
| stream::Error | ErrorValue: application callback failures retain their original concrete type |
| rpc::Error | CallError, RequestError, ProtocolError, BatchError, RemoteError, http::Error, json::Error; streamed operations may also propagate io/framing failures |

Language-level Interrupted, CleanupAbort, CleanupTimeout, CleanupControl, ReceiverBusy,
OperationCancelled and UnsupportedSuspension are marked runtime failures with stable categories.
Cancellation control carries an internal uncatchable-during-cleanup flag; ordinary user
construction of an identically named error does not acquire that control authority.

Native interpreter failures also need a script-visible marked RuntimeError representation
that preserves their category and payload, with the existing trace supplied separately.
Specific native/library errors should not be flattened into RuntimeError when already typed.
Each I/O-consuming JSON function declares io::Error as well as json::Error.
The table describes expected library outcomes; user callbacks can propagate additional
concrete marked types and exceptions. A catch-all remains available for those extensions.
Native signature metadata must describe the groups rather than leave these APIs untyped.

## Shared I/O and resource ownership

### Binary data and endpoints

Bytes is immutable binary data. String-to-UTF-8 and byte-to-text conversion are explicit.
Readers and writers have the following conceptual operations:

| Operation | Success |
| --- | --- |
| Reader.read(max_bytes) | Bytes or () at EOF |
| Reader.read_exact(count) | Bytes, or a short-read error containing bytes/count received |
| Reader.read_all(limit_bytes) | Bytes within an explicit or inherited finite limit |
| Reader.read_text(limit_bytes) | Strict decoded text within a finite limit |
| Writer.write_some(bytes) | Number of bytes written |
| Writer.write_all(bytes) | Complete write count |
| Writer.flush \| () |
| close \| (); idempotent release of the resource |
| io::copy(reader, writer, options?) | Transfer count |

Each fallible operation additionally returns its marked io::Error group. max_bytes must be
positive. Empty bytes are data; () is EOF. read_exact 0 completes without reading.
Native interrupted system calls can retry internally, but language interruption cancels.

A ByteSource adapts a reader to pipeline bytes; a ByteSink adapts a writer or operation.
File helpers return explicit adapters: io::read_file, io::write_file and io::utf8.
Opening a file writer does not promise atomic replacement; partial files survive failures.
An eventual atomic-file adapter must specify rename, durability and cleanup separately.

### Shared handles and leases

Socket, reader, writer, listener, body and client copies alias one native resource.
They do not rewind or duplicate a resource. Closing the resource through one alias affects
the other aliases. Native final-reference cleanup is a fallback, never the failure trigger.

An operation lease gives a pipeline one consumer and appropriate read/write exclusivity.
Competing consumers fail explicitly. In-memory documents and their iterators remain ordinary
replayable values. A network iterator is single-use despite iteration's private receiver copy.

Cancellation closes operation-owned resources and releases borrowed leases. A borrowed TCP
writer finishing a pipeline flushes without half-closing the connection. shutdown_write is
explicit. A protocol failure that desynchronizes a borrowed connection marks it unusable.
Client.close releases its pool and cancels outstanding operations; close-time cancellation
and idempotency follow the same at-most-once release rules.

Lease phases are Reserved -> Acquiring -> Active -> Quiescing -> Releasing -> Released.
Failure from any nonterminal phase enters Quiescing, cancels waits, then Releasing. Mark a
lease released before reporting its release error, so finally cannot invoke it twice. Partial
acquisition belongs to the registry even before a public handle exists. Owned and borrowed
leases use distinct release actions; protocol desynchronization still invalidates a borrowed
connection. A return of a streaming server body transfers its native lease into the response
operation before handler cleanup, rather than relying on a surviving incidental alias.

## Pipelines and managed item streams

### Value and byte paths

Typed stages retain ordinary per-item behavior:

~~~shy
let $responses: [std::net::http::Response]
$urls | std::net::http::get | $responses

let $profile: Profile
(std::net::http::get $url).require_success.body json:|:Profile $profile
~~~

Response is one value. Selecting body creates a byte source. Only a source expands
automatically; returning a reader or iterable from a later stage does not splice it into
the pipeline. Explicit format annotations continue to encode a whole source value.

~~~shy
$document |:json external_command
$records | std::json::encode_line | (std::net::http::upload $request)
~~~

The first encodes one document. The second exposes records through the ordinary source
iterator, encodes each as compact JSON plus a newline, and uploads one continuous body.

An upload sink initializes once, receives chunks, finishes at upstream EOF and emits one
Response to the next value boundary. It never sends one request per chunk.

~~~shy
let $response: std::net::http::Response
$document |:json (std::net::http::upload $request) | $response
~~~

Adapters declare whether they accept typed items or native bytes; an ordinary expression
returning a writer is not silently callable. The runtime recognizes explicit adapter
capabilities. Bare $ retains its current incoming-item meaning.

Marked failures stop ordinary stages, cancel resources and propagate independently of
ExecResult. External statuses retain existing aggregation rules. A successful external
stage cannot overwrite a marked failure. Output already emitted cannot be rolled back. Scalar destinations retain existing per-item
assignment, so a catch can observe the last successful assignment. Array destinations collect
privately and commit only after normal exhaustion and successful operation cleanup; a marked
failure leaves the original binding unchanged. This commit does not depend on external exit
success: ExecResult remains separate from data. Collector limits apply before adding each item.

### Scheduling requirements

Choose a **resumable VM with single-thread cooperative tasks**. This is a required interpreter
change; a native handle containing a closure is not the execution mechanism.

Each task saves instruction position, evaluation stack, call frames, lexical/error regions,
handlers, loop state, last_result, pipeline input and redirections/output capture. Heap values
and modules remain shared on one interpreter thread. No Rust worker runs Shelly code.

Native invocation returns Ready(value), Failed(packet), or Wait(wait_spec, owned_resume_state).
Wait releases Rust borrows and suspends the task at that invocation. Readiness or a bounded
worker completion puts it back on the runnable queue. A worker receives owned bytes/OS state,
not Rc language values, binding references or an Interpreter reference. Invocation/generation
IDs discard late completions after cancellation without writing stale receiver state.

Pipeline stages are tasks linked by bounded queues (64 KiB byte queues and 256 item slots,
subject also to the operation's retained-byte limit). Queue capacity suspends producers;
empty queues suspend consumers. Retained-byte accounting counts unique shared backing roots,
not the apparent size of Document views, and rejects an item whose own retained graph exceeds
the operation budget. Apply bounded node accounting before queuing; unknown unaccountable
objects need an explicit adapter instead of pretending they are zero bytes. Native process/HTTP engines continue servicing reads and
writes while script tasks wait, including early upload rejection. Runnable tasks receive
1,024-instruction or 1 ms quanta in fair round-robin order. Nested script calls suspend in
the same task rather than recursively polling a blocked interpreter frame.

Mutable script receivers are pinned to a task across suspension, with their base/index evaluated
once. Another task attempting to mutate that same binding raises ReceiverBusy before mutation;
it does not wait and create a lock cycle. Same-task nested calls are allowed. Shared native
handles have operation leases instead of this receiver lock: closing an alias can cancel a
pending read. Task-local redirections prevent one suspended callback from changing another's
stdin/stdout. Shared global mutations can interleave at suspension points; the API promises
neither parallel script execution nor atomic multi-statement transactions.

Native functions declare bounded Ready work or supported Wait capability. A legacy blocking
native is rejected with UnsupportedSuspension **before invocation** in pipeline, cleanup and
reporting tasks. CPU-heavy codecs yield/check budgets. A bounded worker pool may support an
owned OS operation; mere offloading is insufficient if cancellation would retain unbounded
workers/resources. Admitting an operation requires a bounded cancellation/disposal contract.

| Task state | Event | Next state |
| --- | --- | --- |
| Runnable | Native/queue wait | Waiting(saved continuation) |
| Waiting | Readiness or completion with current generation | Runnable |
| Waiting/Runnable | Failure or cancellation | Unwinding(completion record) |
| Unwinding | Script cleanup needs supported wait | Waiting(cleanup continuation and remaining budget) |
| Unwinding | Cleanup exhausted/complete | Finished; release operation registry |

Acquisition starts left-to-right. Register the source lease **before** evaluating a later sink
factory. Each resource factory registers partial native acquisition before it can fail.
On setup failure, unwind acquired/reserved leases in reverse order, even if a source alias
still exists. Adapter ownership metadata distinguishes owned resources (close) from borrowed
connections (release exclusivity); a still-live borrowed connection is not automatically closed.

The managed native-wait ABI now spans invocation, iteration and pipeline producers.
Legacy process commands retain their supervised synchronous contract and are rejected
before invocation inside managed byte producers. The acceptance traces below remain design
reasoning; the implementation reference lists the scenarios actually executed.

### Managed scripted readers

stream::Reader(next:, result:, close:, context:) creates shared native state over script
callbacks. next returns an item, marked failure or () for EOF. Wrap legitimate null items.
result computes the completion value after next first returns (); close releases resources.

| State | next | result | close |
| --- | --- | --- | --- |
| Open | Run one next callback | NotFinished | Cancel active wait, then close callback |
| Finishing | Busy; no competing callback | Busy | Cancel finishing, then release |
| Exhausted \| () | Cached success \| () after already completed release |
| Failed | Repropagate stored failure | Repropagate stored failure | Only new release failure, never replay original |
| Cancelled | CancelledError | CancelledError | Idempotent () after release |

First EOF enters Finishing: compute result, cache its checked success, then release. Failure of
result/release produces Failed, not a false successful EOF. One pipeline lease has exclusive
next/result consumption; the lease owner and its same-task nested callbacks may use internal
reader operations while unrelated aliases fail. Reentrant invocation of that reader raises ReaderBusy instead of
waiting on itself. Different readers can suspend and progress independently.

Unwind/cancel the active callback before invoking close; callbacks never overlap. Release and
close callbacks run at most once even if they fail. Failure snapshots preserve concrete errors
and CallStack. Repeated explicit next/result access is a new propagation with the same origin.
Repeated close never replays that original failure, avoiding duplicate exceptions in finally.

Native state clears next/result/close closures after release and caches only completion data
or diagnostics. Captured mutable bindings therefore survive receiver copies only while needed.
An operation registry holds release authority independently of reference counts, so cycles
cannot block active-operation cleanup. Explicit close uses that same release authority; the
root registry entry becomes inactive immediately and cannot later close it a second time. New readers/body operations register with their current creation region and the root
submission before publication. Normal region completion transfers still-live entries into its
parent; exceptional region exit cancels/releases its entries before the enclosing catch.
This is a registry transfer, not a search for handles in returned objects. Returned streaming
resources remain in escrow until cleanup succeeds. Thus failure while building a wrapper
after opening its underlying SSE reader closes that reader immediately, even if an alias was
stored elsewhere. Borrowed transport entries release leases rather than persistent connections.

The root registry is the fallback for published but unconsumed operation readers/bodies;
remaining entries are cancelled at script/REPL-submission end.
Live item readers consequently cannot be carried to the next REPL submission. Persistent
clients/connections can; an explicit new reader creates a new operation in that submission.

Typed item read_as rejects target types accepting () unless boxed:true is explicit. In boxed
mode every success is stream::Item(value: mapped_value), including null. If an any-valued hook
unexpectedly returns bare () in unboxed mode, raise MappingError rather than false EOF.
Whole-document HTTP read_as has no iterator sentinel and can return null normally.

The default root registry permits 256 active/unconsumed item readers. Exceeding it raises a
LimitError before acquisition. Persistent resources require close or caller finally;
native handle cycles do not substitute for lifecycle management. This bounds abandoned reader
retention without introducing a tracing heap ownership algorithm.

## JSON module

### Ordinary values and exact documents

| Entry point | Purpose |
| --- | --- |
| json::decode(text, options?) | Parse and materialize ordinary Shelly values |
| json::parse(text, options?) | Parse an exact Document |
| json::wrap(value, options?) | Convert a structured value into a Document |
| json::encode(value, options?) | Encode JSON text |
| json::convert(document_or_value, type, options?) | Checked typed mapping |
| json::decode_as(text, type, options?) | Parse and map into the requested type |
| json::read(reader, options?) | Read one complete Document |
| json::write(value, writer, options?) | Write incrementally |

These operations use one codec engine and return their success or marked JSON/I/O failures.
Strings passed to parse/decode are text, never paths or URLs. Strings passed to wrap/encode
remain JSON strings; already-encoded JSON must be parsed to embed its structure.
A document is one JSON value, including a scalar or a selected subtree, not just an object.

Document is immutable and not implicitly iterable. Its API includes kind, value,
require_string(pointer), require_boolean(pointer), require_integer(pointer),
at(pointer), find(pointer), has(pointer), array_items(pointer), members and number_text.
array_items returns replayable Document views over an existing array.
Wrong kinds and invalid pointers fail; find only treats absent locations as missing.

A present null is a Document of kind Null. A missing find result is bare ().
Converting the null document to ordinary values produces (). Document-wrapped array elements
ensure [1,null,2] visits three elements through the existing iterator sentinel.

Documents preserve exact number spellings and parsed member order, but not original whitespace
or escape spelling. Subtree views share storage; retaining a small view can retain the root.
An explicit detached copy must be available when retention matters.
Re-encoding is not a promise of original bytes for signatures.

### Numeric and syntax policies

Exact parsing stores bounded, validated decimal tokens without eagerly expanding exponents.
json::Number represents such a number when materializing exact values.

| Materialization mode | Policy |
| --- | --- |
| Native | Signed 64-bit Integer when appropriate, otherwise finite Float with documented rounding |
| Exact | Fitting integer tokens become Integer; other number tokens remain json::Number |

Native conversion rejects overflow and underflow to zero; ordinary finite decimal-to-Float
rounding remains deliberate native behavior. Conversion to an integer requires an exact
integral value in range. No numeric mode silently converts a number to a String.

Duplicate names are rejected by default, including names equal after escape decoding.
Explicit FirstWins and LastWins compatibility modes are possible. Default parsing requires
strict UTF-8 and Unicode scalar strings; unpaired surrogates fail. Network writers do not add
a BOM. BOM acceptance, comments and trailing commas require separately named policies/formats.

Exactly one finite document is consumed and trailing non-whitespace input fails. Empty input
is invalid JSON; null is not an empty HTTP body. Object order is not semantic equality.

These choices follow JSON's interoperability constraints while deliberately selecting stricter
duplicate and Unicode policies than its permissive grammar:
[JSON specification](https://www.rfc-editor.org/rfc/rfc8259.html).

### Protocols and codec descriptions

~~~shy
pub fn User::to_json(): std::json::Document | std::json::Error
{
    std::json::wrap ["name": $self.name]
}

pub fn User::from_json($document: std::json::Document): User | std::json::Error
{
    User(name: $document.require_string "/name")
}
~~~

Encoding calls to_json on an isolated value receiver. Shared resource side effects cannot be
undone by that copy; custom codecs must avoid them. Decoding from_json is a factory without
$self. Required success types are checked; additional return alternatives must be marked.

Dispatch is directional: explicit to_json/from_json, then a type-level json_codec(): Codec,
then introspection. One custom direction need not override the other. Hook failures do not
trigger fallback. Document and Number are engine representations, not recursive user hooks.

A codec descriptor uses TypeIDs and immutable policy: enum wire names; struct renames,
omitted-null fields and unknown-field policy; reversible key mappings. Validate field references,
duplicate wire names, accessibility and type compatibility before reading a body. Pin provider
and method versions per operation. Providers run without resource operations or mutable module
writes, under a 100 ms / 100,000-instruction guard; recursive descriptor requests fail clearly.
Descriptors cannot grant access to private fields.

json_codec is a new receiver-free type-level provider. The interpreter supplies checked metadata
and caller-bound invocation capabilities; it does not give the JSON module privileged constructor
access merely because it possesses a Type object.

### Structural mapping

| Shelly type | Default JSON shape |
| --- | --- |
| String, Boolean, Integer, finite Float, () | Corresponding scalar or null |
| Array/fixed array | Recursive array; fixed lengths checked |
| String-keyed hash | Recursive object |
| Unit enum | Variant-name String |
| Enum-keyed hash | Variant names as keys; decode restores requested enum type |
| Struct | Public stored fields; decoding uses the checked construction interface |
| Named type | Underlying wire shape; typed conversion restores identity |
| Optional | Value or null |
| Union | One unambiguous compatible success member |

Hashes map directly to structs. Detect key collisions after enum renames/conversion. Other
non-string keys require an explicit reversible key codec or pairs representation, never a
display-string conversion. Strict fallback does not turn strings into numbers/booleans.
Required omissions fail; declared defaults/optional omissions apply only when missing; explicit
null is checked separately. Unknown fields fail unless a policy explicitly ignores them.

Selection must preserve ordinary source identity:

| Input to json::convert | Dispatch |
| --- | --- |
| Document | Target from_json; else target codec; else strict structural mapping |
| Ordinary typed value | Exact target from_<original_source_type>; else target from_json after wrap; else codec after wrap; else direct strict structural mapping |

No encode/parse round trip is needed. Named values and typed empty collections select their
original source factory before normalization. A custom factory controls its representation;
the engine validates the successful result and never retries another factory after failure.
Ordinary implicit field coercion does not parse a String as JSON.

Policy precedence is explicit operation option, then that target type's codec, then engine
default. Explicit options propagate to nested mapping; each nested codec supplies unspecified
options for its own type. A factory's internal explicit call is a new policy decision, but
inherits the operation's remaining hard budgets. No option bypasses access checks, constructor
invariants or runtime ceilings. Limits compose by minimum, never by resetting nested budgets.

Select union candidates with pure metadata/shape checks **before** invoking constructors.
If more than one candidate fits, or selection would require trying arbitrary custom factories,
raise AmbiguousUnion. Use an explicit enclosing from_json with a discriminator instead.
Invoke the selected constructor once. No speculative conversion side effects are allowed.

Introspection supplies public storage shape and accessible constructor parameter metadata.
Invoke construction under the original conversion caller's authority; neither a codec nor the
JSON wrapper grants private constructor access. Public computed getters need explicit hooks.
Without a factory, derived round trips work for compatible public fields. With a factory,
wire keys name its declared parameters. A different constructor/storage shape requires a hook
for a round trip. Unsupported private representations raise NoPublicRepresentation; a truly
empty public struct still encodes {}.

Conversion to a marked target activates a failure; objects merely containing an error field
do not. Passive recorded error snapshots may be encoded as public data, but decoding directly
into an error type is an explicit reactivation.

### Runtime Type results

json::convert(input, target: Type, options?) returns any | ErrorValue. The engine's own expected
failures are json::Error; a marked target or custom factory can return another concrete error.
This dynamic error outcome remains capturable, rather than being disguised as any success.
decode_as/read_as use the same dynamic result contract plus documented transport failures.
Before returning success, the conversion engine validates the result
against the requested TypeID, including factory output and nominal wrappers. Wrong hook results
become MappingError at the conversion boundary with the offending factory location, not an
apparently successful any value. The VM distinguishes generated factory-result contract
violations from user-thrown exceptions: JSON mapping wraps only the former as MappingError with
the contract failure as cause. Arbitrary callback exceptions propagate unchanged.

Literal Type arguments may enable compiler specialization; the contract does not require
dependent types. A success-only binding still checks the requested ordinary type. Typed JSON
pipeline destinations use this same engine. No API requires callers to annotate every binding
to trigger error promotion.

### Streaming JSON and limits

| Reader or adapter | Framing |
| --- | --- |
| json::read | One complete document |
| json::read_array(reader, pointer, options?) | Selected array elements as Documents |
| json::read_lines | One compact JSON document per physical line |
| json::read_sequence | Record-separator framed JSON texts |
| json::events | Structural events |
| json::array_source(items, options?) | One incrementally encoded JSON array |
| json::encode_line | One compact record plus LF |

Transport chunks are not records. Tokens may span chunks inside UTF-8, strings, escapes
and numbers. Decoder.feed/poll/finish distinguishes data, NeedInput and end. The initial single-document
Decoder retains bounded bytes and publishes a document after explicit finish. read_array
is the incremental selected-item adapter; events traverses an already parsed Document in
document member order. Neither API treats a transport chunk as a complete record.

JSON Lines rejects blank lines by default, accepts CRLF and a complete final line without LF.
Pretty multi-line JSON is not a line record. JSON Text Sequences use RS framing and expose
record errors explicitly; recovery never silently skips a failed record.
[Sequence framing](https://www.rfc-editor.org/rfc/rfc7464.html).

read_array emits elements before whole-document validity is known. Normal exhaustion validates
the remainder and trailing input. Early close cancels without claiming validity; an explicit
drain-and-validate operation is separate. Discarded subtrees are still syntax/duplicate checked.

The initial public limit profile is concrete and configurable:

| Budget | Default |
| --- | --- |
| Materialized document / encoded single document | 16 MiB |
| Framed record | 1 MiB |
| Nesting | 128 |
| String/scalar bytes | 1 MiB |
| Members/elements in one document | 100,000 total |
| Number token | 1,024 bytes; exponent magnitude at most 10,000 |
| Numeric comparison/canonicalization work | 1,000,000 work units |
| Custom hook work per operation | 5,000,000 VM instructions, 5,000 ms plus any earlier I/O deadline |
| Stream retained buffers | 16 MiB; individual pipeline queues remain smaller |

These are design defaults rather than benchmark claims. Runtime ceilings and explicit lower
limits compose by minimum. Framed streaming is bounded per record and retained working set,
not by silently buffering the whole response; a caller can add total byte/item/time limits.
Decoder work checks input-byte, member and depth budgets during discarded-subtree validation
too. Exact exponents cannot trigger unbounded expansion. Custom hooks suspend/check instruction
budgets, and recursion tracks (direction, TypeID, source identity); a recursive cycle fails while
distinct nested values are allowed within depth/work limits.

Document.detach copies the selected subtree under the same size limits. This releases the
original root once its other views expire. Diagnostic captures detach bounded previews rather
than retaining arbitrarily large document roots.

Array encoding emits [] for empty input. Interrupted encoders fail and may leave partial
bytes. Streaming writers cannot retroactively retract earlier output.

Pointer navigation, immutable set/remove/append, JSON Patch, Merge Patch and deterministic
diff form a later utility layer. Patch application is atomic over immutable documents.
Schema validation is separate from conversion; target a declared dialect, expose bounded issues,
do not coerce or insert defaults, and resolve references explicitly.
Canonicalization is a separate standards-based operation, not merely sorted keys; unsupported
exact numeric representations must fail rather than silently round.

## Networking module

### Transports and framing

TCP connections expose reader, writer, read/write operations, explicit shutdown_write and
close. Listeners expose accept and an explicit connection iterator.
Unix streams follow the same byte contract. TLS wraps a supported transport, validates
certificates and names by default, and exposes explicit trust configuration.

~~~shy
let $connection = std::net::tcp::connect "example.com" 9000
std::io::read_file "payload.bin" | $connection.writer
$connection.shutdown_write
$connection.reader | (std::io::write_file "reply.bin")
~~~

UDP preserves Datagram boundaries, peer addresses and truncation information. Empty datagrams
are valid. receive returns a Datagram or error; it does not treat an empty packet as EOF.
Low-level platform options stay transport-specific rather than leaking into every HTTP call.

Delimited text, fixed records, length-prefixed frames and custom codecs are reusable framing
adapters. Define maximum frame sizes, byte order, prefix widths and truncation handling.
A frame decoder retains partial input and preserves extra bytes after a completed frame.

### HTTP values and bodies

Request is inert configuration: method, path, ordered query pairs, headers, Body and
operation overrides. Client contains configuration and a shared connection pool; ordinary
methods retain Shelly's mutable-receiver semantics. Use one configured client for related
requests. Pool-sharing configuration forks are outside the initial public surface.

Response contains status, repeated headers, effective URL and one body resource.
The initial client exposes the effective URL; redirect-history inspection is deferred.
send/get/post return when final response headers are available. Body failure may occur later.
Redirect/retry configuration determines which exchanges are final and remains bounded.

~~~shy
let $api = std::net::http::Client(
    base_url: "https://example.com/api/",
    auth: std::net::http::bearer $TOKEN,
    timeout_ms: 30000,
)

let $response = $api.post "users" (std::net::http::json_body $user)
let $created: User = $response.require_success.read_as User
~~~

Body constructors explicitly select bytes, UTF-8 text, JSON, form, multipart or a reader.
json_body uses json::wrap/encoding and supplies application/json.
A conflicting caller content type fails unless an explicit override is selected.

Headers preserve ordered repeated fields with case-insensitive lookup. get requires a
singleton, get_all returns all matching values, and absent get returns ().
Do not combine fields such as Set-Cookie indiscriminately.
[HTTP field semantics](https://www.rfc-editor.org/rfc/rfc9110.html#section-5.2).

read_json, read_document, read_as and bytes/text reads consume the same body.
A second consumption fails. Partial manual reads followed by a whole-body decoder fail;
an explicit remaining-reader operation can decode just the unread tail.
A bounded buffer operation creates a replayable body, distinct from a live reader.
Whole-body read/decode operations own a registered consumption lease and release the body on
success or failure. A failed decoder cannot leak a half-consumed body into the pool.

Status is data until require_success checks it. Successful status means 2xx and the check
returns the same Response without consuming its body. No-content bodies are not JSON null.

Failed require_success atomically reserves an exclusive diagnostic lease. Capture at most
64 KiB under min(remaining operation deadline, 500 ms); an expired deadline permits no new
network read. Read one bounded lookahead byte when budget permits to distinguish an exact-limit
complete body from a truncated prefix. Close the live body; discard its pooled connection if
completion/reusability is uncertain. Aliases cannot race this read or subsequently consume it.

The target-type error decoder receives **StatusError** containing a DiagnosticResponse, not a
live Response. The replayable DiagnosticResponse contains status,
headers, URL/exchange metadata, delivery state, detached Bytes prefix, bytes received and
capture_complete/capture_reason. Reads are replayable. read_document refuses an incomplete
capture even if its prefix happens to be valid JSON. Bytes remain available after decoder
failure, without retaining a live socket.

error_decoder: DomainError selects DomainError::from_status_error using the ordinary conversion
protocol; its marked target is the accepted replacement type. This preserves source cause
provenance automatically. A compatible target result becomes primary with StatusError as Cause.
Resolve and pin the decoder capability at client configuration under that caller's authority;
possession of a Type does not grant access to a private factory. A wrapper can configure its
own private conversion factory without making it public to the HTTP module or application.
A decoder throwing/returning a decoding failure leaves StatusError
primary, with the decoder failure related. Its callback return signature must identify the
domain error alternatives accepted as replacements; an arbitrary json::Error is not mistaken
for a successfully decoded domain failure. A successful non-error decoder result is a contract
failure attached to StatusError. StatusError always retains the bounded DiagnosticResponse.

The default URL client accepts relative references under its base and rejects credential-bearing
cross-origin requests unless explicitly enabled. Redirects strip origin-bound credentials across
origins, reject HTTPS downgrade by default, have a finite hop limit and validate every target.
Provide path-segment and query constructors; never interpolate unescaped input into URLs.

Headers/body aliases share resource state. The default client permits 64 open response bodies;
capacity exhaustion raises LimitError before sending another request rather than waiting forever
while a caller is collecting unconsumed Responses. Pool connection limits are separate.
Client.close cancels active operations and releases its pool. Ordinary one-shot calls must keep their owning transport alive through body consumption.

### Deadlines and delivery uncertainty

Use timeout_ms for one total operation budget and separate connect_timeout_ms and
idle_timeout_ms. Capture a monotonic deadline at operation start; redirects, retries, upload,
body reads and framing share the remaining budget. A later body read does not reset it.

Unbounded total duration requires explicit TimeoutPolicy::Unbounded configuration, with a finite idle policy retained
unless explicitly changed. Deadline behavior across client pooling and pagination is explicit:
each request has its own budget; a traversal can additionally have an overall budget.

Automatic retries are disabled initially. An explicit policy checks idempotence, delivery state,
replayability and remaining budget. One-shot bodies cannot be resent without a replay factory.
A timeout after transmission can leave delivery unknown; retain that fact in the error.
Do not automatically retry non-idempotent requests merely because no reply arrived.
[Retry semantics](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2).

Early HTTP responses during upload need concurrent protocol progress. Stop upstream production
on terminal rejection and preserve the response. Unread bodies are drained only within a
bounded policy; otherwise discard the pooled connection.

### REST and servers

REST helpers compose HTTP status checking, JSON conversion, typed endpoint functions and
service-specific error decoders. They do not duplicate parsers or mapping rules.

A paginator receives a Shelly callback that produces items and the next Request.
pages emits page records; items explicitly flattens their items. Fetch lazily, detect
repeated next links/cursors, enforce request/page limits and stop on cancellation.

HTTP servers use the same request/response/body objects, with a separate request-target policy.
Start with serialized Shelly handlers and bounded native connection queues. Handler exceptions
produce an appropriate server failure and run cleanup; exceptions are not serialized to peers
by default. Returning a streaming body retains its resource lease through transmission.
Concurrency of Shelly handlers and general async execution remain separate designs.

### SSE and JSON-RPC

SSE framing follows the [HTML event-stream parser](https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream),
with a deliberately strict UTF-8 profile. Accept LF, CRLF and lone CR; ignore one leading BOM;
join data lines with LF and remove the final joining LF. Comments and unknown fields do not emit
events. Dispatch only at a blank line with data. EOF discards an unterminated pending event;
it never supplies the missing blank delimiter. IDs containing NUL are ignored, retry accepts
ASCII digits only, and empty data lines still contribute a data field.

sse::read over an HTTP body checks text/event-stream before parsing; explicitly supplied raw
readers require the caller to choose SSE framing. Defaults: 1 MiB event, 16 KiB physical line,
16 KiB combined id/event/retry metadata. A malformed/truncated UTF-8 sequence fails rather than
using browser-style replacement. No automatic reconnect/replay, especially for generation POSTs.
Event contains data text, event name, id and optional retry; it does not interpret JSON.
read_as(Type) composes event-data parsing and typed conversion without changing framing.

RPC accepts HTTP, framed stream or a scripted exchange transport. call, notify, exchange and
batch are distinct; a pipeline of calls is not a batch. Use exact Documents to distinguish
missing id/result/error and valid null. Generated request IDs are strings. Correlation compares
JSON kind plus exact value, never display text. Version must be "2.0"; params must be an object
or array if present; replies have exactly one of result/error. Error code is an integer, message
a String, data optional. A null result is a successful Document, not iterator EOF.

| Exchange | Outcome |
| --- | --- |
| Empty batch | Local RequestError before transmission |
| call | Exactly one correlated reply or ProtocolError |
| notify | No RPC response promised; transport acceptance is not remote success |
| Mixed batch | Match every call exactly once; notifications have no response slots |
| Notification-only HTTP batch | Accept successful empty body; any nonempty RPC reply is ProtocolError |
| Duplicate, unexpected or missing reply ID | ProtocolError, preserving bounded response evidence |
| Invalid-batch single error with id:null | BatchError for the batch operation, not a missing-array error |
| Stream transport | Must supply explicit message framing; TCP chunks are not envelopes |

A scripted exchange transport declares whether notify completes without a reply or has an
explicit protocol acknowledgement; no generic read-until-reply is inserted. RPC server
dispatch implements the specification's invalid request/empty batch behavior separately from
client correlation. JSON-level syntax failures, envelope failures, transport failures and
valid remote errors remain distinct.

RemoteError retains code, message and optional data. BatchError, ProtocolError and RemoteError
join the public RPC union. Retry requires method semantics; HTTP POST alone cannot establish
safe replay. [JSON-RPC specification](https://www.jsonrpc.org/specification).

## OpenAI module example

The runnable sample is [examples/openai.shy](../examples/openai.shy), imported as openai
from an application's module search path. The excerpts below show its models and composition;
the source file is authoritative. It is not bundled as std::openai and tests make no live API calls.
Model selection remains a caller argument. The example supports text requests and preserves
the full response for other output kinds. It uses Responses with bearer authentication.

OpenAI output is heterogeneous, so collect text from all message content items rather than
assuming output[0].content[0]. [Response output](https://developers.openai.com/api/docs/guides/text).
HTTP request IDs use x-request-id. [Request metadata](https://developers.openai.com/api/reference/overview).
SSE terminal events distinguish completed, failed and incomplete responses.
[Event contract](https://developers.openai.com/api/reference/resources/responses/streaming-events).

### Module models and conversion

~~~shy
# examples/openai.shy (ordinary application module)
# Responses client. The model remains a caller choice; no live requests occur on import.
import std::io
import std::json
import std::stream
import std::net::http
import std::net::sse

pub enum Role { Developer, User, Assistant }

pub fn Role::json_codec(): std::json::Codec
{
    std::json::enum_codec Role [
        Role::Developer: "developer",
        Role::User: "user",
        Role::Assistant: "assistant",
    ]
}

pub struct Message
{
    pub role: Role,
    pub content: String,
}

pub struct Request
{
    pub model: String,
    pub input: String | [Message],
    pub instructions: String | (),
    pub max_output_tokens: Integer | (),
    pub store: Boolean | (),
}

pub fn Request::json_codec(): std::json::Codec
{
    std::json::struct_codec Request std::json::StructOptions(
        omit_null_fields: ["instructions", "max_output_tokens", "store"],
    )
}

pub struct Usage
{
    pub input_tokens: Integer,
    pub output_tokens: Integer,
    pub total_tokens: Integer,
}

struct Envelope
{
    pub id: String,
    pub status: String,
    pub usage: Usage | (),
}

pub struct Response
{
    pub id: String,
    pub status: String,
    pub usage: Usage | (),
    pub request_id: String | (),
    pub raw: std::json::Document,
}

pub fn Response::from_json($document: std::json::Document): Response | std::json::Error
{
    let $envelope: Envelope = std::json::convert $document Envelope (std::json::MappingOptions(unknown_fields: std::json::UnknownFields::Ignore))

    Response(
        id: $envelope.id,
        status: $envelope.status,
        usage: $envelope.usage,
        raw: $document,
    )
}

pub fn Response::to_json(): std::json::Document
{
    $self.raw
}

pub enum ResponseFailure { Incomplete, Failed, Refused, NoText, Protocol, Truncated }

pub error struct ApiError
{
    pub status: Integer,
    pub message: String,
    pub request_id: String | (),
    pub response: std::json::Document,
}

pub error struct ResponseError
{
    pub what: ResponseFailure,
    pub message: String,
    pub request_id: String | (),
    pub response: std::json::Document | (),
}

pub type Error = ApiError | ResponseError | std::net::http::Error | std::json::Error | std::net::sse::Error | std::stream::Error

pub fn String::from_api_error($error: ApiError): String
{
    "OpenAI HTTP ${error.status}: ${error.message}"
}

pub fn String::from_response_error($error: ResponseError): String
{
    "OpenAI ${error.what}: ${error.message}"
}

# Return the marked value to transfer it to the caller's success-only context.
fn response_failure($what: ResponseFailure, $message: String,
                    $request_id: String | (),
                    $document: std::json::Document | ()): ResponseError
{
    ResponseError(
        what: $what,
        message: $message,
        request_id: $request_id,
        response: $document,
    )
}

pub fn Response::require_text(): String | ResponseError | std::json::Error
{
    if $self.status != "completed"
    {
        let $what = if $self.status == "incomplete"
            { ResponseFailure::Incomplete }
            else { ResponseFailure::Failed }

        return (response_failure $what "Response did not complete" $self.request_id $self.raw)
    }

    let $text = ""
    for $item in $self.raw.array_items "/output"
    {
        if ($item.require_string "/type") != "message" { continue }

        for $part in $item.array_items "/content"
        {
            match ($part.require_string "/type")
            {
                "output_text" => { $text = $text + ($part.require_string "/text") },
                "refusal" =>
                {
                    return (response_failure ResponseFailure::Refused ($part.require_string "/refusal") $self.request_id $self.raw)
                },
                _ => {},
            }
        }
    }

    if $text == ""
    {
        return (response_failure ResponseFailure::NoText "Response contains no text output" $self.request_id $self.raw)
    }

    $text
}

fn ApiError::from_status_error($error: std::net::http::StatusError): ApiError | std::json::Error | std::net::http::Error
{
    let $reply = $error.response
    let $document = $reply.read_document

    ApiError(
        status: $reply.status,
        message: ($document.require_string "/error/message"),
        request_id: ($reply.headers.get "x-request-id"),
        response: $document,
    )
}

~~~

Response::from_json maps routine fields through introspection while retaining unknown output.
The lower-level create operation returns an inspectable Response for incomplete results,
tool-only output and refusals. require_text is an explicit application policy.

### HTTP integration

~~~shy
pub struct Client
{
    transport: std::net::http::Client,
    model: String,
}

pub fn Client::new($api_key: String, $model: String, $base_url: String | ()): Client | Error
{
    Client(
        transport: std::net::http::Client(
            base_url: (if $base_url == () { "https://api.openai.com/v1/" } else { $base_url }),
            auth: (std::net::http::bearer $api_key),
            timeout_ms: 120000,
            error_decoder: ApiError,
        ),
        model: $model,
    )
}

pub fn Client::create($request: Request): Response | Error
{
    let $reply: std::net::http::Response = $self.::transport.post "responses" (std::net::http::json_body $request)

    let $response: Response = $reply.require_success.read_as Response
    $response.request_id = $reply.headers.get "x-request-id"
    $response
}

pub fn Client::text($input: String, $instructions: String | ()): String | Error
{
    let $response: Response = $self.create Request(
        model: $self.::model,
        input: $input,
        instructions: $instructions,
        store: false,
    )

    $response.require_text
}

pub fn Client::close()
{
    $self.::transport.close
}

~~~

error_decoder: ApiError selects ApiError::from_status_error through the conversion
protocol. The target type supplies the replacement-error contract; thrown/returned JSON or
body failures remain decoder failures. A callback overload must declare the same target type
and accept the same StatusError source.
Client::new uses the factory-return rule defined above; this extends current constructors.
Callback-specific failures can be thrown despite generic HTTP signatures; this example's
exported group names its known API, HTTP, JSON and stream failures.

### Streaming integration

Expose two operations: events returns typed Events for tools/multiple channels; stream_text
is a convenience for one output-text channel. It rejects a second text channel rather than
flattening interleaved deltas in an order that differs from the completed response. create and
require_text still support aggregation of completed message content.

Use the conversion protocol at the event boundary:

~~~shy
pub struct Event
{
    pub kind: String,
    pub sequence: Integer,
    pub raw: std::json::Document,
}
pub fn Event::from_json($document: std::json::Document): Event | std::json::Error
{
    Event(kind: ($document.require_string "/type"),
        sequence: ($document.require_integer "/sequence_number"), raw: $document)
}

~~~

TextSession is module-private application protocol state, implemented in Shelly, not a proposed
OpenAI-aware Rust native. Its method contracts are part of this design exercise:

| Method | Contract |
| --- | --- |
| TextSession(request_id:) | Start AwaitCreated with no selected channel and an empty accumulator |
| accept(Event): String \| () or Error | Apply the transitions below; return a delta or record a terminal transition |
| next(events): String \| () or Error | Advance events until a delta/terminal; EOF before terminal raises Truncated |
| result(): Response or ResponseError | Return cached validated completion; NotFinished otherwise |

A successful completed transition stores the terminal Response. next subsequently returns
unit and result returns the cached response. An empty delta String remains real data.

| State/input | Validation and transition |
| --- | --- |
| Any nonterminal event | Nonnegative, strictly increasing sequence; reject duplicates/backtracking; gaps are allowed |
| AwaitCreated + response.created | Bind nonempty response.id; enter Active; duplicate creation fails |
| Before creation, other than error | Protocol failure before emission |
| output_item.added | Register bounded output_index -> item.id/type; duplicate index/id fails |
| content_part.added with output_text | Register the first (item.id, output_index, content_index); require known message item, empty initial text and no prior text channel |
| output_text.delta | Require exact registered tuple, open part and String delta; append under text limit, then emit delta |
| output_text.done | Require same tuple and text exactly equal to accumulated deltas; mark part text-done; duplicates fail |
| content_part.done / output_item.done | For selected text, require matching tuple/item and identical text; advance part/item lifecycle; reject late deltas/reopens |
| response.in_progress and other response-bearing events | Response ID must match; no second response can silently reuse this stream |
| response.completed | Matching response ID/status, selected part/item done, completed require_text equal to emitted accumulator; cache Response and finish |
| response.failed / response.incomplete / error | Raise typed failure; preserve request ID and bounded available response/event evidence |
| EOF before terminal | Truncated, including unterminated SSE terminal data |
| Known nontext/unknown extension event | Check sequence and any carried response ID; ignore only its nontext payload; never emit it |

For terminal events check kind/status consistency first. Refusal and no-text policy comes from
Response.require_text. Completed text is reconciled against the **whole** terminal aggregate,
so an extra text channel cannot be omitted while reporting success. Indexes are nonnegative;
the selected tuple must originate in added events, not merely first appear in a delta.
A malformed required field raises json::Error; a valid but contradictory event raises
ResponseFailure::Protocol. These are wire validation rules, not compiler appeasement checks.

Session limits are 256 output items, 32,768 events, 1 MiB accumulated UTF-8 text and 64 MiB
total decoded event bytes, composed with SSE/JSON limits and the request deadline. Reject before
emitting the chunk that crosses a limit. Validate unknown-event counts/sequence too. The first
valid terminal defines the end of this operation; close the transport rather than wait for a
possibly indefinite EOF. Data after that terminal is not consumed or claimed to be validated.

The pipeline-facing method stays small. TextSession implements the table above; its full SDK
implementation is intentionally outside this design-only change.

~~~shy
pub fn text_stream($events: std::stream::Reader, $request_id: String | ()): std::stream::Reader | ErrorValue
{
    let $session = TextSession(request_id: $request_id)
    std::stream::Reader(
        next: fn (): String | () | Error { $session.next $events },
        result: fn (): Response | Error { $session.result },
        close: fn (): () | ErrorValue { $events.close },
    )
}
pub fn Client::events($request: Request): std::stream::Reader | ErrorValue
{
    let $body = std::json::set (std::json::wrap $request) "/stream" true
    let $reply = $self.::transport.post "responses" (std::net::http::json_body $body)
    $reply.require_success
    (std::net::sse::read $reply).read_as Event
}
pub fn Client::stream_text($request: Request): std::stream::Reader | ErrorValue
{
    let $body = std::json::set (std::json::wrap $request) "/stream" true
    let $reply = $self.::transport.post "responses" (std::net::http::json_body $body)
    $reply.require_success
    let $events = (std::net::sse::read $reply).read_as Event
    text_stream $events ($reply.headers.get "x-request-id")
}
~~~

read_as invokes Event::from_json automatically and transfers the underlying reader's
single-consumer lease. Mapping failure closes the source. Reader construction/acquisition is
registered before a later adapter can fail. Failed completion preserves already emitted bytes,
but .result never labels them a completed successful answer.

### Caller usage and reporting

~~~shy
import openai
import std::io

type WorkflowError = openai::Error | std::io::Error

let $ai = openai::Client(api_key: $OPENAI_API_KEY, model: $OPENAI_MODEL)

try
{
    let $answers: [String]
    ["Explain TCP.", "Explain UDP."] |
        $ai.text $ "Use one sentence." | $answers

    let $chunks = $ai.stream_text openai::Request(
        model: $OPENAI_MODEL,
        input: "Explain structured shell pipelines.",
        store: false,
    )
    $chunks | std::io::utf8 | (std::io::write_file "answer.txt")
    let $response = $chunks.result
    echo $response.id
}
catch ($stack: CallStack, $error: WorkflowError)
{
    report_error $stack $error
}
finally
{
    $ai.close
}
~~~

The pipeline acquires/owns the source operation before opening the sink, closes it on failure,
and releases it after completion. A second chunks.close finally is unnecessary; its cached
result remains available. Only the persistent client needs caller cleanup. Client construction
before try cannot make finally touch an unconstructed value; construction failures reach the
top-level reporter. Callback/runtime failures outside WorkflowError also reach that reporter.

For a script that needs no recovery, the caller can omit catch entirely and let the implicit
boundary report. Error handling remains a small fraction of the workflow. Adding a request,
field, conversion hook or output destination must not require a new per-call result check.

## Compatibility and implementation sequence

The design choices D1-D7 are now fixed contracts rather than unspecified implementation gates:

| Former gate | Resolution |
| --- | --- |
| D1 Responsibility | VM region ledger, scalar capture restrictions, explicit escrow/acknowledgement |
| D2 Type rules | ConsumeSuccess/Capture/Propagate/CatchFilter; checked factories and runtime Type results |
| D3 Trace/display | Activation-time frames; pinned formatter; explicit bounded related snapshots |
| D4 Cleanup | Completion records, cumulative budgets, cancellation generations and at-most-once release |
| D5 Streaming execution | Resumable single-thread VM tasks, native Wait ABI, bounded queues/registries |
| D6 Codecs | Original-source precedence, caller-bound metadata, pure union selection and ABI migration |
| D7 Protocols | DiagnosticResponse ownership, pinned SSE/RPC profiles and OpenAI TextSession transitions |

Implement foundations before public modules:

1. Language errors/contexts/regions, CallStack exposure, exception completion records and reporting.
2. Resumable invocation/iteration/redirection, native wait capability, cancellation and lease registry.
3. Exact JSON, strict conversion, checked metadata and codec protocols.
4. Transport and HTTP engines with local deterministic fixtures.
5. Framed JSON, SSE/RPC, pagination and application modules.
6. Schema, patch and canonicalization utilities after core behavior is established.

This is a coordinated compatibility release, **JSON protocol ABI 1**. String::from_json previously
encoded JSON text; that meaning is removed in this ABI, not selected by import order. Encoding
moves to json::encode/to_json. Every from_json hook must take exactly one Document parameter and
return its target or marked errors; old broad encoder signatures fail module validation with a
migration diagnostic. Move existing scripted String::from_json encoder overrides to to_json
or an explicit encoding function; a new String::from_json must decode a JSON string document.

The executable uses ABI 1 for its in-memory programs and format references. No serialized
compiled-program format is introduced here; future persistence must stamp and validate ABI versions.
A REPL cannot switch ABI mid-session. Within ABI 1, pinned method versions remain stable for an
existing operation; new operations can use a redefinition with the same required signature.
There is no temporary legacy encoder secretly eligible for new decoder dispatch.

CSV/TSV retain their own protocols/text mapping. Only JSON encoding boundaries stop looking up
the legacy String encoder. Update marked-result checks at every call, discard, assignment and
iteration boundary; current ExecResult behavior remains separate.

Use hidden native registrations behind script wrappers, directory/mod.rs for multi-file Rust
modules and Shelly behavioral tests. Do not modify target/release/shelly. Implementation and behavioral coverage accompany this revision; see the implementation
reference for commands, evidence and initial profile limits.

## Adversarial transition traces

These are paper traces with specified outcomes, not executed tests.

| Counterexample | Deterministic outcome |
| --- | --- |
| let $r = fail | ConsumeSuccess throws immediately into the nearest catch |
| let $r: Reply &#124; Error = fail; let $alias: Reply &#124; Error = $r; return $r | One escrowed token transfers; aliases expire without duplicate failure |
| Pending scalar assigned to outer/global/captured cell | Original failure promoted before mutation; outside storage unchanged |
| ["result": fail] followed by clearing the hash | First insertion promotes; no hidden obligation enters the hash |
| Borrow pending scalar into helper, inspect and return it | Owner remains caller; helper does not create a second occurrence |
| Two pending captures left at try-body exit | Earliest activation primary; second Unhandled; one catch acknowledges both |
| Pending root return plus another pending local | Returned failure primary; local attached Unhandled; no successful transfer |
| Pending root return and failed finally | Returned error primary; cleanup attached; no lost token |
| Catch stores Failure(stack:, error:), then later reads its error | Passive history; no spontaneous failure |
| Explicitly convert that history into domain error | New occurrence; original source Cause preserved |
| Convert a pending captured error and capture its replacement | Only replacement remains pending; original alias is passive Cause history |
| Named error-group catch receives FirstError | Concrete FirstError payload and TypeID; filter-union narrowing works |
| Callback suspends midway through SSE read | Saved frame/regions retained; other pipeline tasks and native I/O progress |
| Script callback reenters same reader | ReaderBusy rather than recursive wait/deadlock |
| Wrapper fails after opening SSE but before creating outer reader | Creation-region registry releases SSE/body before caller catch |
| Sink factory fails with live source alias retained | Already reserved source lease quiesces/releases; alias observes terminal state |
| Cancellation while read is waiting | Cancel wait generation, unwind callback, run close once under cleanup budget |
| Infinite loop in finally, then second Ctrl+C | Budget or CleanupAbort exits script cleanup; mandatory native release; REPL recovers |
| close in finally after original reader failure | No replay of original; only a new release failure can attach |
| Error normally returns through produce -> wrapper | Snapshot already contains produce and wrapper before their frames disappear |
| Handler/new formatter recursively fails | Original packet preserved; bounded structural/native fallback |
| HTTP 500 body is HTML or diagnostic prefix is cut off | StatusError primary with body prefix/flags and decoding cause; body closed |
| Duplicated/misidentified OpenAI delta | Sequence/identity or done/terminal reconciliation fails; result cannot claim success |

## Acceptance scenarios and remaining validation

| Area | Required behavioral fixtures |
| --- | --- |
| Errors | Bare/_ discard; inferred success; explicit capture; alias overwrite; return escrow; caught history |
| Catches | Nested named groups; concrete narrowing; TypeID redefinition; specific/group/all filters |
| Cleanup | Return/break/continue; helper exit; two release failures; cumulative budget; repeated Ctrl+C |
| Traces | Native origin vs boundary; normal returned error; conversion cause; recursion; reporting fallback |
| JSON | Missing/null; null iteration; number/exponent work; escaped duplicate names; width/depth limits |
| Mapping | Enum keys/collisions; original named/empty hash factory; private constructor; one-shot union selection |
| Streams | Split UTF-8; incomplete frame; late failure; early close; reentry; cached result; abandoned reader cap |
| Pipeline | Process/network duplex beyond queue capacity; task-local redirections; setup failure; early rejection |
| HTTP | Two consumers; partial read/decode; incomplete diagnostic capture; expired deadline; cross-origin redirects |
| RPC | Null result; absent fields; exact ID kinds; invalid/empty batches; notification-only body |
| OpenAI | Reasoning before text; refusal/tool-only; duplicate/gapped sequences; wrong tuple/ID; terminal mismatch; EOF |
| Usability | Endpoint plus typed codec without per-call checks; one workflow catch; no manual stack/cause plumbing |

The remaining work is empirical validation of the chosen contracts: VM suspension coverage,
platform cancellation/disposal behavior, and tuning limit defaults. Those are implementation
risks, not invitations to invent different observable semantics. Tests should challenge the
paper traces before exposing the corresponding public features.

Typed readers across REPL submissions, automatic HTTP retries and flattened multi-channel text
streaming are deliberately outside the initial contract. Their absence does not make callers
write additional error checks. A later extension should preserve the ordinary success path.