@yielded/agent-storage-cloudflare 0.1.0-beta.174 → 0.1.0-beta.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/DoMemoryStore.mjs +1 -1
  2. package/dist/DoMessageDeliveryStore.mjs +1 -1
  3. package/dist/DoStorageFailpoint.d.mts +3 -1
  4. package/dist/DoStorageFailpoint.mjs +7 -2
  5. package/dist/DoStorageFailpoint.mjs.map +1 -1
  6. package/dist/{DoStorageVersion-NKnQU3eR.d.mts → DoStorageVersion-Mc7CXMdN.d.mts} +2 -2
  7. package/dist/DoStorageVersion.d.mts +1 -1
  8. package/dist/DoStorageVersion.mjs +2 -2
  9. package/dist/DoSubmissionLedger.d.mts +1 -1
  10. package/dist/DoSubmissionLedger.mjs +194 -98
  11. package/dist/DoSubmissionLedger.mjs.map +1 -1
  12. package/dist/{DoThreadStore-Bd4fTns3.d.mts → DoThreadStore-BzYJjm4O.d.mts} +18 -8
  13. package/dist/{DoThreadStore-DyzOZDXK.mjs → DoThreadStore-DG0h5J6u.mjs} +25 -19
  14. package/dist/DoThreadStore-DG0h5J6u.mjs.map +1 -0
  15. package/dist/DoThreadStore.d.mts +2 -2
  16. package/dist/DoThreadStore.mjs +3 -2
  17. package/dist/PortProtocol.d.mts +7201 -7201
  18. package/dist/PortRouting.mjs +21 -11
  19. package/dist/PortRouting.mjs.map +1 -1
  20. package/dist/{do-journal-ZMAzzENZ.mjs → do-journal-ySC3eXNv.mjs} +144 -45
  21. package/dist/do-journal-ySC3eXNv.mjs.map +1 -0
  22. package/dist/index.d.mts +2 -2
  23. package/dist/index.mjs +1 -1
  24. package/dist/{migrations-C3N9GN6A.mjs → migrations-CAO_akhD.mjs} +45 -22
  25. package/dist/migrations-CAO_akhD.mjs.map +1 -0
  26. package/dist/{owned-state-DcFh0YNo.mjs → owned-state-BgGOA2Ak.mjs} +4 -3
  27. package/dist/owned-state-BgGOA2Ak.mjs.map +1 -0
  28. package/package.json +1 -1
  29. package/src/DoStorageFailpoint.ts +4 -0
  30. package/src/DoSubmissionLedger.ts +430 -228
  31. package/src/DoThreadStore.ts +25 -44
  32. package/src/PortRouting.ts +57 -31
  33. package/src/internal/do-journal.ts +318 -70
  34. package/src/internal/migrations.ts +52 -10
  35. package/src/internal/owned-state.ts +12 -5
  36. package/src/internal/prompt-checkpoint.ts +6 -3
  37. package/src/internal/storage-span.ts +14 -10
  38. package/dist/DoThreadStore-DyzOZDXK.mjs.map +0 -1
  39. package/dist/do-journal-ZMAzzENZ.mjs.map +0 -1
  40. package/dist/migrations-C3N9GN6A.mjs.map +0 -1
  41. package/dist/owned-state-DcFh0YNo.mjs.map +0 -1
@@ -110,6 +110,8 @@ import {
110
110
  UnknownResolutionKind,
111
111
  unknownResolutionKind,
112
112
  unknownResolutionMatchesResult,
113
+ submissionInputRecordId,
114
+ submissionInputBatchId,
113
115
  submissionAbortRecordId,
114
116
  submissionSettlementRecordId,
115
117
  submissionSettlementBatchId,
@@ -345,8 +347,8 @@ const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
345
347
  const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
346
348
 
347
349
  const decodeSubmissionSnapshotType = (input: typeof SubmissionSnapshot.Type) =>
348
- Schema.decodeEffect(Schema.toType(Schema.Struct(SubmissionSnapshot.fields)))(input).pipe(
349
- Effect.map((fields) => SubmissionSnapshot.make(fields)),
350
+ SubmissionSnapshot.makeEffect(input).pipe(
351
+ Effect.mapError((issue) => new Schema.SchemaError(issue)),
350
352
  );
351
353
 
352
354
  const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
@@ -366,6 +368,46 @@ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
366
368
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
367
369
  const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
368
370
  const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
371
+ const decodeAdmissionRequestType = Schema.decodeEffect(Schema.toType(AdmissionRequest));
372
+ const decodeSubmissionLookupType = Schema.decodeEffect(Schema.toType(SubmissionLookup));
373
+ const decodeClaimRequestType = Schema.decodeEffect(Schema.toType(ClaimRequest));
374
+
375
+ const decodeMarkInputAppliedRequestType = Schema.decodeEffect(
376
+ Schema.toType(MarkInputAppliedRequest),
377
+ );
378
+
379
+ const decodeSettlementFinalizationType = Schema.decodeEffect(Schema.toType(SettlementFinalization));
380
+ const decodeClaimJoiningRequestType = Schema.decodeEffect(Schema.toType(ClaimJoiningRequest));
381
+
382
+ const decodeAbortIntentRequestType = Schema.decodeEffect(Schema.toType(AbortIntentRequest));
383
+
384
+ const decodeRecoverySnapshotRequestType = Schema.decodeEffect(
385
+ Schema.toType(RecoverySnapshotRequest),
386
+ );
387
+
388
+ const SubmissionRows = Schema.Array(SubmissionRow);
389
+ const ChildReservationRows = Schema.Array(ChildReservationRow);
390
+ const MaxQueueSequenceRows = Schema.Array(MaxQueueSequenceRow);
391
+ const SubmissionWorkItems = Schema.Array(SubmissionWorkItem);
392
+ const decodeSubmissionWorkItems = Schema.decodeEffect(SubmissionWorkItems);
393
+ const equivalentWorkerOrigin = Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin));
394
+
395
+ const decodeFirstWorkerAdmissions = Schema.decodeUnknownEffect(
396
+ Schema.Array(
397
+ Schema.Struct({
398
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
399
+ }),
400
+ ),
401
+ );
402
+
403
+ const CanonicalSettlementRows = Schema.Array(
404
+ Schema.Struct({
405
+ batch_id: Schema.fromJsonString(BoundedIdentifier),
406
+ record_id: Schema.fromJsonString(BoundedIdentifier),
407
+ record_json: BoundedStoredText,
408
+ }),
409
+ );
410
+
369
411
  const isDoStorageError = Schema.is(DoStorageError);
