@yielded/agent-storage-cloudflare 0.1.0-beta.173 → 0.1.0-beta.175

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 (44) 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/DoStorageFailpointTesting.mjs +1 -4
  7. package/dist/DoStorageFailpointTesting.mjs.map +1 -1
  8. package/dist/{DoStorageVersion-NKnQU3eR.d.mts → DoStorageVersion-Mc7CXMdN.d.mts} +2 -2
  9. package/dist/DoStorageVersion.d.mts +1 -1
  10. package/dist/DoStorageVersion.mjs +2 -2
  11. package/dist/DoSubmissionLedger.mjs +121 -67
  12. package/dist/DoSubmissionLedger.mjs.map +1 -1
  13. package/dist/DoSubscriptionStore.mjs.map +1 -1
  14. package/dist/{DoThreadStore-CtyahRTW.mjs → DoThreadStore-CgIZQUNR.mjs} +30 -25
  15. package/dist/DoThreadStore-CgIZQUNR.mjs.map +1 -0
  16. package/dist/DoThreadStore.mjs +1 -1
  17. package/dist/PortProtocol.d.mts +12 -0
  18. package/dist/PortRouting.mjs +6 -1
  19. package/dist/PortRouting.mjs.map +1 -1
  20. package/dist/{do-journal-_MdLGTd3.mjs → do-journal-7P3DUBz0.mjs} +140 -65
  21. package/dist/do-journal-7P3DUBz0.mjs.map +1 -0
  22. package/dist/index.d.mts +1 -1
  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-BHzUi0eb.mjs → owned-state-B4QhezMp.mjs} +8 -6
  27. package/dist/owned-state-B4QhezMp.mjs.map +1 -0
  28. package/package.json +1 -1
  29. package/src/DoStorageFailpoint.ts +4 -0
  30. package/src/DoStorageFailpointTesting.ts +9 -7
  31. package/src/DoSubmissionLedger.ts +271 -150
  32. package/src/DoSubscriptionStore.ts +1 -0
  33. package/src/DoThreadStore.ts +27 -50
  34. package/src/PortRouting.ts +10 -2
  35. package/src/internal/canonical-append.ts +1 -1
  36. package/src/internal/do-journal.ts +987 -800
  37. package/src/internal/migrations.ts +53 -10
  38. package/src/internal/owned-state.ts +18 -7
  39. package/src/internal/prompt-checkpoint.ts +19 -13
  40. package/src/internal/storage-span.ts +22 -13
  41. package/dist/DoThreadStore-CtyahRTW.mjs.map +0 -1
  42. package/dist/do-journal-_MdLGTd3.mjs.map +0 -1
  43. package/dist/migrations-C3N9GN6A.mjs.map +0 -1
  44. package/dist/owned-state-BHzUi0eb.mjs.map +0 -1
@@ -27,6 +27,7 @@ import {
27
27
  PersistedJson,
28
28
  WorkerAdmission,
29
29
  RecordEnvelope,
30
+ RecordId,
30
31
  SettlementOutcome,
31
32
  } from "@yielded/agent/records";
32
33
  import { runIdForSubmission } from "@yielded/agent/run-journal";
