effect-agent 0.1.0-beta.111 → 0.1.0-beta.113

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 (67) hide show
  1. package/dist/{DurableAgentRuntime-PyIgNYXF.mjs → DurableAgentRuntime-BIrgxwRl.mjs} +154 -21
  2. package/dist/DurableAgentRuntime-BIrgxwRl.mjs.map +1 -0
  3. package/dist/{EventSource-BUYLmL77.d.mts → EventSource-ARs56eBq.d.mts} +3 -3
  4. package/dist/{PreparedInputAdmission-J3YK6LAm.d.mts → PreparedInputAdmission-_WYg-D0f.d.mts} +4 -4
  5. package/dist/{Records-4qnRmniE.d.mts → Records-BtxMy8xD.d.mts} +94 -13
  6. package/dist/{Schedule-C2lH2kxY.d.mts → Schedule-DGPyXBzK.d.mts} +2 -2
  7. package/dist/{Subscription-pQUv0QD6.d.mts → Subscription-B9VszlF9.d.mts} +3 -3
  8. package/dist/{SubscriptionInput-DdJOiNMY.d.mts → SubscriptionInput-BJWZqCWP.d.mts} +3 -3
  9. package/dist/capabilities/Mcp.d.mts +1 -1
  10. package/dist/capabilities/Mcp.mjs +5 -6
  11. package/dist/capabilities/Mcp.mjs.map +1 -1
  12. package/dist/durable/ActivityStore.d.mts +1 -1
  13. package/dist/durable/Admin.d.mts +1 -1
  14. package/dist/durable/AgentRegistration.d.mts +1 -1
  15. package/dist/durable/Certification.d.mts +1 -1
  16. package/dist/durable/CommittedActivity.d.mts +1 -1
  17. package/dist/durable/Digest.d.mts +1 -1
  18. package/dist/durable/DurableAgentRuntime.d.mts +2 -2
  19. package/dist/durable/DurableAgentRuntime.mjs +2 -2
  20. package/dist/durable/EventSource.d.mts +1 -1
  21. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  22. package/dist/durable/MessageDelivery.d.mts +3 -3
  23. package/dist/durable/MessageDeliveryStoreConformance.d.mts +39 -39
  24. package/dist/durable/PersistentHistory.d.mts +1 -1
  25. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  26. package/dist/durable/Records.d.mts +1 -1
  27. package/dist/durable/Recovery.d.mts +1 -1
  28. package/dist/durable/RunJournal.d.mts +1 -1
  29. package/dist/durable/Schedule.d.mts +1 -1
  30. package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
  31. package/dist/durable/ScheduleTransition.d.mts +1 -1
  32. package/dist/durable/Scheduling.d.mts +2 -2
  33. package/dist/durable/SqlStorageV2Upgrade.d.mts +2 -2
  34. package/dist/durable/SqlSubscriptionStore.d.mts +2 -2
  35. package/dist/durable/SqlThreadNativeReads.d.mts +1 -1
  36. package/dist/durable/SubmissionLedger.d.mts +2 -2
  37. package/dist/durable/SubmissionLedger.mjs +19 -8
  38. package/dist/durable/SubmissionLedger.mjs.map +1 -1
  39. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  40. package/dist/durable/SubmissionLedgerConformance.mjs +2 -1
  41. package/dist/durable/SubmissionLedgerConformance.mjs.map +1 -1
  42. package/dist/durable/SubmissionStatus.d.mts +1 -1
  43. package/dist/durable/Subscription.d.mts +1 -1
  44. package/dist/durable/SubscriptionInput.d.mts +1 -1
  45. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  46. package/dist/durable/SubscriptionTools.d.mts +3 -3
  47. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  48. package/dist/durable/Subscriptions.d.mts +5 -5
  49. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  50. package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
  51. package/dist/durable/ThreadInvariants.d.mts +1 -1
  52. package/dist/durable/ThreadProjection.d.mts +1 -1
  53. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  54. package/dist/durable/ThreadStore.d.mts +2 -2
  55. package/dist/durable/ThreadStore.mjs +16 -2
  56. package/dist/durable/ThreadStore.mjs.map +1 -1
  57. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  58. package/dist/durable/WorkerHost.d.mts +1 -1
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +1 -1
  61. package/package.json +1 -1
  62. package/src/capabilities/Mcp.ts +6 -7
  63. package/src/durable/DurableAgentRuntime.ts +278 -49
  64. package/src/durable/SubmissionLedger.ts +22 -9
  65. package/src/durable/SubmissionLedgerConformance.ts +13 -1
  66. package/src/durable/ThreadStore.ts +16 -0
  67. package/dist/DurableAgentRuntime-PyIgNYXF.mjs.map +0 -1