370
412
 
371
413
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
@@ -606,7 +648,7 @@ const makeServices = Effect.fnUntraced(function* () {
606
648
  }));
607
649
 
608
650
  const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
609
- decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
651
+ decodeRows(SubmissionRows, "effect_agent_submissions", rowKey, rows).pipe(
610
652
  Effect.mapError(internalFailure(operation)),
611
653
  );
612
654
 
@@ -721,13 +763,7 @@ const makeServices = Effect.fnUntraced(function* () {
721
763
  AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(sqlFailure(operation)));
722
764
 
723
765
  const decoded = yield* decodeRows(
724
- Schema.Array(
725
- Schema.Struct({
726
- batch_id: Schema.fromJsonString(BoundedIdentifier),
727
- record_id: Schema.fromJsonString(BoundedIdentifier),
728
- record_json: BoundedStoredText,
729
- }),
730
- ),
766
+ CanonicalSettlementRows,
731
767
  "effect_agent_canonical_records",
732
768
  recordId,
733
769
  found,
@@ -810,12 +846,9 @@ const makeServices = Effect.fnUntraced(function* () {
810
846
  });
811
847
 
812
848
  const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
813
- decodeRows(
814
- Schema.Array(ChildReservationRow),
815
- "effect_agent_child_reservations",
816
- rowKey,
817
- rows,
818
- ).pipe(Effect.mapError(internalFailure(operation)));
849
+ decodeRows(ChildReservationRows, "effect_agent_child_reservations", rowKey, rows).pipe(
850
+ Effect.mapError(internalFailure(operation)),
851
+ );
819
852
 
820
853
  const readChildReservation = Effect.fnUntraced(function* (
821
854
  operation: string,
@@ -1036,10 +1069,11 @@ const makeServices = Effect.fnUntraced(function* () {
1036
1069
 
1037
1070
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (
1038
1071
  request: AdmissionRequest,
1072
+ options?: { readonly readyIfInitialized?: boolean },
1039
1073
  ) {
1040
1074
  const operation = "ledger admit";
1041
1075
 
1042
- const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1076
+ const validated = yield* decodeAdmissionRequestType(request).pipe(
1043
1077
  Effect.mapError(internalFailure(operation)),
1044
1078
  );
1045
1079
 
@@ -1114,9 +1148,7 @@ const makeServices = Effect.fnUntraced(function* () {
1114
1148
  Effect.gen(function* () {
1115
1149
  const keyRowKey = `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`;
1116
1150
 
1117
- const existingRows = yield* byKey(validated, operation);
1118
-
1119
- const existing = yield* decodeSubmissionRows(operation, keyRowKey, existingRows);
1151
+ const existing = yield* byKey(validated, operation);
1120
1152
 
1121
1153
  if (existing.length > 1) {
1122
1154
  return yield* corruptionFailure(
@@ -1220,13 +1252,9 @@ const makeServices = Effect.fnUntraced(function* () {
1220
1252
  WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1221
1253
  `.pipe(Effect.mapError(sqlFailure(operation)));
1222
1254
 
1223
- const first = yield* Schema.decodeUnknownEffect(
1224
- Schema.Array(
1225
- Schema.Struct({
1226
- worker_admission_json: Schema.NullOr(BoundedStoredText),
1227
- }),
1228
- ),
1229
- )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1255
+ const first = yield* decodeFirstWorkerAdmissions(firstRows).pipe(
1256
+ Effect.mapError(internalFailure(operation)),
1257
+ );
1230
1258
 
1231
1259
  if (first[0] !== undefined) {
1232
1260
  const previous =
@@ -1236,12 +1264,7 @@ const makeServices = Effect.fnUntraced(function* () {
1236
1264
  first[0].worker_admission_json,
1237
1265
  ).pipe(Effect.mapError(internalFailure(operation)));
1238
1266
 
1239
- if (
1240
- !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1241
- previous?.origin,
1242
- validated.workerAdmission?.origin,
1243
- )
1244
- )
1267
+ if (!equivalentWorkerOrigin(previous?.origin, validated.workerAdmission?.origin))
1245
1268
  return yield* AdmissionPolicyError.make({
1246
1269
  reason: "refused",
1247
1270
  code: "worker-origin-conflict",
@@ -1269,7 +1292,7 @@ const makeServices = Effect.fnUntraced(function* () {
1269
1292
  `.pipe(Effect.mapError(sqlFailure(operation)));
1270
1293
 
1271
1294
  const decodedMax = yield* decodeRows(
1272
- Schema.Array(MaxQueueSequenceRow),
1295
+ MaxQueueSequenceRows,
1273
1296
  "effect_agent_submissions",
1274
1297
  validated.threadId,
1275
1298
  maxRows,
@@ -1281,6 +1304,17 @@ const makeServices = Effect.fnUntraced(function* () {
1281
1304
 
1282
1305
  const now = yield* currentInstant;
1283
1306
 
1307
+ // The first canonical record is ThreadCreated. This is the same positive-tail proof
1308
+ // used by ensureThreadCreated, captured under this admission's owner transaction.
1309
+ const ready =
1310
+ options?.readyIfInitialized === true &&
1311
+ validated.workerAdmission === undefined &&
1312
+ (yield* journal
1313
+ .getThread(validated.threadId)
1314
+ .pipe(Effect.mapError(internalFailure(operation)))).some(
1315
+ (thread) => thread.tail_sequence > 0,
1316
+ );
1317
+
1284
1318
  yield* sql`
1285
1319
  INSERT INTO effect_agent_submissions (
1286
1320
  submission_id,
@@ -1296,6 +1330,7 @@ const makeServices = Effect.fnUntraced(function* () {
1296
1330
  receipt_id,
1297
1331
  state,
1298
1332
  created_at,
1333
+ ready_at,
1299
1334
  parent_submission_id,
1300
1335
  parent_tool_call_id,
1301
1336
  admission_group,
@@ -1314,8 +1349,9 @@ const makeServices = Effect.fnUntraced(function* () {
1314
1349
  ${inputJson},
1315
1350
  ${validated.inputDigest},
1316
1351
  ${mintedReceiptId},
1317
- 'admitted',
1352
+ ${ready ? "ready" : "admitted"},
1318
1353
  ${now.iso},
1354
+ ${ready ? now.iso : null},
1319
1355
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1320
1356
  ${validated.parentLinkage?.parentToolCallId ?? null},
1321
1357
  ${validated.admissionGroup ?? null},
@@ -1341,6 +1377,14 @@ const makeServices = Effect.fnUntraced(function* () {
1341
1377
 
1342
1378
  // The INSERT (never replay or hydration) proves these new submission-owned sets
1343
1379
  // empty. Gated writes update them; rollback/invalidation discards that proof.
1380
+ // MAX(queue_sequence) and this insert share the transaction: no later row exists.
1381
+ // Every matching admission invalidates this complete empty suffix before readiness.
1382
+ submissionViews.seedMatching(
1383
+ JSON.stringify(["joining-suffix", validated.threadId, queueSequence]),
1384
+ (row) => row.thread_id === validated.threadId && row.queue_sequence > queueSequence,
1385
+ [],
1386
+ 0,
1387
+ );
1344
1388
  rows.aborts.seed("submission_id", mintedSubmissionId, []);
1345
1389
  rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
1346
1390
  rows.approvals.seed("submission_id", mintedSubmissionId, []);
@@ -1350,13 +1394,21 @@ const makeServices = Effect.fnUntraced(function* () {
1350
1394
 
1351
1395
  yield* recordProgress("submission", operation);
1352
1396
 
1353
- return yield* decodeAdmissionResult({
1397
+ const admitted = yield* decodeAdmissionResult({
1354
1398
  submissionId: mintedSubmissionId,
1355
1399
  receiptId: mintedReceiptId,
1356
1400
  queueSequence,
1357
- state: "admitted",
1401
+ state: ready ? "ready" : "admitted",
1358
1402
  replayed: false,
1359
1403
  }).pipe(Effect.mapError(internalFailure(operation)));
1404
+
1405
+ if (ready && lifecycle !== undefined)
1406
+ yield* retainLifecycle(yield* requireSubmission(operation, mintedSubmissionId), {
1407
+ _tag: "SubmissionReady",
1408
+ submissionId: admitted.submissionId,
1409
+ });
1410
+
1411
+ return admitted;
1360
1412
  }),
1361
1413
  );
1362
1414
 
@@ -1466,7 +1518,7 @@ const makeServices = Effect.fnUntraced(function* () {
1466
1518
  ) {
1467
1519
  const operation = "ledger lookup";
1468
1520
 
1469
- const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1521
+ const validated = yield* decodeSubmissionLookupType(request).pipe(
1470
1522
  Effect.mapError(internalFailure(operation)),
1471
1523
  );
1472
1524
 
@@ -1478,13 +1530,7 @@ const makeServices = Effect.fnUntraced(function* () {
1478
1530
  return Option.some(yield* decodeSubmissionSnapshot(operation, row.value));
1479
1531
  }
1480
1532
 
1481
- const foundRows = yield* byKey(validated, operation);
1482
-
1483
- const decoded = yield* decodeSubmissionRows(
1484
- operation,
1485
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1486
- foundRows,
1487
- );
1533
+ const decoded = yield* byKey(validated, operation);
1488
1534
 
1489
1535
  if (decoded.length > 1) {
1490
1536
  return yield* corruptionFailure(
@@ -1511,13 +1557,7 @@ const makeServices = Effect.fnUntraced(function* () {
1511
1557
  request,
1512
1558
  ).pipe(Effect.mapError(internalFailure(operation)));
1513
1559
 
1514
- const foundRows = yield* byKey(validated, operation);
1515
-
1516
- const decoded = yield* decodeSubmissionRows(
1517
- operation,
1518
- `${validated.threadId}/${validated.principal}/${validated.idempotencyKey}`,
1519
- foundRows,
1520
- );
1560
+ const decoded = yield* byKey(validated, operation);
1521
1561
 
1522
1562
  if (decoded.length > 1) {
1523
1563
  return yield* corruptionFailure(
@@ -1540,7 +1580,7 @@ const makeServices = Effect.fnUntraced(function* () {
1540
1580
  )(function* (request: ClaimRequest) {
1541
1581
  const operation = "ledger select head";
1542
1582
 
1543
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1583
+ const validated = yield* decodeClaimRequestType(request).pipe(
1544
1584
  Effect.mapError(internalFailure(operation)),
1545
1585
  );
1546
1586
 
@@ -1552,7 +1592,28 @@ const makeServices = Effect.fnUntraced(function* () {
1552
1592
  Effect.gen(function* () {
1553
1593
  if (owner.hasOwner(validated.threadId)) return Option.none<Claim>();
1554
1594
 
1555
- const headRows = yield* sql<Record<string, unknown>>`
1595
+ const active = yield* activeNonterminal;
1596
+ const heads: Array<SubmissionRow> = [];
1597
+
1598
+ const limit =
1599
+ validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1;
1600
+
1601
+ if (active.length <= 128) {
1602
+ const candidates = active
1603
+ .filter((row) => row.thread_id === validated.threadId)
1604
+ .sort((left, right) => left.queue_sequence - right.queue_sequence);
1605
+
1606
+ for (const candidate of candidates) {
1607
+ if (
1608
+ candidate.state === "unknown" &&
1609
+ Option.isNone(yield* readAbortIntent(operation, candidate.submission_id))
1610
+ )
1611
+ continue;
1612
+ heads.push(yield* requireSubmission(operation, candidate.submission_id));
1613
+ if (heads.length === limit) break;
1614
+ }
1615
+ } else {
1616
+ const headRows = yield* sql<Record<string, unknown>>`
1556
1617
  SELECT ${submissionColumns}
1557
1618
  FROM effect_agent_submissions
1558
1619
  WHERE thread_id = ${validated.threadId}
@@ -1567,7 +1628,8 @@ const makeServices = Effect.fnUntraced(function* () {
1567
1628
  LIMIT ${validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1}
1568
1629
  `.pipe(Effect.mapError(sqlFailure(operation)));
1569
1630
 
1570
- const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1631
+ heads.push(...(yield* decodeSubmissionRows(operation, validated.threadId, headRows)));
1632
+ }
1571
1633
 
1572
1634
  if (heads.length === 0) return Option.none<Claim>();
1573
1635
  let head = heads[0];
@@ -1632,53 +1694,159 @@ const makeServices = Effect.fnUntraced(function* () {
1632
1694
  return claimed;
1633
1695
  });
1634
1696
 
1697
+ const markInputAppliedInTransaction = Effect.fnUntraced(function* (
1698
+ validated: MarkInputAppliedRequest,
1699
+ ) {
1700
+ const operation = "ledger mark input applied";
1701
+
1702
+ const submission = yield* requireSubmission(operation, validated.submissionId);
1703
+
1704
+ yield* requireOwner(operation, submission);
1705
+ if (submission.input_applied_record_id !== null) {
1706
+ if (
1707
+ submission.input_applied_record_id === validated.recordId &&
1708
+ submission.input_applied_sequence === validated.sequence
1709
+ ) {
1710
+ return;
1711
+ }
1712
+
1713
+ return yield* corruptionFailure(
1714
+ operation,
1715
+ "effect_agent_submissions",
1716
+ validated.submissionId,
1717
+ "A different canonical input marker is already recorded for this Submission.",
1718
+ );
1719
+ }
1720
+ yield* sql`
1721
+ UPDATE effect_agent_submissions
1722
+ SET
1723
+ input_applied_record_id = ${validated.recordId},
1724
+ input_applied_sequence = ${validated.sequence},
1725
+ state = CASE
1726
+ WHEN state IN ('admitted', 'ready') THEN 'input-applied'
1727
+ ELSE state
1728
+ END
1729
+ WHERE submission_id = ${validated.submissionId}
1730
+ RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1731
+ rows.submissions.write,
1732
+ Effect.mapError(internalFailure(operation)),
1733
+ );
1734
+ });
1735
+
1635
1736
  const markInputApplied: SubmissionLedger["Service"]["markInputApplied"] = Effect.fnUntraced(
1636
1737
  function* (request: MarkInputAppliedRequest) {
1637
1738
  const operation = "ledger mark input applied";
1638
1739
 
1639
- const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1640
- request,
1641
- ).pipe(Effect.mapError(internalFailure(operation)));
1740
+ const validated = yield* decodeMarkInputAppliedRequestType(request).pipe(
1741
+ Effect.mapError(internalFailure(operation)),
1742
+ );
1642
1743
 
1643
1744
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1644
- yield* inWriteTransaction(
1645
- operation,
1646
- Effect.gen(function* () {
1647
- const submission = yield* requireSubmission(operation, validated.submissionId);
1745
+ yield* inWriteTransaction(operation, markInputAppliedInTransaction(validated));
1746
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1747
+ },
1748
+ );
1648
1749
 
1649
- yield* requireOwner(operation, submission);
1650
- if (submission.input_applied_record_id !== null) {
1651
- if (
1652
- submission.input_applied_record_id === validated.recordId &&
1653
- submission.input_applied_sequence === validated.sequence
1654
- ) {
1655
- return;
1656
- }
1750
+ const appendInput: NonNullable<SubmissionLedger["Service"]["appendInput"]> = Effect.fnUntraced(
1751
+ function* (submissionId, request) {
1752
+ const operation = "append original input and context";
1657
1753
 
1658
- return yield* corruptionFailure(
1659
- operation,
1660
- "effect_agent_submissions",
1661
- validated.submissionId,
1662
- "A different canonical input marker is already recorded for this Submission.",
1663
- );
1664
- }
1665
- yield* sql`
1666
- UPDATE effect_agent_submissions
1667
- SET
1668
- input_applied_record_id = ${validated.recordId},
1669
- input_applied_sequence = ${validated.sequence},
1670
- state = CASE
1671
- WHEN state IN ('admitted', 'ready') THEN 'input-applied'
1672
- ELSE state
1673
- END
1674
- WHERE submission_id = ${validated.submissionId}
1675
- RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
1676
- rows.submissions.write,
1677
- Effect.mapError(internalFailure(operation)),
1678
- );
1679
- }),
1754
+ const prepared = yield* prepareCanonicalAppend(request).pipe(
1755
+ Effect.provideService(Crypto.Crypto, crypto),
1756
+ );
1757
+
1758
+ const record = prepared.request.batch.records[0];
1759
+
1760
+ const context = prepared.request.executions
1761
+ ?.flatMap((update) => update.facts ?? [])
1762
+ .find(({ payload }) => payload._tag === "RunContextRecorded");
1763
+
1764
+ if (
1765
+ prepared.request.executions?.length !== 1 ||
1766
+ prepared.request.executions[0]?.previousRevision !== 0 ||
1767
+ prepared.request.batch.records.length !== 1 ||
1768
+ record?.payload._tag !== "UserInputRecorded" ||
1769
+ record.payload.kind !== "user" ||
1770
+ record.payload.submissionId !== submissionId ||
1771
+ record.payload.runId !== runIdForSubmission(submissionId) ||
1772
+ record.recordId !== submissionInputRecordId(submissionId) ||
1773
+ prepared.request.batch.batchId !== submissionInputBatchId(submissionId) ||
1774
+ (context?.payload._tag === "RunContextRecorded" &&
1775
+ context.payload.historyThrough !== prepared.request.expectedTailSequence)
1776
+ )
1777
+ return yield* ThreadStoreError.make({
1778
+ operation,
1779
+ message: "Original input publication has a different context boundary",
1780
+ });
1781
+
1782
+ const mapError = (cause: { readonly message: string }) =>
1783
+ ThreadStoreError.make({ operation, message: cause.message, cause });
1784
+
1785
+ yield* journal.prepareAppend(prepared.raw).pipe(Effect.mapError(mapError));
1786
+
1787
+ return yield* owner.withProgress(
1788
+ prepared.request.threadId,
1789
+ owner.withMutation(
1790
+ Effect.gen(function* () {
1791
+ yield* hitFailpoint("append:before", operation);
1792
+ yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1793
+
1794
+ const result = yield* journal
1795
+ .withWriteTransaction(operation)(
1796
+ Effect.gen(function* () {
1797
+ const submission = yield* requireSubmission(operation, submissionId);
1798
+
1799
+ yield* requireOwner(operation, submission);
1800
+ if (submission.thread_id !== prepared.request.threadId)
1801
+ return yield* LedgerError.make({
1802
+ operation,
1803
+ message: "Original input belongs to another Thread",
1804
+ });
1805
+ const result = yield* journal.appendPrepared(prepared.raw);
1806
+
1807
+ yield* markInputAppliedInTransaction(
1808
+ MarkInputAppliedRequest.make({
1809
+ submissionId,
1810
+ recordId: record.recordId,
1811
+ sequence: result.firstSequence,
1812
+ }),
1813
+ );
1814
+
1815
+ return result;
1816
+ }),
1817
+ )
1818
+ .pipe(
1819
+ Effect.mapError((error) =>
1820
+ error._tag === "DoAppendConflict"
1821
+ ? AppendConflict.make({
1822
+ threadId: prepared.request.threadId,
1823
+ batchId: prepared.request.batch.batchId,
1824
+ reason: error.reason,
1825
+ ...(error.actualTailSequence === undefined
1826
+ ? {}
1827
+ : { actualTailSequence: error.actualTailSequence }),
1828
+ })
1829
+ : error._tag === "OwnershipLost"
1830
+ ? error
1831
+ : mapError(error),
1832
+ ),
1833
+ );
1834
+
1835
+ yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1836
+ yield* hitFailpoint("append:after", operation);
1837
+
1838
+ return result;
1839
+ }).pipe(
1840
+ Effect.mapError((error) =>
1841
+ error._tag === "OwnershipLost" ||
1842
+ error._tag === "ThreadStoreError" ||
1843
+ error._tag === "AppendConflict"
1844
+ ? error
1845
+ : mapError(error),
1846
+ ),
1847
+ ),
1848
+ ),
1680
1849
  );
1681
- yield* hitFailpoint("ledger:mark-input-applied:after", operation);
1682
1850
  },
1683
1851
  );
1684
1852
 
@@ -1897,9 +2065,9 @@ const makeServices = Effect.fnUntraced(function* () {
1897
2065
  )(function* (request: SettlementFinalization) {
1898
2066
  const operation = "ledger finalize settlement";
1899
2067
 
1900
- const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1901
- request,
1902
- ).pipe(Effect.mapError(internalFailure(operation)));
2068
+ const validated = yield* decodeSettlementFinalizationType(request).pipe(
2069
+ Effect.mapError(internalFailure(operation)),
2070
+ );
1903
2071
 
1904
2072
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1905
2073
 
@@ -2211,14 +2379,13 @@ const makeServices = Effect.fnUntraced(function* () {
2211
2379
  ) {
2212
2380
  const operation = "ledger claim joining";
2213
2381
 
2214
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2382
+ const validated = yield* decodeClaimJoiningRequestType(request).pipe(
2215
2383
  Effect.mapError(internalFailure(operation)),
2216
2384
  );
2217
2385
 
2218
2386
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2219
2387
 
2220
- const claims = yield* inWriteTransaction(
2221
- operation,
2388
+ const claims = yield* owner.withMutation(
2222
2389
  Effect.gen(function* () {
2223
2390
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2224
2391
 
@@ -2238,68 +2405,92 @@ const makeServices = Effect.fnUntraced(function* () {
2238
2405
 
2239
2406
  if (stopped.length > 0) return [];
2240
2407
 
2241
- const laterRows = yield* sql<Record<string, unknown>>`
2242
- SELECT ${submissionColumns}
2243
- FROM effect_agent_submissions
2244
- WHERE thread_id = ${validated.threadId}
2245
- AND queue_sequence > ${host.queue_sequence}
2246
- ORDER BY queue_sequence ASC
2247
- `.pipe(Effect.mapError(sqlFailure(operation)));
2248
-
2249
- const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2250
- const claimed: Array<JoiningClaim> = [];
2251
-
2252
- for (const row of later) {
2253
- if (claimed.length >= validated.maxCount) break;
2254
- // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
2255
- // the coordinator re-delivers already-joined input through the coverage rule.
2256
- if (
2257
- (row.state === "joining" || row.state === "joined") &&
2258
- row.joined_host_submission_id === validated.hostSubmissionId
2259
- ) {
2260
- continue;
2261
- }
2262
- // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
2263
- // settles aborted never-claimed queued work immediately, and settlement order of
2264
- // never-run work is not execution order (DUR-004 bounds execution).
2265
- if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2266
- // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2267
- // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2268
- if (row.state !== "ready") break;
2269
- const canonical = yield* readCanonicalSettlement(operation, row);
2270
-
2271
- if (canonical?.settlement.outcome === "aborted") continue;
2272
- if (canonical !== undefined) break;
2273
- yield* sql`
2408
+ // Retain only an empty complete suffix. Any later admission invalidates this view;
2409
+ // nonempty rows keep their SQL ordering, and rollback discards the empty proof.
2410
+ const later = yield* submissionViews
2411
+ .matching(
2412
+ JSON.stringify(["joining-suffix", validated.threadId, host.queue_sequence]),
2413
+ (row) =>
2414
+ row.thread_id === validated.threadId && row.queue_sequence > host.queue_sequence,
2415
+ sql`
2416
+ SELECT ${submissionColumns}
2417
+ FROM effect_agent_submissions
2418
+ WHERE thread_id = ${validated.threadId}
2419
+ AND queue_sequence > ${host.queue_sequence}
2420
+ ORDER BY queue_sequence ASC
2421
+ `,
2422
+ 0,
2423
+ )
2424
+ .pipe(
2425
+ Effect.mapError((error) =>
2426
+ error._tag === "SqlError"
2427
+ ? sqlFailure(operation)(error)
2428
+ : internalFailure(operation)(error),
2429
+ ),
2430
+ );
2431
+
2432
+ if (later.length === 0) return [];
2433
+
2434
+ // The reader gate stays held through promotion. Nonempty candidates retain
2435
+ // their native transaction, live-owner checks and contiguous-prefix rules.
2436
+ return yield* inWriteTransaction(
2437
+ operation,
2438
+ Effect.gen(function* () {
2439
+ const claimed: Array<JoiningClaim> = [];
2440
+
2441
+ for (const row of later) {
2442
+ if (claimed.length >= validated.maxCount) break;
2443
+ // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
2444
+ // the coordinator re-delivers already-joined input through the coverage rule.
2445
+ if (
2446
+ (row.state === "joining" || row.state === "joined") &&
2447
+ row.joined_host_submission_id === validated.hostSubmissionId
2448
+ ) {
2449
+ continue;
2450
+ }
2451
+ // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
2452
+ // settles aborted never-claimed queued work immediately, and settlement order of
2453
+ // never-run work is not execution order (DUR-004 bounds execution).
2454
+ if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2455
+ // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2456
+ // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2457
+ if (row.state !== "ready") break;
2458
+ const canonical = yield* readCanonicalSettlement(operation, row);
2459
+
2460
+ if (canonical?.settlement.outcome === "aborted") continue;
2461
+ if (canonical !== undefined) break;
2462
+ yield* sql`
2274
2463
  UPDATE effect_agent_submissions
2275
2464
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2276
2465
  WHERE submission_id = ${row.submission_id}
2277
2466
  RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2278
- rows.submissions.write,
2279
- Effect.mapError(internalFailure(operation)),
2280
- );
2467
+ rows.submissions.write,
2468
+ Effect.mapError(internalFailure(operation)),
2469
+ );
2281
2470
 
2282
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2283
- Effect.mapError((error) =>
2284
- corruptionFailure(
2285
- operation,
2286
- "effect_agent_submissions",
2287
- row.submission_id,
2288
- error.message,
2289
- ),
2290
- ),
2291
- );
2471
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2472
+ Effect.mapError((error) =>
2473
+ corruptionFailure(
2474
+ operation,
2475
+ "effect_agent_submissions",
2476
+ row.submission_id,
2477
+ error.message,
2478
+ ),
2479
+ ),
2480
+ );
2292
2481
 
2293
- claimed.push(
2294
- yield* decodeJoiningClaim({
2295
- submissionId: row.submission_id,
2296
- queueSequence: row.queue_sequence,
2297
- inputPayload,
2298
- }).pipe(Effect.mapError(internalFailure(operation))),
2299
- );
2300
- }
2482
+ claimed.push(
2483
+ yield* decodeJoiningClaim({
2484
+ submissionId: row.submission_id,
2485
+ queueSequence: row.queue_sequence,
2486
+ inputPayload,
2487
+ }).pipe(Effect.mapError(internalFailure(operation))),
2488
+ );
2489
+ }
2301
2490
 
2302
- return claimed;
2491
+ return claimed;
2492
+ }),
2493
+ );
2303
2494
  }),
2304
2495
  );
2305
2496
 
@@ -3451,7 +3642,7 @@ const makeServices = Effect.fnUntraced(function* () {
3451
3642
  ).pipe(Effect.mapError(sqlFailure(operation)));
3452
3643
 
3453
3644
  const decoded = yield* decodeRows(
3454
- Schema.Array(SubmissionWorkItem),
3645
+ SubmissionWorkItems,
3455
3646
  "effect_agent_submissions",
3456
3647
  "nonterminal_scan",
3457
3648
  rows,
@@ -3471,59 +3662,74 @@ const makeServices = Effect.fnUntraced(function* () {
3471
3662
  return [decoded, next] as const;
3472
3663
  });
3473
3664
 
3474
- const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.unwrap(
3475
- state.read(
3476
- Effect.gen(function* () {
3477
- // Cache only a complete bounded active set. Large hosts keep the paged scan.
3478
- const active = yield* cached(
3479
- workRows.matching(
3480
- "nonterminal",
3481
- (row) => row.state !== "settled",
3482
- sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
3483
- deployment_id, receipt_id, state
3484
- FROM effect_agent_submissions WHERE state <> 'settled' LIMIT 129`,
3485
- 128,
3486
- ),
3487
- "ledger scan nonterminal",
3488
- );
3665
+ // Both discovery and head selection use this same complete, owner-invalidated bounded view.
3666
+ const activeNonterminal = cached(
3667
+ workRows.matching(
3668
+ "nonterminal",
3669
+ (row) => row.state !== "settled",
3670
+ sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
3671
+ deployment_id, receipt_id, state
3672
+ FROM effect_agent_submissions WHERE state <> 'settled' LIMIT 129`,
3673
+ 128,
3674
+ ),
3675
+ "ledger scan nonterminal",
3676
+ );
3489
3677
 
3490
- if (active.length > 128)
3491
- return Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3492
- undefined,
3493
- scanPage,
3494
- );
3678
+ const boundedNonterminal = state.read(
3679
+ Effect.gen(function* () {
3680
+ // Cache only a complete bounded active set. Large hosts keep the paged scan.
3681
+ const active = yield* activeNonterminal;
3495
3682
 
3496
- const sorted = [...active].sort((left, right) =>
3497
- left.thread_id < right.thread_id
3498
- ? -1
3499
- : left.thread_id > right.thread_id
3500
- ? 1
3501
- : left.queue_sequence - right.queue_sequence,
3502
- );
3683
+ if (active.length > 128) return undefined;
3503
3684
 
3504
- const work = yield* Schema.decodeEffect(Schema.Array(SubmissionWorkItem))(
3505
- sorted.map((row) => ({
3506
- submissionId: row.submission_id,
3507
- threadId: row.thread_id,
3508
- queueSequence: row.queue_sequence,
3509
- principal: row.principal,
3510
- idempotencyKey: row.idempotency_key,
3511
- deploymentId: row.deployment_id,
3512
- receiptId: row.receipt_id,
3513
- state: row.state,
3514
- })),
3515
- ).pipe(Effect.mapError(internalFailure("ledger scan nonterminal")));
3516
-
3517
- return Stream.fromIterable(work);
3518
- }),
3685
+ const sorted = [...active].sort((left, right) =>
3686
+ left.thread_id < right.thread_id
3687
+ ? -1
3688
+ : left.thread_id > right.thread_id
3689
+ ? 1
3690
+ : left.queue_sequence - right.queue_sequence,
3691
+ );
3692
+
3693
+ const work = yield* decodeSubmissionWorkItems(
3694
+ sorted.map((row) => ({
3695
+ submissionId: row.submission_id,
3696
+ threadId: row.thread_id,
3697
+ queueSequence: row.queue_sequence,
3698
+ principal: row.principal,
3699
+ idempotencyKey: row.idempotency_key,
3700
+ deploymentId: row.deployment_id,
3701
+ receiptId: row.receipt_id,
3702
+ state: row.state,
3703
+ })),
3704
+ ).pipe(Effect.mapError(internalFailure("ledger scan nonterminal")));
3705
+
3706
+ return work;
3707
+ }),
3708
+ );
3709
+
3710
+ const pagedNonterminal = Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3711
+ undefined,
3712
+ scanPage,
3713
+ );
3714
+
3715
+ // Large scans keep their bounded page lifetime and streaming backpressure.
3716
+ const scanNonterminal = Stream.unwrap(
3717
+ Effect.map(boundedNonterminal, (work) =>
3718
+ work === undefined ? pagedNonterminal : Stream.fromIterable(work),
3519
3719
  ),
3520
3720
  );
3521
3721
 
3722
+ const listNonterminal = Effect.filterOrElse(
3723
+ boundedNonterminal,
3724
+ (work) => work !== undefined,
3725
+ () => Stream.runCollect(pagedNonterminal),
3726
+ );
3727
+
3522
3728
  const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
3523
3729
  Effect.fnUntraced(function* (request) {
3524
3730
  const operation = "ledger read abort intent";
3525
3731
 
3526
- const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3732
+ const validated = yield* decodeAbortIntentRequestType(request).pipe(
3527
3733
  Effect.mapError(internalFailure(operation)),
3528
3734
  );
3529
3735
 
@@ -3548,9 +3754,9 @@ const makeServices = Effect.fnUntraced(function* () {
3548
3754
  Effect.fnUntraced(function* (request: RecoverySnapshotRequest) {
3549
3755
  const operation = "ledger load recovery snapshot";
3550
3756
 
3551
- const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3552
- request,
3553
- ).pipe(Effect.mapError(internalFailure(operation)));
3757
+ const validated = yield* decodeRecoverySnapshotRequestType(request).pipe(
3758
+ Effect.mapError(internalFailure(operation)),
3759
+ );
3554
3760
 
3555
3761
  return yield* state.read(
3556
3762
  Effect.gen(function* () {
@@ -3590,19 +3796,16 @@ const makeServices = Effect.fnUntraced(function* () {
3590
3796
  .filter((row) => row.joined_host_submission_id === validated.submissionId)
3591
3797
  .sort((a, b) => a.queue_sequence - b.queue_sequence);
3592
3798
 
3593
- const joinSubmissions = yield* decodeSubmissionRows(
3594
- operation,
3595
- validated.submissionId,
3596
- joinRows,
3597
- );
3799
+ const joins: Array<JoinSnapshot> = [];
3598
3800
 
3599
- const joins = yield* Effect.forEach(joinSubmissions, (row) =>
3600
- decodeJoinSnapshot({
3601
- submissionId: row.submission_id,
3602
- state: row.state,
3603
- hostSubmissionId: validated.submissionId,
3604
- }).pipe(Effect.mapError(internalFailure(operation))),
3605
- );
3801
+ for (const row of joinRows)
3802
+ joins.push(
3803
+ yield* decodeJoinSnapshot({
3804
+ submissionId: row.submission_id,
3805
+ state: row.state,
3806
+ hostSubmissionId: validated.submissionId,
3807
+ }).pipe(Effect.mapError(internalFailure(operation))),
3808
+ );
3606
3809
 
3607
3810
  let hostSubmissionId: RecoverySnapshot["hostSubmissionId"];
3608
3811
 
@@ -3643,15 +3846,17 @@ const makeServices = Effect.fnUntraced(function* () {
3643
3846
 
3644
3847
  const decisionRows = yield* readApprovalDecisions(operation, validated.submissionId);
3645
3848
 
3646
- const approvalDecisions = yield* Effect.forEach(decisionRows, (row) =>
3647
- approvalIntentFromRow(operation, row),
3648
- );
3849
+ const approvalDecisions: Array<ApprovalDecisionIntent> = [];
3850
+
3851
+ for (const row of decisionRows)
3852
+ approvalDecisions.push(yield* approvalIntentFromRow(operation, row));
3649
3853
 
3650
3854
  const resolutionRows = yield* readUnknownResolutions(operation, validated.submissionId);
3651
3855
 
3652
- const unknownResolutions = yield* Effect.forEach(resolutionRows, (row) =>
3653
- unknownResolutionIntentFromRow(operation, row),
3654
- );
3856
+ const unknownResolutions: Array<UnknownResolutionIntent> = [];
3857
+
3858
+ for (const row of resolutionRows)
3859
+ unknownResolutions.push(yield* unknownResolutionIntentFromRow(operation, row));
3655
3860
 
3656
3861
  // Parent-side subagent view: this Submission's child budget reservations in parent
3657
3862
  // Tool Call order, plus each attached child's current lane state (a disposable
@@ -3666,21 +3871,16 @@ const makeServices = Effect.fnUntraced(function* () {
3666
3871
  .filter((row) => row.parent_submission_id === validated.submissionId)
3667
3872
  .sort((a, b) => a.parent_tool_call_id.localeCompare(b.parent_tool_call_id));
3668
3873
 
3669
- const decodedChildReservations = yield* decodeChildReservationRows(
3670
- operation,
3671
- validated.submissionId,
3672
- childReservationRows,
3673
- );
3874
+ const childReservations: Array<ChildBudgetReservationSnapshot> = [];
3674
3875
 
3675
- const childReservations = yield* Effect.forEach(decodedChildReservations, (row) =>
3676
- childReservationSnapshotFromRow(operation, row),
3677
- );
3876
+ for (const row of childReservationRows)
3877
+ childReservations.push(yield* childReservationSnapshotFromRow(operation, row));
3678
3878
 
3679
3879
  const markers = yield* readChildSettlementMarkers(operation, validated.submissionId);
3680
3880
  const markersByChild = new Map(markers.map((row) => [row.child_submission_id, row]));
3681
3881
  const childAttachments: Array<ChildAttachmentSnapshot> = [];
3682
3882
 
3683
- for (const row of decodedChildReservations) {
3883
+ for (const row of childReservationRows) {
3684
3884
  if (row.child_submission_id === null) continue;
3685
3885
  const child = yield* readSubmission(operation, row.child_submission_id);
3686
3886
 
@@ -3751,6 +3951,7 @@ const makeServices = Effect.fnUntraced(function* () {
3751
3951
  resolveAdmission,
3752
3952
  selectHead,
3753
3953
  markInputApplied,
3954
+ appendInput,
3754
3955
  finalizeSettlement: (request) => owner.withMutation(finalizeSettlement(request)),
3755
3956
  requestAbort,
3756
3957
  stopWorker,
@@ -3768,6 +3969,7 @@ const makeServices = Effect.fnUntraced(function* () {
3768
3969
  beginChildBudgetRelease,
3769
3970
  releaseChildBudget,
3770
3971
  scanNonterminal,
3972
+ listNonterminal,
3771
3973
  loadRecoverySnapshot,
3772
3974
  readAbortIntent: readAbortIntentForSubmission,
3773
3975
  }),