@@ -189,7 +190,7 @@ class SubmissionRow extends Schema.Class<SubmissionRow>("SubmissionRow")({
189
190
  receipt_id: BoundedIdentifier,
190
191
  state: SubmissionState,
191
192
  settled_outcome: Schema.NullOr(SettlementOutcome),
192
- settled_record_id: Schema.NullOr(BoundedIdentifier),
193
+ settled_record_id: Schema.NullOr(BoundedIdentifier.pipe(Schema.decodeTo(RecordId))),
193
194
  finalized_at: Schema.NullOr(BoundedTimestamp),
194
195
  created_at: BoundedTimestamp,
195
196
  ready_at: Schema.NullOr(BoundedTimestamp),
@@ -344,8 +345,8 @@ const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
344
345
  const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
345
346
 
346
347
  const decodeSubmissionSnapshotType = (input: typeof SubmissionSnapshot.Type) =>
347
- Schema.decodeEffect(Schema.toType(Schema.Struct(SubmissionSnapshot.fields)))(input).pipe(
348
- Effect.map((fields) => SubmissionSnapshot.make(fields)),
348
+ SubmissionSnapshot.makeEffect(input).pipe(
349
+ Effect.mapError((issue) => new Schema.SchemaError(issue)),
349
350
  );
350
351
 
351
352
  const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
@@ -365,6 +366,46 @@ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
365
366
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
366
367
  const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
367
368
  const equivalentUnknownResolution = Schema.toEquivalence(UnknownResolution);
369
+ const decodeAdmissionRequestType = Schema.decodeEffect(Schema.toType(AdmissionRequest));
370
+ const decodeSubmissionLookupType = Schema.decodeEffect(Schema.toType(SubmissionLookup));
371
+ const decodeClaimRequestType = Schema.decodeEffect(Schema.toType(ClaimRequest));
372
+
373
+ const decodeMarkInputAppliedRequestType = Schema.decodeEffect(
374
+ Schema.toType(MarkInputAppliedRequest),
375
+ );
376
+
377
+ const decodeSettlementFinalizationType = Schema.decodeEffect(Schema.toType(SettlementFinalization));
378
+ const decodeClaimJoiningRequestType = Schema.decodeEffect(Schema.toType(ClaimJoiningRequest));
379
+
380
+ const decodeAbortIntentRequestType = Schema.decodeEffect(Schema.toType(AbortIntentRequest));
381
+
382
+ const decodeRecoverySnapshotRequestType = Schema.decodeEffect(
383
+ Schema.toType(RecoverySnapshotRequest),
384
+ );
385
+
386
+ const SubmissionRows = Schema.Array(SubmissionRow);
387
+ const ChildReservationRows = Schema.Array(ChildReservationRow);
388
+ const MaxQueueSequenceRows = Schema.Array(MaxQueueSequenceRow);
389
+ const SubmissionWorkItems = Schema.Array(SubmissionWorkItem);
390
+ const decodeSubmissionWorkItems = Schema.decodeEffect(SubmissionWorkItems);
391
+ const equivalentWorkerOrigin = Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin));
392
+
393
+ const decodeFirstWorkerAdmissions = Schema.decodeUnknownEffect(
394
+ Schema.Array(
395
+ Schema.Struct({
396
+ worker_admission_json: Schema.NullOr(BoundedStoredText),
397
+ }),
398
+ ),
399
+ );
400
+
401
+ const CanonicalSettlementRows = Schema.Array(
402
+ Schema.Struct({
403
+ batch_id: Schema.fromJsonString(BoundedIdentifier),
404
+ record_id: Schema.fromJsonString(BoundedIdentifier),
405
+ record_json: BoundedStoredText,
406
+ }),
407
+ );
408
+
368
409
  const isDoStorageError = Schema.is(DoStorageError);
369
410
 
370
411
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
@@ -605,7 +646,7 @@ const makeServices = Effect.fnUntraced(function* () {
605
646
  }));
606
647
 
607
648
  const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
608
- decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
649
+ decodeRows(SubmissionRows, "effect_agent_submissions", rowKey, rows).pipe(
609
650
  Effect.mapError(internalFailure(operation)),
610
651
  );
611
652
 
