@yielded/agent-storage-cloudflare 0.1.0-beta.174 → 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 (37) 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.mjs +116 -65
  10. package/dist/DoSubmissionLedger.mjs.map +1 -1
  11. package/dist/{DoThreadStore-DyzOZDXK.mjs → DoThreadStore-CgIZQUNR.mjs} +24 -19
  12. package/dist/DoThreadStore-CgIZQUNR.mjs.map +1 -0
  13. package/dist/DoThreadStore.mjs +1 -1
  14. package/dist/PortProtocol.d.mts +7063 -7063
  15. package/dist/PortRouting.mjs +6 -1
  16. package/dist/PortRouting.mjs.map +1 -1
  17. package/dist/{do-journal-ZMAzzENZ.mjs → do-journal-7P3DUBz0.mjs} +131 -43
  18. package/dist/do-journal-7P3DUBz0.mjs.map +1 -0
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.mjs +1 -1
  21. package/dist/{migrations-C3N9GN6A.mjs → migrations-CAO_akhD.mjs} +45 -22
  22. package/dist/migrations-CAO_akhD.mjs.map +1 -0
  23. package/dist/{owned-state-DcFh0YNo.mjs → owned-state-B4QhezMp.mjs} +2 -1
  24. package/dist/{owned-state-DcFh0YNo.mjs.map → owned-state-B4QhezMp.mjs.map} +1 -1
  25. package/package.json +1 -1
  26. package/src/DoStorageFailpoint.ts +4 -0
  27. package/src/DoSubmissionLedger.ts +262 -149
  28. package/src/DoThreadStore.ts +18 -44
  29. package/src/PortRouting.ts +10 -2
  30. package/src/internal/do-journal.ts +286 -66
  31. package/src/internal/migrations.ts +52 -10
  32. package/src/internal/owned-state.ts +9 -1
  33. package/src/internal/prompt-checkpoint.ts +6 -3
  34. package/src/internal/storage-span.ts +14 -10
  35. package/dist/DoThreadStore-DyzOZDXK.mjs.map +0 -1
  36. package/dist/do-journal-ZMAzzENZ.mjs.map +0 -1
  37. package/dist/migrations-C3N9GN6A.mjs.map +0 -1
@@ -345,8 +345,8 @@ const decodeAbortIntent = Schema.decodeUnknownEffect(AbortIntent);
345
345
  const decodeInputAppliedMarker = Schema.decodeUnknownEffect(InputAppliedMarker);
346
346
 
347
347
  const decodeSubmissionSnapshotType = (input: typeof SubmissionSnapshot.Type) =>