@@ -9,9 +9,11 @@ import {
9
9
  Duration,
10
10
  Effect,
11
11
  Equal,
12
+ Exit,
12
13
  Layer,
13
14
  Option,
14
15
  Ref,
16
+ Result,
15
17
  Schedule,
16
18
  Schema,
17
19
  Semaphore,
@@ -343,6 +345,7 @@ import {
343
345
  ThreadObservation,
344
346
  ThreadRead,
345
347
  ThreadStore,
348
+ ThreadStoreDiagnostic,
346
349
  ThreadStoreError,
347
350
  ThreadTailRequest,
348
351
  FencedAppendRequest,
@@ -554,6 +557,183 @@ export const recoveryRepairRecordId = (submissionId: SubmissionId, decisionTag:
554
557
  */
555
558
  export { Receipt } from "../core/Receipt.ts";
556
559
 
560
+ // Foreign names and tags may contain payloads. Retain only these static classifications.
561
+ const RecoveryCauseTag = Schema.Literals([
562
+ "AdmissionPolicyError",
563
+ "DigestError",
564
+ "LedgerError",
565
+ "OwnershipLost",
566
+ "SettlementConflict",
567
+ "ThreadStoreError",
568
+ "ThreadNotMaterialized",
569
+ "AppendConflict",
570
+ "FenceRejected",
571
+ "RunJournalError",
572
+ "DurableRuntimeFailpointError",
573
+ "SchemaError",
574
+ "Error",
575
+ "TypeError",
576
+ "RangeError",
577
+ "ReferenceError",
578
+ "SyntaxError",
579
+ "URIError",
580
+ "EvalError",
581
+ "AggregateError",
582
+ "ForeignError",
583
+ "NonErrorCause",
584
+ ]);
585
+
586
+ const isRecoveryCauseTag = Schema.is(RecoveryCauseTag);
587
+
588
+ /** Recovery could not establish execution authority. This is never a Settlement or replay grant. */
589
+ export class RecoveryFailure extends Schema.Class<RecoveryFailure>(
590
+ "@effect-agent/thread/RecoveryFailure",
591
+ )({
592
+ phase: Schema.Literals(["history", "recovery"]),
593
+ reason: Schema.Literals(["failure", "defect", "timeout"]),
594
+ errorTag: Schema.String.check(Schema.isMaxLength(128)),
595
+ /** Static operation names only; error messages and payloads are deliberately excluded. */
596
+ operation: Schema.String.check(Schema.isMaxLength(256)),
597
+ /** Ordered, bounded classifications. Foreign labels, operations and prose stay private. */
598
+ causes: Schema.Array(
599
+ Schema.Struct({
600
+ errorTag: RecoveryCauseTag,
601
+ }),
602
+ ).check(Schema.isMaxLength(16)),
603
+ causesTruncated: Schema.optionalKey(Schema.Literal(true)),
604
+ diagnostic: Schema.optionalKey(ThreadStoreDiagnostic),
605
+ }) {}
606
+
607
+ const decodeRecoveryCause = Schema.decodeUnknownOption(
608
+ Schema.Struct({
609
+ diagnostic: Schema.optionalKey(Schema.Unknown),
610
+ cause: Schema.optionalKey(Schema.Unknown),
611
+ }),
612
+ );
613
+
614
+ /** Reuse the bounded foreign-error capture, then retain only content-free causal metadata. */
615
+ const recoveryFailureDetails = (cause: Cause.Cause<DurableWorkerFailure>) => {
616
+ const pending = [FailureDiagnostic.capture(cause)];
617
+ const causes: Array<RecoveryFailure["causes"][number]> = [];
618
+ let truncated = false;
619
+
620
+ while (pending.length > 0 && causes.length < 16) {
621
+ const node = pending.shift();
622
+
623
+ if (node === undefined) break;
624
+ switch (node._tag) {
625
+ case "Cause":
626
+ pending.unshift(
627
+ ...node.reasons.flatMap((reason) =>
628
+ reason._tag === "Fail" ? [reason.error] : reason._tag === "Die" ? [reason.defect] : [],
629
+ ),
630
+ );
631
+ truncated ||= node.truncated === true;
632
+ break;
633
+ case "Error": {
634
+ const tag = node.errorTag ?? node.name;
635
+
636
+ causes.push({
637
+ errorTag: isRecoveryCauseTag(tag) ? tag : "ForeignError",
638
+ });
639
+ pending.unshift(
640
+ ...(node.cause === undefined ? [] : [node.cause]),
641
+ ...(node.reason === undefined ? [] : [node.reason]),
642
+ ...(node.errors ?? []),
643
+ );
644
+ truncated ||= node.truncated === true;
645
+ break;
646
+ }
647
+ case "Omitted":
648
+ truncated = true;
649
+ break;
650
+ case "Value":
651
+ causes.push({ errorTag: "NonErrorCause" });
652
+ break;
653
+ }
654
+ }
655
+
656
+ // Retain only adapter-created diagnostics, never reify a foreign structural lookalike.
657
+ // Foreign wrappers retain only approved classifications above, never their operation text.
658
+ let nested: unknown = Option.getOrUndefined(Cause.findErrorOption(cause));
659
+ let diagnostic: ThreadStoreDiagnostic | undefined;
660
+
661
+ for (let depth = 0; depth < 16 && nested !== undefined; depth++) {
662
+ const decoded = decodeRecoveryCause(nested);
663
+
664
+ if (Option.isNone(decoded)) break;
665
+ if (decoded.value.diagnostic instanceof ThreadStoreDiagnostic) {
666
+ diagnostic = decoded.value.diagnostic;
667
+ break;
668
+ }
669
+ nested = decoded.value.cause;
670
+ }
671
+
672
+ return {
673
+ causes,
674
+ ...(truncated || pending.length > 0 ? { causesTruncated: true as const } : {}),
675
+ ...(diagnostic === undefined ? {} : { diagnostic }),
676
+ };
677
+ };
678
+
679
+ /** @internal One bounded failure boundary for execution history and host recovery control reads. */
680
+ export const isolateRecovery = Effect.fnUntraced(function* <A, E extends DurableWorkerFailure, R>(
681
+ body: Effect.Effect<A, E, R>,
682
+ options: {
683
+ readonly timeout: Duration.Duration;
684
+ readonly phase: () => RecoveryFailure["phase"];
685
+ readonly operation?: string;
686
+ },
687
+ ): Effect.fn.Return<Result.Result<A, RecoveryFailure>, E, Exclude<R, Scope.Scope>> {
688
+ const outcome = yield* body.pipe(
689
+ Effect.scoped,
690
+ Effect.timeoutOption(options.timeout),
691
+ Effect.exit,
692
+ );
693
+
694
+ if (Exit.isSuccess(outcome) && Option.isSome(outcome.value))
695
+ return Result.succeed(outcome.value.value);
696
+ if (
697
+ Exit.isFailure(outcome) &&
698
+ (Cause.hasInterrupts(outcome.cause) ||
699
+ outcome.cause.reasons.some(
700
+ (reason) => reason._tag === "Fail" && reason.error._tag === "DurableRuntimeFailpointError",
701
+ ))
702
+ )
703
+ return yield* Effect.failCause(outcome.cause);
704
+
705
+ const error = Exit.isFailure(outcome) ? Cause.findErrorOption(outcome.cause) : Option.none<E>();
706
+ const phase = options.phase();
707
+
708
+ return Result.fail(
709
+ RecoveryFailure.make({
710
+ phase,
711
+ reason: Exit.isSuccess(outcome)
712
+ ? "timeout"
713
+ : Cause.hasDies(outcome.cause)
714
+ ? "defect"
715
+ : "failure",
716
+ errorTag: Option.isSome(error)
717
+ ? error.value._tag
718
+ : Exit.isSuccess(outcome)
719
+ ? "RecoveryTimeout"
720
+ : "Defect",
721
+ operation:
722
+ Option.isSome(error) && "operation" in error.value
723
+ ? error.value.operation.slice(0, 256)
724
+ : (options.operation ??
725
+ (phase === "history" ? "read recovery history" : "recover submission")),
726
+ ...(Exit.isFailure(outcome) ? recoveryFailureDetails(outcome.cause) : { causes: [] }),
727
+ }),
728
+ );
729
+ });
730
+
731
+ /** One failed Thread remains pending; the host must not claim it before recovery succeeds. */
732
+ export class RecoveryBlocked extends Schema.TaggedError<RecoveryBlocked>()("RecoveryBlocked", {
733
+ threadId: ThreadId,
734
+ failure: RecoveryFailure,
735
+ }) {}
736
+
557
737
  /** One executed (or deliberately deferred) recovery decision (durability §14, DUR-013). */
558
738
  export class RecoveryReport extends Schema.Class<RecoveryReport>(
559
739
  "@effect-agent/thread/RecoveryReport",
@@ -570,6 +750,18 @@ export class RecoveryReport extends Schema.Class<RecoveryReport>(
570
750
  disposition: Schema.Literals(["repaired", "deferred", "none", "unknown"]),
571
751
  }) {}
572
752
 
753
+ /** Submission decisions and operational faults have different ownership and settlement semantics. */
754
+ export class RecoverySweepResult extends Schema.Class<RecoverySweepResult>("RecoverySweepResult")({
755
+ reports: Schema.Array(RecoveryReport),
756
+ /** Exactly one fault per failed Thread, regardless of its queued Submission count. */
757
+ blocked: Schema.Array(RecoveryBlocked),
758
+ }) {}
759
+
760
+ /** Select one Thread before reading history, or omit to recover every pending Thread. */
761
+ export interface RecoverySweepOptions {
762
+ readonly threadId?: ThreadId;
763
+ }
764
+
573
765
  /** Per-submission options accepted by `DurableAgentRuntime.submit` (D2). */
574
766
  export interface DurableSubmitOptions {
575
767
  readonly threadId: ThreadId;
@@ -731,6 +923,8 @@ export interface DurableRuntimeConfigOptions {
731
923
  readonly leaseRenewalInterval?: Duration.Duration | undefined;
732
924
  /** Active-Run abort-intent poll cadence (default 500ms). */
733
925
  readonly abortPollInterval?: Duration.Duration | undefined;
926
+ /** Cooperative bound for one Thread's recovery, including child reads (default 30s). */
927
+ readonly recoveryTimeout?: Duration.Duration | undefined;
734
928
  /** Deployment-owned model pricing authority, captured for every recoverable Run. */
735
929
  readonly estimateCostMicrousd?: RunCostEstimator | undefined;
736
930
  }
@@ -744,6 +938,7 @@ export class DurableRuntimeConfig extends Context.Service<
744
938
  readonly settlementPollInterval: Duration.Duration;
745
939
  readonly leaseRenewalInterval: Duration.Duration;
746
940
  readonly abortPollInterval: Duration.Duration;
941
+ readonly recoveryTimeout: Duration.Duration;
747
942
  readonly estimateCostMicrousd?: RunCostEstimator | undefined;
748
943
  }
749
944
  >()("@effect-agent/thread/DurableRuntimeConfig") {
@@ -754,6 +949,7 @@ export class DurableRuntimeConfig extends Context.Service<
754
949
  settlementPollInterval: options.settlementPollInterval ?? Duration.millis(500),
755
950
  leaseRenewalInterval: options.leaseRenewalInterval ?? Duration.seconds(10),
756
951
  abortPollInterval: options.abortPollInterval ?? Duration.millis(500),
952
+ recoveryTimeout: options.recoveryTimeout ?? Duration.seconds(30),
757
953
  ...(options.estimateCostMicrousd === undefined
758
954
  ? {}
759
955
  : { estimateCostMicrousd: options.estimateCostMicrousd }),
@@ -9511,50 +9707,69 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
9511
9707
  return yield* recoverSnapshot(found.value, history);
9512
9708
  });
9513
9709
 
9514
- const runRecoveryImpl = Effect.fn("DurableAgentRuntime.runRecovery")(
9515
- function* (): Effect.fn.Return<ReadonlyArray<RecoveryReport>, DurableWorkerFailure> {
9516
- const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
9517
- const reports: Array<RecoveryReport> = [];
9518
- // SubmissionLedger guarantees `(threadId, queueSequence)` order. Capture and retain
9519
- // one verified canonical prefix only for the current contiguous Thread group: read
9520
- // work is one tail inspection plus `ceil(passStartTail / READ_PAGE)` pages per Thread
9521
- // rather than multiplied by its nonterminal Submission count, and the prefix becomes
9522
- // unreachable before the next Thread is read.
9523
- let index = 0;
9524
-
9525
- while (index < nonterminal.length) {
9526
- const first = nonterminal[index];
9527
-
9528
- if (first === undefined) break;
9529
- const submissionIds: Array<SubmissionId> = [];
9530
- let hasStandardReporting = false;
9531
-
9532
- for (let offset = index; offset < nonterminal.length; offset += 1) {
9533
- const entry = nonterminal[offset];
9534
-
9535
- if (entry === undefined || entry.threadId !== first.threadId) break;
9536
- submissionIds.push(entry.submissionId);
9537
- hasStandardReporting ||= entry.workerAdmission?.origin.reporting?.mode === "standard";
9538
- }
9539
- const history = yield* readRecoveryHistory(first.threadId, submissionIds);
9710
+ const runRecovery = Effect.fn("DurableAgentRuntime.runRecovery")(function* (
9711
+ options?: RecoverySweepOptions,
9712
+ ): Effect.fn.Return<RecoverySweepResult, DurableWorkerFailure> {
9713
+ const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
9714
+ const reports: Array<RecoveryReport> = [];
9715
+ const blocked: Array<RecoveryBlocked> = [];
9716
+ // SubmissionLedger guarantees `(threadId, queueSequence)` order. Capture and retain
9717
+ // one verified canonical prefix only for the current contiguous Thread group: read
9718
+ // work is one tail inspection plus `ceil(passStartTail / READ_PAGE)` pages per Thread
9719
+ // rather than multiplied by its nonterminal Submission count, and the prefix becomes
9720
+ // unreachable before the next Thread is read.
9721
+ let index = 0;
9722
+
9723
+ while (index < nonterminal.length) {
9724
+ const first = nonterminal[index];
9725
+
9726
+ if (first === undefined) break;
9727
+ const submissionIds: Array<SubmissionId> = [];
9728
+
9729
+ for (let offset = index; offset < nonterminal.length; offset += 1) {
9730
+ const entry = nonterminal[offset];
9731
+
9732
+ if (entry === undefined || entry.threadId !== first.threadId) break;
9733
+ submissionIds.push(entry.submissionId);
9734
+ }
9540
9735
 
9541
- if (history.materialized && hasStandardReporting)
9542
- yield* updateRuntime.repair(first.threadId);
9736
+ index += submissionIds.length;
9737
+ if (options?.threadId !== undefined && options.threadId !== first.threadId) continue;
9738
+ let phase: RecoveryFailure["phase"] = "recovery";
9543
9739
 
9544
- while (index < nonterminal.length) {
9545
- const submission = nonterminal[index];
9740
+ // A retained record or child read can fail before any new Attempt exists. Isolate the
9741
+ // entire Thread: a partial repair never grants a later head permission to run through
9742
+ // incomplete evidence. Scope/timeout release resources before the next Thread starts.
9743
+ const outcome = yield* isolateRecovery(
9744
+ Effect.gen(function* () {
9745
+ // The worklist contains only control state. Retained input/worker metadata belongs
9746
+ // to this Thread's failure boundary, never to global discovery or fresh admission.
9747
+ const group = yield* Effect.forEach(submissionIds, (submissionId) =>
9748
+ lookupKnownSubmission("recover submission", submissionId),
9749
+ );
9546
9750
 
9547
- if (submission === undefined || submission.threadId !== first.threadId) {
9548
- break;
9549
- }
9550
- reports.push(yield* recoverSnapshot(submission, history));
9551
- index += 1;
9552
- }
9553
- }
9751
+ phase = "history";
9752
+ const history = yield* readRecoveryHistory(first.threadId, submissionIds);
9554
9753
 
9555
- return reports;
9556
- },
9557
- );
9754
+ phase = "recovery";
9755
+ if (
9756
+ history.materialized &&
9757
+ group.some((row) => row.workerAdmission?.origin.reporting?.mode === "standard")
9758
+ )
9759
+ yield* updateRuntime.repair(first.threadId);
9760
+
9761
+ return yield* Effect.forEach(group, (submission) => recoverSnapshot(submission, history));
9762
+ }),
9763
+ { timeout: config.recoveryTimeout, phase: () => phase },
9764
+ );
9765
+
9766
+ if (Result.isSuccess(outcome)) reports.push(...outcome.success);
9767
+ else
9768
+ blocked.push(RecoveryBlocked.make({ threadId: first.threadId, failure: outcome.failure }));
9769
+ }
9770
+
9771
+ return RecoverySweepResult.make({ reports, blocked });
9772
+ });
9558
9773
 
9559
9774
  const submit = Effect.fn("DurableAgentRuntime.submit")(function* <InputSchema extends Schema.Top>(
9560
9775
  agent: DurableSubmitAgent<InputSchema>,
@@ -10189,7 +10404,11 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10189
10404
 
10190
10405
  for (const submission of nonterminal) {
10191
10406
  if (submission.threadId !== threadId) continue;
10192
- explanations.push(yield* explainSubmission(submission));
10407
+ explanations.push(
10408
+ yield* explainSubmission(
10409
+ yield* lookupKnownSubmission("explain Thread", submission.submissionId),
10410
+ ),
10411
+ );
10193
10412
  }
10194
10413
 
10195
10414
  return explanations;
@@ -10207,13 +10426,11 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10207
10426
  // everything settled (DUR-015).
10208
10427
  const rows = new Map<SubmissionId, SubmissionSnapshot>();
10209
10428
  const nonterminal = yield* Stream.runCollect(ledger.scanNonterminal);
10429
+ const named = new Set<SubmissionId>();
10210
10430
 
10211
10431
  for (const submission of nonterminal) {
10212
- if (submission.threadId === threadId) {
10213
- rows.set(submission.submissionId, submission);
10214
- }
10432
+ if (submission.threadId === threadId) named.add(submission.submissionId);
10215
10433
  }
10216
- const named = new Set<SubmissionId>();
10217
10434
 
10218
10435
  for (const envelope of exported.records) {
10219
10436
  const payload = envelope.record.payload;
@@ -10400,11 +10617,13 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10400
10617
  const generatedAt = yield* nowUtc;
10401
10618
  const entries: Array<ObligationEntry> = [];
10402
10619
 
10403
- for (const submission of nonterminal) {
10620
+ for (const entry of nonterminal) {
10404
10621
  const snapshot = yield* ledger.loadRecoverySnapshot(
10405
- RecoverySnapshotRequest.make({ submissionId: submission.submissionId }),
10622
+ RecoverySnapshotRequest.make({ submissionId: entry.submissionId }),
10406
10623
  );
10407
10624
 
10625
+ const submission = snapshot.submission;
10626
+
10408
10627
  let blockedOn: ObligationBlockedOn;
10409
10628
  let since: DateTime.Utc = submission.readyAt ?? submission.createdAt;
10410
10629
 
@@ -10598,7 +10817,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10598
10817
  processThreadHead: processThreadHeadImpl,
10599
10818
  runWorker: runWorkerImpl,
10600
10819
  runResolvedWorker: runResolvedWorkerImpl,
10601
- runRecovery: runRecoveryImpl(),
10820
+ runRecovery,
10602
10821
  recoverSubmission,
10603
10822
  });
10604
10823
  });
@@ -10894,7 +11113,17 @@ export class DurableAgentRuntime extends Context.Service<
10894
11113
  >
10895
11114
  >;
10896
11115
  readonly runResolvedWorker: Effect.Effect<void, DurableWorkerFailure | DurableBindingFailure>;
10897
- readonly runRecovery: Effect.Effect<ReadonlyArray<RecoveryReport>, DurableWorkerFailure>;
11116
+ /**
11117
+ * Recover each pending Thread independently. History/child failures, defects and the
11118
+ * configured timeout return one RecoveryBlocked per Thread; hosts must retain visibility
11119
+ * outside execution history and exclude those Threads from claims. Ledger scan failures,
11120
+ * owner interruption and injected crash boundaries still fail the whole sweep.
11121
+ * Optional Thread selection lets a host recover its selected dispatch lane independently
11122
+ * of old cleanup. Unselected Threads are neither read nor reported by this call.
11123
+ */
11124
+ readonly runRecovery: (
11125
+ options?: RecoverySweepOptions,
11126
+ ) => Effect.Effect<RecoverySweepResult, DurableWorkerFailure>;
10898
11127
  /** Apply one recovery decision; untouched ready input is deferred to its worker claim. */
10899
11128
  readonly recoverSubmission: (
10900
11129
  submissionId: SubmissionId,
@@ -204,22 +204,33 @@ export class SubmissionLookupByKey extends Schema.TaggedClass<SubmissionLookupBy
204
204
  export const SubmissionLookup = Schema.Union([SubmissionLookupById, SubmissionLookupByKey]);
205
205
  export type SubmissionLookup = typeof SubmissionLookup.Type;
206
206
 
207
- /** The full durable ledger view of one Submission. */
208
- export class SubmissionSnapshot extends Schema.Class<SubmissionSnapshot>(
209
- "@effect-agent/thread/SubmissionSnapshot",
207
+ /**
208
+ * Control-only worklist entry. Discovery validates identities, ordering and state without
209
+ * decoding retained execution payloads. Hydrate only the selected Thread through `lookup`
210
+ * or `loadRecoverySnapshot`, inside its recovery failure boundary.
211
+ */
212
+ export class SubmissionWorkItem extends Schema.Class<SubmissionWorkItem>(
213
+ "@effect-agent/thread/SubmissionWorkItem",
210
214
  )({
211
215
  submissionId: SubmissionId,
212
216
  threadId: ThreadId,
213
217
  queueSequence: QueueSequence,
214
218
  principal: Principal,
215
219
  idempotencyKey: IdempotencyKey,
220
+ deploymentId: DeploymentId,
221
+ receiptId: ReceiptId,
222
+ state: SubmissionState,
223
+ }) {}
224
+
225
+ /** The full durable ledger view of one Submission. */
226
+ export class SubmissionSnapshot extends Schema.Class<SubmissionSnapshot>(
227
+ "@effect-agent/thread/SubmissionSnapshot",
228
+ )({
229
+ ...SubmissionWorkItem.fields,
216
230
  agentId: AgentId,
217
231
  agentDigests: DefinitionDigests,
218
- deploymentId: DeploymentId,
219
232
  inputPayload: PersistedJson,
220
233
  inputDigest: Digest,
221
- receiptId: ReceiptId,
222
- state: SubmissionState,
223
234
  settledOutcome: Schema.optionalKey(SettlementOutcome),
224
235
  createdAt: Schema.DateTimeUtcFromString,
225
236
  readyAt: Schema.optionalKey(Schema.DateTimeUtcFromString),
@@ -1050,8 +1061,10 @@ export type SubmissionLedgerFailure =
1050
1061
  * divergent re-resolution fails with `UnknownResolutionConflict`. Transitions
1051
1062
  * `unknown → input-applied` once no open call remains, waking the lane. Fails with
1052
1063
  * `SettlementConflict` once settled.
1053
- * - `scanNonterminal` — streams every Submission whose state is not `settled`, ordered by
1054
- * (threadId, queueSequence); recovery's admission-independent worklist (DUR-014).
1064
+ * - `scanNonterminal` — streams control-only entries for every Submission whose state is not
1065
+ * `settled`, ordered by (threadId, queueSequence); recovery's admission-independent worklist
1066
+ * (DUR-014). It must not decode execution payloads. SQL or control-identity corruption fails
1067
+ * the scan; selected-Thread payload hydration belongs inside the caller's isolation boundary.
1055
1068
  * - `loadRecoverySnapshot` — strongly consistent full snapshot for the pure recovery classifier.
1056
1069
  *
1057
1070
  * No operation claims exactly-once external side effects; the ledger records decisions
@@ -1137,7 +1150,7 @@ export class SubmissionLedger extends Context.Service<
1137
1150
  readonly releaseChildBudget: (
1138
1151
  request: ReleaseChildBudgetRequest,
1139
1152
  ) => Effect.Effect<ChildBudgetReservationSnapshot, ChildReservationConflict | LedgerError>;
1140
- readonly scanNonterminal: Stream.Stream<SubmissionSnapshot, LedgerError>;
1153
+ readonly scanNonterminal: Stream.Stream<SubmissionWorkItem, LedgerError>;
1141
1154
  readonly loadRecoverySnapshot: (
1142
1155
  request: RecoverySnapshotRequest,
1143
1156
  ) => Effect.Effect<RecoverySnapshot, LedgerError>;
@@ -1947,7 +1947,19 @@ const scanNonterminalWorklist = conformanceCase(
1947
1947
  );
1948
1948
  yield* ensure(
1949
1949
  mine.at(0)?.state === "running" && mine.at(1)?.state === "ready",
1950
- "scanNonterminal snapshots must carry the current Submission states",
1950
+ "scanNonterminal entries must carry the current Submission states",
1951
+ );
1952
+ yield* ensure(
1953
+ mine.at(0)?.receiptId === second.receiptId &&
1954
+ mine.at(1)?.receiptId === third.receiptId &&
1955
+ mine.every(
1956
+ (entry) =>
1957
+ entry.principal === CONFORMANCE_PRINCIPAL &&
1958
+ !("inputPayload" in entry) &&
1959
+ !("workerAdmission" in entry) &&
1960
+ !("messageAdmission" in entry),
1961
+ ),
1962
+ "The worklist retains receipt and principal identity without execution payloads",
1951
1963
  );
1952
1964
  const start = scanned.findIndex((snapshot) => snapshot.submissionId === second.submissionId);
1953
1965
 
@@ -436,10 +436,26 @@ export class LoadCheckpointRequest extends Schema.Class<LoadCheckpointRequest>(
436
436
  atOrBeforeSequence: Schema.optionalKey(CanonicalSequence),
437
437
  }) {}
438
438
 
439
+ /**
440
+ * Content-free storage provenance constructed by adapters from source-authored labels.
441
+ * Raw foreign lookalikes are not trusted diagnostics. Never include rejected values, SQL,
442
+ * or schema messages.
443
+ */
444
+ export class ThreadStoreDiagnostic extends Schema.Class<ThreadStoreDiagnostic>(
445
+ "@effect-agent/thread/ThreadStoreDiagnostic",
446
+ )({
447
+ causeTag: Schema.String.check(Schema.isMaxLength(128)),
448
+ operation: Schema.String.check(Schema.isMaxLength(256)),
449
+ decoder: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
450
+ issueTag: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(128))),
451
+ sequence: Schema.optionalKey(CanonicalSequence),
452
+ }) {}
453
+
439
454
  export class ThreadStoreError extends Schema.TaggedError<ThreadStoreError>()("ThreadStoreError", {
440
455
  operation: Schema.String,
441
456
  message: Schema.String,
442
457
  cause: Schema.optionalKey(Schema.Defect()),
458
+ diagnostic: Schema.optionalKey(ThreadStoreDiagnostic),
443
459
  }) {}
444
460
 
445
461
  export class ThreadNotMaterialized extends Schema.TaggedError<ThreadNotMaterialized>()(