@@ -683,6 +724,11 @@ const makeServices = Effect.fnUntraced(function* () {
683
724
 
684
725
  const admission = yield* decodeAdmissionFact(row).pipe(Effect.mapError(decodeFailure));
685
726
 
727
+ const settledAt =
728
+ row.finalized_at === null
729
+ ? undefined
730
+ : yield* decodeUtcInstant(row.finalized_at).pipe(Effect.mapError(decodeFailure));
731
+
686
732
  const readyAt =
687
733
  row.ready_at === null
688
734
  ? undefined
@@ -692,6 +738,8 @@ const makeServices = Effect.fnUntraced(function* () {
692
738
  ...admission,
693
739
  state: row.state,
694
740
  ...(row.settled_outcome === null ? {} : { settledOutcome: row.settled_outcome }),
741
+ ...(settledAt === undefined ? {} : { settledAt }),
742
+ ...(row.settled_record_id === null ? {} : { settledRecordId: row.settled_record_id }),
695
743
  ...(readyAt === undefined ? {} : { readyAt }),
696
744
  }).pipe(Effect.mapError(decodeFailure));
697
745
 
@@ -713,13 +761,7 @@ const makeServices = Effect.fnUntraced(function* () {
713
761
  AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(sqlFailure(operation)));
714
762
 
715
763
  const decoded = yield* decodeRows(
716
- Schema.Array(
717
- Schema.Struct({
718
- batch_id: Schema.fromJsonString(BoundedIdentifier),
719
- record_id: Schema.fromJsonString(BoundedIdentifier),
720
- record_json: BoundedStoredText,
721
- }),
722
- ),
764
+ CanonicalSettlementRows,
723
765
  "effect_agent_canonical_records",
724
766
  recordId,
725
767
  found,
@@ -802,12 +844,9 @@ const makeServices = Effect.fnUntraced(function* () {
802
844
  });
803
845
 
804
846
  const decodeChildReservationRows = (operation: string, rowKey: string, rows: unknown) =>
805
- decodeRows(
806
- Schema.Array(ChildReservationRow),
807
- "effect_agent_child_reservations",
808
- rowKey,
809
- rows,
810
- ).pipe(Effect.mapError(internalFailure(operation)));
847
+ decodeRows(ChildReservationRows, "effect_agent_child_reservations", rowKey, rows).pipe(
848
+ Effect.mapError(internalFailure(operation)),
849
+ );
811
850
 
812
851
  const readChildReservation = Effect.fnUntraced(function* (
813
852
  operation: string,
@@ -1028,10 +1067,11 @@ const makeServices = Effect.fnUntraced(function* () {
1028
1067
 
1029
1068
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (
1030
1069
  request: AdmissionRequest,
1070
+ options?: { readonly readyIfInitialized?: boolean },
1031
1071
  ) {
1032
1072
  const operation = "ledger admit";
1033
1073
 
1034
- const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1074
+ const validated = yield* decodeAdmissionRequestType(request).pipe(
1035
1075
  Effect.mapError(internalFailure(operation)),
1036
1076
  );
1037
1077
 
@@ -1212,13 +1252,9 @@ const makeServices = Effect.fnUntraced(function* () {
1212
1252
  WHERE thread_id=${validated.threadId} ORDER BY queue_sequence LIMIT 1
1213
1253
  `.pipe(Effect.mapError(sqlFailure(operation)));
1214
1254
 
1215
- const first = yield* Schema.decodeUnknownEffect(
1216
- Schema.Array(
1217
- Schema.Struct({
1218
- worker_admission_json: Schema.NullOr(BoundedStoredText),
1219
- }),
1220
- ),
1221
- )(firstRows).pipe(Effect.mapError(internalFailure(operation)));
1255
+ const first = yield* decodeFirstWorkerAdmissions(firstRows).pipe(
1256
+ Effect.mapError(internalFailure(operation)),
1257
+ );
1222
1258
 
1223
1259
  if (first[0] !== undefined) {
1224
1260
  const previous =
@@ -1228,12 +1264,7 @@ const makeServices = Effect.fnUntraced(function* () {
1228
1264
  first[0].worker_admission_json,
1229
1265
  ).pipe(Effect.mapError(internalFailure(operation)));
1230
1266
 
1231
- if (
1232
- !Schema.toEquivalence(Schema.optional(WorkerAdmission.fields.origin))(
1233
- previous?.origin,
1234
- validated.workerAdmission?.origin,
1235
- )
1236
- )
1267
+ if (!equivalentWorkerOrigin(previous?.origin, validated.workerAdmission?.origin))
1237
1268
  return yield* AdmissionPolicyError.make({
1238
1269
  reason: "refused",
1239
1270
  code: "worker-origin-conflict",
@@ -1261,7 +1292,7 @@ const makeServices = Effect.fnUntraced(function* () {
1261
1292
  `.pipe(Effect.mapError(sqlFailure(operation)));
1262
1293
 
1263
1294
  const decodedMax = yield* decodeRows(
1264
- Schema.Array(MaxQueueSequenceRow),
1295
+ MaxQueueSequenceRows,
1265
1296
  "effect_agent_submissions",
1266
1297
  validated.threadId,
1267
1298
  maxRows,
@@ -1273,6 +1304,17 @@ const makeServices = Effect.fnUntraced(function* () {
1273
1304
 
1274
1305
  const now = yield* currentInstant;
1275
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
+
1276
1318
  yield* sql`
1277
1319
  INSERT INTO effect_agent_submissions (
1278
1320
  submission_id,
@@ -1288,6 +1330,7 @@ const makeServices = Effect.fnUntraced(function* () {
1288
1330
  receipt_id,
1289
1331
  state,
1290
1332
  created_at,
1333
+ ready_at,
1291
1334
  parent_submission_id,
1292
1335
  parent_tool_call_id,
1293
1336
  admission_group,
@@ -1306,8 +1349,9 @@ const makeServices = Effect.fnUntraced(function* () {
1306
1349
  ${inputJson},
1307
1350
  ${validated.inputDigest},
1308
1351
  ${mintedReceiptId},
1309
- 'admitted',
1352
+ ${ready ? "ready" : "admitted"},
1310
1353
  ${now.iso},
1354
+ ${ready ? now.iso : null},
1311
1355
  ${validated.parentLinkage?.parentSubmissionId ?? null},
1312
1356
  ${validated.parentLinkage?.parentToolCallId ?? null},
1313
1357
  ${validated.admissionGroup ?? null},
@@ -1333,6 +1377,14 @@ const makeServices = Effect.fnUntraced(function* () {
1333
1377
 
1334
1378
  // The INSERT (never replay or hydration) proves these new submission-owned sets
1335
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
+ );
1336
1388
  rows.aborts.seed("submission_id", mintedSubmissionId, []);
1337
1389
  rows.submissions.seed("joined_host_submission_id", mintedSubmissionId, []);
1338
1390
  rows.approvals.seed("submission_id", mintedSubmissionId, []);
@@ -1342,13 +1394,21 @@ const makeServices = Effect.fnUntraced(function* () {
1342
1394
 
1343
1395
  yield* recordProgress("submission", operation);
1344
1396
 
1345
- return yield* decodeAdmissionResult({
1397
+ const admitted = yield* decodeAdmissionResult({
1346
1398
  submissionId: mintedSubmissionId,
1347
1399
  receiptId: mintedReceiptId,
1348
1400
  queueSequence,
1349
- state: "admitted",
1401
+ state: ready ? "ready" : "admitted",
1350
1402
  replayed: false,
1351
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;
1352
1412
  }),
1353
1413
  );
1354
1414
 
@@ -1458,7 +1518,7 @@ const makeServices = Effect.fnUntraced(function* () {
1458
1518
  ) {
1459
1519
  const operation = "ledger lookup";
1460
1520
 
1461
- const validated = yield* Schema.decodeEffect(Schema.toType(SubmissionLookup))(request).pipe(
1521
+ const validated = yield* decodeSubmissionLookupType(request).pipe(
1462
1522
  Effect.mapError(internalFailure(operation)),
1463
1523
  );
1464
1524
 
@@ -1532,7 +1592,7 @@ const makeServices = Effect.fnUntraced(function* () {
1532
1592
  )(function* (request: ClaimRequest) {
1533
1593
  const operation = "ledger select head";
1534
1594
 
1535
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1595
+ const validated = yield* decodeClaimRequestType(request).pipe(
1536
1596
  Effect.mapError(internalFailure(operation)),
1537
1597
  );
1538
1598
 
@@ -1544,7 +1604,28 @@ const makeServices = Effect.fnUntraced(function* () {
1544
1604
  Effect.gen(function* () {
1545
1605
  if (owner.hasOwner(validated.threadId)) return Option.none<Claim>();
1546
1606
 
1547
- const headRows = yield* sql<Record<string, unknown>>`
1607
+ const active = yield* activeNonterminal;
1608
+ const heads: Array<SubmissionRow> = [];
1609
+
1610
+ const limit =
1611
+ validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1;
1612
+
1613
+ if (active.length <= 128) {
1614
+ const candidates = active
1615
+ .filter((row) => row.thread_id === validated.threadId)
1616
+ .sort((left, right) => left.queue_sequence - right.queue_sequence);
1617
+
1618
+ for (const candidate of candidates) {
1619
+ if (
1620
+ candidate.state === "unknown" &&
1621
+ Option.isNone(yield* readAbortIntent(operation, candidate.submission_id))
1622
+ )
1623
+ continue;
1624
+ heads.push(yield* requireSubmission(operation, candidate.submission_id));
1625
+ if (heads.length === limit) break;
1626
+ }
1627
+ } else {
1628
+ const headRows = yield* sql<Record<string, unknown>>`
1548
1629
  SELECT ${submissionColumns}
1549
1630
  FROM effect_agent_submissions
1550
1631
  WHERE thread_id = ${validated.threadId}
@@ -1559,7 +1640,8 @@ const makeServices = Effect.fnUntraced(function* () {
1559
1640
  LIMIT ${validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1}
1560
1641
  `.pipe(Effect.mapError(sqlFailure(operation)));
1561
1642
 
1562
- const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1643
+ heads.push(...(yield* decodeSubmissionRows(operation, validated.threadId, headRows)));
1644
+ }
1563
1645
 
1564
1646
  if (heads.length === 0) return Option.none<Claim>();
1565
1647
  let head = heads[0];
@@ -1628,9 +1710,9 @@ const makeServices = Effect.fnUntraced(function* () {
1628
1710
  function* (request: MarkInputAppliedRequest) {
1629
1711
  const operation = "ledger mark input applied";
1630
1712
 
1631
- const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1632
- request,
1633
- ).pipe(Effect.mapError(internalFailure(operation)));
1713
+ const validated = yield* decodeMarkInputAppliedRequestType(request).pipe(
1714
+ Effect.mapError(internalFailure(operation)),
1715
+ );
1634
1716
 
1635
1717
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1636
1718
  yield* inWriteTransaction(
@@ -1889,9 +1971,9 @@ const makeServices = Effect.fnUntraced(function* () {
1889
1971
  )(function* (request: SettlementFinalization) {
1890
1972
  const operation = "ledger finalize settlement";
1891
1973
 
1892
- const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1893
- request,
1894
- ).pipe(Effect.mapError(internalFailure(operation)));
1974
+ const validated = yield* decodeSettlementFinalizationType(request).pipe(
1975
+ Effect.mapError(internalFailure(operation)),
1976
+ );
1895
1977
 
1896
1978
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1897
1979
 
@@ -2203,14 +2285,13 @@ const makeServices = Effect.fnUntraced(function* () {
2203
2285
  ) {
2204
2286
  const operation = "ledger claim joining";
2205
2287
 
2206
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2288
+ const validated = yield* decodeClaimJoiningRequestType(request).pipe(
2207
2289
  Effect.mapError(internalFailure(operation)),
2208
2290
  );
2209
2291
 
2210
2292
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2211
2293
 
2212
- const claims = yield* inWriteTransaction(
2213
- operation,
2294
+ const claims = yield* owner.withMutation(
2214
2295
  Effect.gen(function* () {
2215
2296
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2216
2297
 
@@ -2230,68 +2311,92 @@ const makeServices = Effect.fnUntraced(function* () {
2230
2311
 
2231
2312
  if (stopped.length > 0) return [];
2232
2313
 
2233
- const laterRows = yield* sql<Record<string, unknown>>`
2234
- SELECT ${submissionColumns}
2235
- FROM effect_agent_submissions
2236
- WHERE thread_id = ${validated.threadId}
2237
- AND queue_sequence > ${host.queue_sequence}
2238
- ORDER BY queue_sequence ASC
2239
- `.pipe(Effect.mapError(sqlFailure(operation)));
2240
-
2241
- const later = yield* decodeSubmissionRows(operation, validated.threadId, laterRows);
2242
- const claimed: Array<JoiningClaim> = [];
2243
-
2244
- for (const row of later) {
2245
- if (claimed.length >= validated.maxCount) break;
2246
- // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
2247
- // the coordinator re-delivers already-joined input through the coverage rule.
2248
- if (
2249
- (row.state === "joining" || row.state === "joined") &&
2250
- row.joined_host_submission_id === validated.hostSubmissionId
2251
- ) {
2252
- continue;
2253
- }
2254
- // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
2255
- // settles aborted never-claimed queued work immediately, and settlement order of
2256
- // never-run work is not execution order (DUR-004 bounds execution).
2257
- if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2258
- // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2259
- // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2260
- if (row.state !== "ready") break;
2261
- const canonical = yield* readCanonicalSettlement(operation, row);
2262
-
2263
- if (canonical?.settlement.outcome === "aborted") continue;
2264
- if (canonical !== undefined) break;
2265
- yield* sql`
2314
+ // Retain only an empty complete suffix. Any later admission invalidates this view;
2315
+ // nonempty rows keep their SQL ordering, and rollback discards the empty proof.
2316
+ const later = yield* submissionViews
2317
+ .matching(
2318
+ JSON.stringify(["joining-suffix", validated.threadId, host.queue_sequence]),
2319
+ (row) =>
2320
+ row.thread_id === validated.threadId && row.queue_sequence > host.queue_sequence,
2321
+ sql`
2322
+ SELECT ${submissionColumns}
2323
+ FROM effect_agent_submissions
2324
+ WHERE thread_id = ${validated.threadId}
2325
+ AND queue_sequence > ${host.queue_sequence}
2326
+ ORDER BY queue_sequence ASC
2327
+ `,
2328
+ 0,
2329
+ )
2330
+ .pipe(
2331
+ Effect.mapError((error) =>
2332
+ error._tag === "SqlError"
2333
+ ? sqlFailure(operation)(error)
2334
+ : internalFailure(operation)(error),
2335
+ ),
2336
+ );
2337
+
2338
+ if (later.length === 0) return [];
2339
+
2340
+ // The reader gate stays held through promotion. Nonempty candidates retain
2341
+ // their native transaction, live-owner checks and contiguous-prefix rules.
2342
+ return yield* inWriteTransaction(
2343
+ operation,
2344
+ Effect.gen(function* () {
2345
+ const claimed: Array<JoiningClaim> = [];
2346
+
2347
+ for (const row of later) {
2348
+ if (claimed.length >= validated.maxCount) break;
2349
+ // Rows already claimed by THIS host extend its contiguous prefix and are skipped;
2350
+ // the coordinator re-delivers already-joined input through the coverage rule.
2351
+ if (
2352
+ (row.state === "joining" || row.state === "joined") &&
2353
+ row.joined_host_submission_id === validated.hostSubmissionId
2354
+ ) {
2355
+ continue;
2356
+ }
2357
+ // P7 §7(c): an aborted-settled row is a CLOSED obligation, not a gap — recovery
2358
+ // settles aborted never-claimed queued work immediately, and settlement order of
2359
+ // never-run work is not execution order (DUR-004 bounds execution).
2360
+ if (row.state === "settled" && row.settled_outcome === "aborted") continue;
2361
+ // Any other non-ready row — an admitted-not-ready gap in particular — breaks the
2362
+ // contiguous ready prefix (plan §2.5); later ready work stays queued (DUR-004).
2363
+ if (row.state !== "ready") break;
2364
+ const canonical = yield* readCanonicalSettlement(operation, row);
2365
+
2366
+ if (canonical?.settlement.outcome === "aborted") continue;
2367
+ if (canonical !== undefined) break;
2368
+ yield* sql`
2266
2369
  UPDATE effect_agent_submissions
2267
2370
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2268
2371
  WHERE submission_id = ${row.submission_id}
2269
2372
  RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2270
- rows.submissions.write,
2271
- Effect.mapError(internalFailure(operation)),
2272
- );
2373
+ rows.submissions.write,
2374
+ Effect.mapError(internalFailure(operation)),
2375
+ );
2273
2376
 
2274
- const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2275
- Effect.mapError((error) =>
2276
- corruptionFailure(
2277
- operation,
2278
- "effect_agent_submissions",
2279
- row.submission_id,
2280
- error.message,
2281
- ),
2282
- ),
2283
- );
2377
+ const inputPayload = yield* parseStoredJsonText(row.input_json).pipe(
2378
+ Effect.mapError((error) =>
2379
+ corruptionFailure(
2380
+ operation,
2381
+ "effect_agent_submissions",
2382
+ row.submission_id,
2383
+ error.message,
2384
+ ),
2385
+ ),
2386
+ );
2284
2387
 
2285
- claimed.push(
2286
- yield* decodeJoiningClaim({
2287
- submissionId: row.submission_id,
2288
- queueSequence: row.queue_sequence,
2289
- inputPayload,
2290
- }).pipe(Effect.mapError(internalFailure(operation))),
2291
- );
2292
- }
2388
+ claimed.push(
2389
+ yield* decodeJoiningClaim({
2390
+ submissionId: row.submission_id,
2391
+ queueSequence: row.queue_sequence,
2392
+ inputPayload,
2393
+ }).pipe(Effect.mapError(internalFailure(operation))),
2394
+ );
2395
+ }
2293
2396
 
2294
- return claimed;
2397
+ return claimed;
2398
+ }),
2399
+ );
2295
2400
  }),
2296
2401
  );
2297
2402
 
@@ -3443,7 +3548,7 @@ const makeServices = Effect.fnUntraced(function* () {
3443
3548
  ).pipe(Effect.mapError(sqlFailure(operation)));
3444
3549
 
3445
3550
  const decoded = yield* decodeRows(
3446
- Schema.Array(SubmissionWorkItem),
3551
+ SubmissionWorkItems,
3447
3552
  "effect_agent_submissions",
3448
3553
  "nonterminal_scan",
3449
3554
  rows,
@@ -3463,59 +3568,74 @@ const makeServices = Effect.fnUntraced(function* () {
3463
3568
  return [decoded, next] as const;
3464
3569
  });
3465
3570
 
3466
- const scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError> = Stream.unwrap(
3467
- state.read(
3468
- Effect.gen(function* () {
3469
- // Cache only a complete bounded active set. Large hosts keep the paged scan.
3470
- const active = yield* cached(
3471
- workRows.matching(
3472
- "nonterminal",
3473
- (row) => row.state !== "settled",
3474
- sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
3475
- deployment_id, receipt_id, state
3476
- FROM effect_agent_submissions WHERE state <> 'settled' LIMIT 129`,
3477
- 128,
3478
- ),
3479
- "ledger scan nonterminal",
3480
- );
3571
+ // Both discovery and head selection use this same complete, owner-invalidated bounded view.
3572
+ const activeNonterminal = cached(
3573
+ workRows.matching(
3574
+ "nonterminal",
3575
+ (row) => row.state !== "settled",
3576
+ sql`SELECT submission_id, thread_id, queue_sequence, principal, idempotency_key,
3577
+ deployment_id, receipt_id, state
3578
+ FROM effect_agent_submissions WHERE state <> 'settled' LIMIT 129`,
3579
+ 128,
3580
+ ),
3581
+ "ledger scan nonterminal",
3582
+ );
3481
3583
 
3482
- if (active.length > 128)
3483
- return Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3484
- undefined,
3485
- scanPage,
3486
- );
3584
+ const boundedNonterminal = state.read(
3585
+ Effect.gen(function* () {
3586
+ // Cache only a complete bounded active set. Large hosts keep the paged scan.
3587
+ const active = yield* activeNonterminal;
3487
3588
 
3488
- const sorted = [...active].sort((left, right) =>
3489
- left.thread_id < right.thread_id
3490
- ? -1
3491
- : left.thread_id > right.thread_id
3492
- ? 1
3493
- : left.queue_sequence - right.queue_sequence,
3494
- );
3589
+ if (active.length > 128) return undefined;
3495
3590
 
3496
- const work = yield* Schema.decodeEffect(Schema.Array(SubmissionWorkItem))(
3497
- sorted.map((row) => ({
3498
- submissionId: row.submission_id,
3499
- threadId: row.thread_id,
3500
- queueSequence: row.queue_sequence,
3501
- principal: row.principal,
3502
- idempotencyKey: row.idempotency_key,
3503
- deploymentId: row.deployment_id,
3504
- receiptId: row.receipt_id,
3505
- state: row.state,
3506
- })),
3507
- ).pipe(Effect.mapError(internalFailure("ledger scan nonterminal")));
3508
-
3509
- return Stream.fromIterable(work);
3510
- }),
3591
+ const sorted = [...active].sort((left, right) =>
3592
+ left.thread_id < right.thread_id
3593
+ ? -1
3594
+ : left.thread_id > right.thread_id
3595
+ ? 1
3596
+ : left.queue_sequence - right.queue_sequence,
3597
+ );
3598
+
3599
+ const work = yield* decodeSubmissionWorkItems(
3600
+ sorted.map((row) => ({
3601
+ submissionId: row.submission_id,
3602
+ threadId: row.thread_id,
3603
+ queueSequence: row.queue_sequence,
3604
+ principal: row.principal,
3605
+ idempotencyKey: row.idempotency_key,
3606
+ deploymentId: row.deployment_id,
3607
+ receiptId: row.receipt_id,
3608
+ state: row.state,
3609
+ })),
3610
+ ).pipe(Effect.mapError(internalFailure("ledger scan nonterminal")));
3611
+
3612
+ return work;
3613
+ }),
3614
+ );
3615
+
3616
+ const pagedNonterminal = Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3617
+ undefined,
3618
+ scanPage,
3619
+ );
3620
+
3621
+ // Large scans keep their bounded page lifetime and streaming backpressure.
3622
+ const scanNonterminal = Stream.unwrap(
3623
+ Effect.map(boundedNonterminal, (work) =>
3624
+ work === undefined ? pagedNonterminal : Stream.fromIterable(work),
3511
3625
  ),
3512
3626
  );
3513
3627
 
3628
+ const listNonterminal = Effect.filterOrElse(
3629
+ boundedNonterminal,
3630
+ (work) => work !== undefined,
3631
+ () => Stream.runCollect(pagedNonterminal),
3632
+ );
3633
+
3514
3634
  const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
3515
3635
  Effect.fnUntraced(function* (request) {
3516
3636
  const operation = "ledger read abort intent";
3517
3637
 
3518
- const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3638
+ const validated = yield* decodeAbortIntentRequestType(request).pipe(
3519
3639
  Effect.mapError(internalFailure(operation)),
3520
3640
  );
3521
3641
 
@@ -3540,9 +3660,9 @@ const makeServices = Effect.fnUntraced(function* () {
3540
3660
  Effect.fnUntraced(function* (request: RecoverySnapshotRequest) {
3541
3661
  const operation = "ledger load recovery snapshot";
3542
3662
 
3543
- const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3544
- request,
3545
- ).pipe(Effect.mapError(internalFailure(operation)));
3663
+ const validated = yield* decodeRecoverySnapshotRequestType(request).pipe(
3664
+ Effect.mapError(internalFailure(operation)),
3665
+ );
3546
3666
 
3547
3667
  return yield* state.read(
3548
3668
  Effect.gen(function* () {
@@ -3760,6 +3880,7 @@ const makeServices = Effect.fnUntraced(function* () {
3760
3880
  beginChildBudgetRelease,
3761
3881
  releaseChildBudget,
3762
3882
  scanNonterminal,
3883
+ listNonterminal,
3763
3884
  loadRecoverySnapshot,
3764
3885
  readAbortIntent: readAbortIntentForSubmission,
3765
3886
  }),
@@ -89,6 +89,7 @@ const layoutStatements = [
89
89
  const layoutObjects = layoutStatements.map((statement, index) => {
90
90
  const name = /^CREATE (TABLE|INDEX) ([a-z_][a-z_0-9]*)/.exec(statement);
91
91
 
92
+ // oxlint-disable-next-line effect/no-untyped-throw, effect/no-native-error -- Module initialization validates developer-authored SQL constants before any storage workflow starts.
92
93
  if (name === null) throw new Error("Invalid subscription layout statement");
93
94
 
94
95
  return [name[1].toLowerCase(), name[2], index] as const;