348
- Schema.decodeEffect(Schema.toType(Schema.Struct(SubmissionSnapshot.fields)))(input).pipe(
349
- Effect.map((fields) => SubmissionSnapshot.make(fields)),
348
+ SubmissionSnapshot.makeEffect(input).pipe(
349
+ Effect.mapError((issue) => new Schema.SchemaError(issue)),
350
350
  );
351
351
 
352
352
  const decodeSubmissionId = Schema.decodeUnknownEffect(SubmissionSnapshot.fields.submissionId);
@@ -366,6 +366,46 @@ const decodeChildReservationSnapshotUnknown = Schema.decodeUnknownEffect(
366
366
  const decodeChildAttachmentSnapshot = Schema.decodeUnknownEffect(ChildAttachmentSnapshot);
367
367
  const equivalentPersistedJson = Schema.toEquivalence(PersistedJson);
368
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
+
369
409
  const isDoStorageError = Schema.is(DoStorageError);
370
410
 
371
411
  /** Wrap an adapter-internal failure into the port's LedgerError without erasing its tag. */
@@ -606,7 +646,7 @@ const makeServices = Effect.fnUntraced(function* () {
606
646
  }));
607
647
 
608
648
  const decodeSubmissionRows = (operation: string, rowKey: string, rows: unknown) =>
609
- decodeRows(Schema.Array(SubmissionRow), "effect_agent_submissions", rowKey, rows).pipe(
649
+ decodeRows(SubmissionRows, "effect_agent_submissions", rowKey, rows).pipe(
610
650
  Effect.mapError(internalFailure(operation)),
611
651
  );
612
652
 
@@ -721,13 +761,7 @@ const makeServices = Effect.fnUntraced(function* () {
721
761
  AND record_id = ${JSON.stringify(recordId)}`.pipe(Effect.mapError(sqlFailure(operation)));
722
762
 
723
763
  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
- ),
764
+ CanonicalSettlementRows,
731
765
  "effect_agent_canonical_records",
732
766
  recordId,
733
767
  found,
@@ -810,12 +844,9 @@ const makeServices = Effect.fnUntraced(function* () {
810
844
  });
811
845
 
812
846
  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)));
847
+ decodeRows(ChildReservationRows, "effect_agent_child_reservations", rowKey, rows).pipe(
848
+ Effect.mapError(internalFailure(operation)),
849
+ );
819
850
 
820
851
  const readChildReservation = Effect.fnUntraced(function* (
821
852
  operation: string,
@@ -1036,10 +1067,11 @@ const makeServices = Effect.fnUntraced(function* () {
1036
1067
 
1037
1068
  const admit: SubmissionLedger["Service"]["admit"] = Effect.fnUntraced(function* (
1038
1069
  request: AdmissionRequest,
1070
+ options?: { readonly readyIfInitialized?: boolean },
1039
1071
  ) {
1040
1072
  const operation = "ledger admit";
1041
1073
 
1042
- const validated = yield* Schema.decodeEffect(Schema.toType(AdmissionRequest))(request).pipe(
1074
+ const validated = yield* decodeAdmissionRequestType(request).pipe(
1043
1075
  Effect.mapError(internalFailure(operation)),
1044
1076
  );
1045
1077
 
@@ -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
 
@@ -1540,7 +1592,7 @@ const makeServices = Effect.fnUntraced(function* () {
1540
1592
  )(function* (request: ClaimRequest) {
1541
1593
  const operation = "ledger select head";
1542
1594
 
1543
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimRequest))(request).pipe(
1595
+ const validated = yield* decodeClaimRequestType(request).pipe(
1544
1596
  Effect.mapError(internalFailure(operation)),
1545
1597
  );
1546
1598
 
@@ -1552,7 +1604,28 @@ const makeServices = Effect.fnUntraced(function* () {
1552
1604
  Effect.gen(function* () {
1553
1605
  if (owner.hasOwner(validated.threadId)) return Option.none<Claim>();
1554
1606
 
1555
- 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>>`
1556
1629
  SELECT ${submissionColumns}
1557
1630
  FROM effect_agent_submissions
1558
1631
  WHERE thread_id = ${validated.threadId}
@@ -1567,7 +1640,8 @@ const makeServices = Effect.fnUntraced(function* () {
1567
1640
  LIMIT ${validated.handoff === undefined ? 1 : validated.handoff.deferredSubmissionIds.length + 1}
1568
1641
  `.pipe(Effect.mapError(sqlFailure(operation)));
1569
1642
 
1570
- const heads = yield* decodeSubmissionRows(operation, validated.threadId, headRows);
1643
+ heads.push(...(yield* decodeSubmissionRows(operation, validated.threadId, headRows)));
1644
+ }
1571
1645
 
1572
1646
  if (heads.length === 0) return Option.none<Claim>();
1573
1647
  let head = heads[0];
@@ -1636,9 +1710,9 @@ const makeServices = Effect.fnUntraced(function* () {
1636
1710
  function* (request: MarkInputAppliedRequest) {
1637
1711
  const operation = "ledger mark input applied";
1638
1712
 
1639
- const validated = yield* Schema.decodeEffect(Schema.toType(MarkInputAppliedRequest))(
1640
- request,
1641
- ).pipe(Effect.mapError(internalFailure(operation)));
1713
+ const validated = yield* decodeMarkInputAppliedRequestType(request).pipe(
1714
+ Effect.mapError(internalFailure(operation)),
1715
+ );
1642
1716
 
1643
1717
  yield* hitFailpoint("ledger:mark-input-applied:before", operation);
1644
1718
  yield* inWriteTransaction(
@@ -1897,9 +1971,9 @@ const makeServices = Effect.fnUntraced(function* () {
1897
1971
  )(function* (request: SettlementFinalization) {
1898
1972
  const operation = "ledger finalize settlement";
1899
1973
 
1900
- const validated = yield* Schema.decodeEffect(Schema.toType(SettlementFinalization))(
1901
- request,
1902
- ).pipe(Effect.mapError(internalFailure(operation)));
1974
+ const validated = yield* decodeSettlementFinalizationType(request).pipe(
1975
+ Effect.mapError(internalFailure(operation)),
1976
+ );
1903
1977
 
1904
1978
  yield* hitFailpoint("ledger:finalize-settlement:before", operation);
1905
1979
 
@@ -2211,14 +2285,13 @@ const makeServices = Effect.fnUntraced(function* () {
2211
2285
  ) {
2212
2286
  const operation = "ledger claim joining";
2213
2287
 
2214
- const validated = yield* Schema.decodeEffect(Schema.toType(ClaimJoiningRequest))(request).pipe(
2288
+ const validated = yield* decodeClaimJoiningRequestType(request).pipe(
2215
2289
  Effect.mapError(internalFailure(operation)),
2216
2290
  );
2217
2291
 
2218
2292
  yield* hitFailpoint("ledger:claim-joining:before", operation);
2219
2293
 
2220
- const claims = yield* inWriteTransaction(
2221
- operation,
2294
+ const claims = yield* owner.withMutation(
2222
2295
  Effect.gen(function* () {
2223
2296
  const host = yield* requireSubmission(operation, validated.hostSubmissionId);
2224
2297
 
@@ -2238,68 +2311,92 @@ const makeServices = Effect.fnUntraced(function* () {
2238
2311
 
2239
2312
  if (stopped.length > 0) return [];
2240
2313
 
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`
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`
2274
2369
  UPDATE effect_agent_submissions
2275
2370
  SET state = 'joining', joined_host_submission_id = ${validated.hostSubmissionId}
2276
2371
  WHERE submission_id = ${row.submission_id}
2277
2372
  RETURNING *, ${definitionJson(sql)} AS agent_digests_json`.pipe(
2278
- rows.submissions.write,
2279
- Effect.mapError(internalFailure(operation)),
2280
- );
2373
+ rows.submissions.write,
2374
+ Effect.mapError(internalFailure(operation)),
2375
+ );
2281
2376
 
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
- );
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
+ );
2292
2387
 
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
- }
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
+ }
2301
2396
 
2302
- return claimed;
2397
+ return claimed;
2398
+ }),
2399
+ );
2303
2400
  }),
