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.
- package/dist/{DurableAgentRuntime-PyIgNYXF.mjs → DurableAgentRuntime-BIrgxwRl.mjs} +154 -21
- package/dist/DurableAgentRuntime-BIrgxwRl.mjs.map +1 -0
- package/dist/{EventSource-BUYLmL77.d.mts → EventSource-ARs56eBq.d.mts} +3 -3
- package/dist/{PreparedInputAdmission-J3YK6LAm.d.mts → PreparedInputAdmission-_WYg-D0f.d.mts} +4 -4
- package/dist/{Records-4qnRmniE.d.mts → Records-BtxMy8xD.d.mts} +94 -13
- package/dist/{Schedule-C2lH2kxY.d.mts → Schedule-DGPyXBzK.d.mts} +2 -2
- package/dist/{Subscription-pQUv0QD6.d.mts → Subscription-B9VszlF9.d.mts} +3 -3
- package/dist/{SubscriptionInput-DdJOiNMY.d.mts → SubscriptionInput-BJWZqCWP.d.mts} +3 -3
- package/dist/capabilities/Mcp.d.mts +1 -1
- package/dist/capabilities/Mcp.mjs +5 -6
- package/dist/capabilities/Mcp.mjs.map +1 -1
- package/dist/durable/ActivityStore.d.mts +1 -1
- package/dist/durable/Admin.d.mts +1 -1
- package/dist/durable/AgentRegistration.d.mts +1 -1
- package/dist/durable/Certification.d.mts +1 -1
- package/dist/durable/CommittedActivity.d.mts +1 -1
- package/dist/durable/Digest.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.d.mts +2 -2
- package/dist/durable/DurableAgentRuntime.mjs +2 -2
- package/dist/durable/EventSource.d.mts +1 -1
- package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
- package/dist/durable/MessageDelivery.d.mts +3 -3
- package/dist/durable/MessageDeliveryStoreConformance.d.mts +39 -39
- package/dist/durable/PersistentHistory.d.mts +1 -1
- package/dist/durable/PreparedInputAdmission.d.mts +1 -1
- package/dist/durable/Records.d.mts +1 -1
- package/dist/durable/Recovery.d.mts +1 -1
- package/dist/durable/RunJournal.d.mts +1 -1
- package/dist/durable/Schedule.d.mts +1 -1
- package/dist/durable/ScheduleStoreConformance.d.mts +1 -1
- package/dist/durable/ScheduleTransition.d.mts +1 -1
- package/dist/durable/Scheduling.d.mts +2 -2
- package/dist/durable/SqlStorageV2Upgrade.d.mts +2 -2
- package/dist/durable/SqlSubscriptionStore.d.mts +2 -2
- package/dist/durable/SqlThreadNativeReads.d.mts +1 -1
- package/dist/durable/SubmissionLedger.d.mts +2 -2
- package/dist/durable/SubmissionLedger.mjs +19 -8
- package/dist/durable/SubmissionLedger.mjs.map +1 -1
- package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
- package/dist/durable/SubmissionLedgerConformance.mjs +2 -1
- package/dist/durable/SubmissionLedgerConformance.mjs.map +1 -1
- package/dist/durable/SubmissionStatus.d.mts +1 -1
- package/dist/durable/Subscription.d.mts +1 -1
- package/dist/durable/SubscriptionInput.d.mts +1 -1
- package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
- package/dist/durable/SubscriptionTools.d.mts +3 -3
- package/dist/durable/SubscriptionTransition.d.mts +1 -1
- package/dist/durable/Subscriptions.d.mts +5 -5
- package/dist/durable/ThreadContextHistory.d.mts +1 -1
- package/dist/durable/ThreadContextHistoryProjection.d.mts +1 -1
- package/dist/durable/ThreadInvariants.d.mts +1 -1
- package/dist/durable/ThreadProjection.d.mts +1 -1
- package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
- package/dist/durable/ThreadStore.d.mts +2 -2
- package/dist/durable/ThreadStore.mjs +16 -2
- package/dist/durable/ThreadStore.mjs.map +1 -1
- package/dist/durable/ThreadStoreConformance.d.mts +1 -1
- package/dist/durable/WorkerHost.d.mts +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/capabilities/Mcp.ts +6 -7
- package/src/durable/DurableAgentRuntime.ts +278 -49
- package/src/durable/SubmissionLedger.ts +22 -9
- package/src/durable/SubmissionLedgerConformance.ts +13 -1
- package/src/durable/ThreadStore.ts +16 -0
- 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
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
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
|
-
|
|
9542
|
-
|
|
9736
|
+
index += submissionIds.length;
|
|
9737
|
+
if (options?.threadId !== undefined && options.threadId !== first.threadId) continue;
|
|
9738
|
+
let phase: RecoveryFailure["phase"] = "recovery";
|
|
9543
9739
|
|
|
9544
|
-
|
|
9545
|
-
|
|
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
|
-
|
|
9548
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
10620
|
+
for (const entry of nonterminal) {
|
|
10404
10621
|
const snapshot = yield* ledger.loadRecoverySnapshot(
|
|
10405
|
-
RecoverySnapshotRequest.make({ 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
|
|
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
|
-
|
|
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
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
1054
|
-
* (threadId, queueSequence); recovery's admission-independent worklist
|
|
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<
|
|
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
|
|
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>()(
|