2304
2401
  );
2305
2402
 
@@ -3451,7 +3548,7 @@ const makeServices = Effect.fnUntraced(function* () {
3451
3548
  ).pipe(Effect.mapError(sqlFailure(operation)));
3452
3549
 
3453
3550
  const decoded = yield* decodeRows(
3454
- Schema.Array(SubmissionWorkItem),
3551
+ SubmissionWorkItems,
3455
3552
  "effect_agent_submissions",
3456
3553
  "nonterminal_scan",
3457
3554
  rows,
@@ -3471,59 +3568,74 @@ const makeServices = Effect.fnUntraced(function* () {
3471
3568
  return [decoded, next] as const;
3472
3569
  });
3473
3570
 
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
- );
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
+ );
3489
3583
 
3490
- if (active.length > 128)
3491
- return Stream.paginate<ScanCursor | undefined, SubmissionWorkItem, LedgerError>(
3492
- undefined,
3493
- scanPage,
3494
- );
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;
3495
3588
 
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
- );
3589
+ if (active.length > 128) return undefined;
3503
3590
 
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
- }),
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),
3519
3625
  ),
3520
3626
  );
3521
3627
 
3628
+ const listNonterminal = Effect.filterOrElse(
3629
+ boundedNonterminal,
3630
+ (work) => work !== undefined,
3631
+ () => Stream.runCollect(pagedNonterminal),
3632
+ );
3633
+
3522
3634
  const readAbortIntentForSubmission: SubmissionLedger["Service"]["readAbortIntent"] =
3523
3635
  Effect.fnUntraced(function* (request) {
3524
3636
  const operation = "ledger read abort intent";
3525
3637
 
3526
- const validated = yield* Schema.decodeEffect(Schema.toType(AbortIntentRequest))(request).pipe(
3638
+ const validated = yield* decodeAbortIntentRequestType(request).pipe(
3527
3639
  Effect.mapError(internalFailure(operation)),
3528
3640
  );
3529
3641
 
@@ -3548,9 +3660,9 @@ const makeServices = Effect.fnUntraced(function* () {
3548
3660
  Effect.fnUntraced(function* (request: RecoverySnapshotRequest) {
3549
3661
  const operation = "ledger load recovery snapshot";
3550
3662
 
3551
- const validated = yield* Schema.decodeEffect(Schema.toType(RecoverySnapshotRequest))(
3552
- request,
3553
- ).pipe(Effect.mapError(internalFailure(operation)));
3663
+ const validated = yield* decodeRecoverySnapshotRequestType(request).pipe(
3664
+ Effect.mapError(internalFailure(operation)),
3665
+ );
3554
3666
 
3555
3667
  return yield* state.read(
3556
3668
  Effect.gen(function* () {
@@ -3768,6 +3880,7 @@ const makeServices = Effect.fnUntraced(function* () {
3768
3880
  beginChildBudgetRelease,
3769
3881
  releaseChildBudget,
3770
3882
  scanNonterminal,
3883
+ listNonterminal,
3771
3884
  loadRecoverySnapshot,
3772
3885
  readAbortIntent: readAbortIntentForSubmission,
3773
3886
  }),