effect-agent 0.1.0-beta.134 → 0.1.0-beta.136
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/{Admin-CUja0lmO.d.mts → Admin-Fx85KT-W.d.mts} +42 -4
- package/dist/{DurableAgentRuntime-DH2baLGe.mjs → DurableAgentRuntime-Bso5grIH.mjs} +50 -12
- package/dist/DurableAgentRuntime-Bso5grIH.mjs.map +1 -0
- package/dist/{EventSource-DoiZF3xv.d.mts → EventSource-DLT8oDK8.d.mts} +2 -2
- package/dist/{PreparedInputAdmission-B1bzyIuV.d.mts → PreparedInputAdmission-CKFgG6Em.d.mts} +3 -3
- package/dist/{RunJournal-BXrq0VIK.mjs → RunJournal-Gd83HYqG.mjs} +50 -27
- package/dist/RunJournal-Gd83HYqG.mjs.map +1 -0
- package/dist/{Subscription-m_yNfNeN.d.mts → Subscription-CCmHgOYM.d.mts} +2 -2
- package/dist/{SubscriptionInput-ON3-b8Sl.d.mts → SubscriptionInput-BFJMhu4K.d.mts} +2 -2
- 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/DurableAgentRuntime.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.mjs +1 -1
- package/dist/durable/EventSource.d.mts +1 -1
- package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
- package/dist/durable/MessageDelivery.d.mts +41 -41
- package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
- package/dist/durable/PersistentHistory.d.mts +1 -1
- package/dist/durable/PersistentHistory.mjs +1 -1
- package/dist/durable/PreparedInputAdmission.d.mts +1 -1
- package/dist/durable/Recovery.d.mts +1 -1
- package/dist/durable/RunJournal.d.mts +1 -1
- package/dist/durable/RunJournal.mjs +1 -1
- package/dist/durable/Scheduling.d.mts +1 -1
- package/dist/durable/SqlStorageV2Upgrade.d.mts +1 -1
- package/dist/durable/SqlSubscriptionStore.d.mts +1 -1
- package/dist/durable/SqlThreadNativeReads.d.mts +1 -1
- package/dist/durable/SqlThreadNativeReads.mjs +1 -1
- package/dist/durable/SubmissionLedger.d.mts +2 -2
- package/dist/durable/SubmissionLedger.mjs +29 -2
- package/dist/durable/SubmissionLedger.mjs.map +1 -1
- package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
- package/dist/durable/SubmissionLedgerConformance.mjs +69 -2
- 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 +2 -2
- 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/ThreadInvariants.d.mts +1 -1
- package/dist/durable/ThreadInvariants.mjs +1 -1
- package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
- package/dist/durable/ThreadStore.d.mts +1 -1
- package/dist/durable/ThreadStore.mjs +1 -1
- package/dist/durable/ThreadStoreConformance.d.mts +1 -1
- package/dist/durable/ThreadStoreConformance.mjs +1 -1
- package/dist/durable/WorkerHost.d.mts +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/src/durable/DurableAgentRuntime.ts +94 -10
- package/src/durable/RunJournal.ts +120 -56
- package/src/durable/SubmissionLedger.ts +32 -0
- package/src/durable/SubmissionLedgerConformance.ts +120 -0
- package/src/durable/internal/journal-checkpoint.ts +1 -1
- package/src/durable/internal/journal-metadata.ts +10 -3
- package/dist/DurableAgentRuntime-DH2baLGe.mjs.map +0 -1
- package/dist/RunJournal-BXrq0VIK.mjs.map +0 -1
|
@@ -555,6 +555,9 @@ const PROMPT_TRANSPARENT_TAGS: ReadonlySet<string> = new Set([
|
|
|
555
555
|
* are never canonical settlements or evidence for compaction, recovery or accounting. Real
|
|
556
556
|
* results replace them at the original declaration, including results appended after another Run.
|
|
557
557
|
* Prior user intent and assistant text remain visible; prior system instructions do not.
|
|
558
|
+
* Each Run retains the history preceding its start and its own continuation. Other Runs'
|
|
559
|
+
* subsequent Turns cannot replace its input. Compaction covers only its creator's Run view;
|
|
560
|
+
* a projection without an owner retains interleaved exchanges from the complete Thread history.
|
|
558
561
|
*/
|
|
559
562
|
/** @internal Lightweight canonical boundaries collected without retaining record payloads. */
|
|
560
563
|
export interface JournalBoundary {
|
|
@@ -598,13 +601,11 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
598
601
|
committedTurns: seed?.committedTurns ?? 0,
|
|
599
602
|
};
|
|
600
603
|
|
|
601
|
-
// RUN-026 pre-scan:
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
// coversThrough resolve to the record appended later (higher sequence),
|
|
607
|
-
// matching at-most-once replay intent.
|
|
604
|
+
// RUN-026 pre-scan: valid compactions retire only their creator's Run view. A bound
|
|
605
|
+
// stays below its own sequence and cannot split a visible response from its settled results.
|
|
606
|
+
// Pruning and rollovers may cover complete owner-Run batches; a summarize record carries
|
|
607
|
+
// its summary. Invalid records leave full history authoritative. A wider containing view
|
|
608
|
+
// replaces earlier coverage, with equal bounds preferring the later record.
|
|
608
609
|
// One span per settled record, paired with its declaring response the same
|
|
609
610
|
// way the fold pairs them: a settled belongs to the most recent
|
|
610
611
|
// ModelResponseRecorded of its Run. A bound inside (response, settled)
|
|
@@ -634,9 +635,39 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
634
635
|
toolExecutionEvidence,
|
|
635
636
|
settledToolCallRecordIds,
|
|
636
637
|
settledById,
|
|
637
|
-
compactions,
|
|
638
638
|
} = metadata;
|
|
639
639
|
|
|
640
|
+
const declarationByResultSequence = new Map(settledSpans.map(({ from, to }) => [to, from]));
|
|
641
|
+
|
|
642
|
+
const isInRunView = (
|
|
643
|
+
sequence: number,
|
|
644
|
+
payload: { readonly _tag: string; readonly runId?: RunId | undefined },
|
|
645
|
+
runId: RunId | undefined,
|
|
646
|
+
): boolean => {
|
|
647
|
+
if (runId === undefined || payload.runId === undefined || payload.runId === runId) return true;
|
|
648
|
+
const first = firstSequenceByRun.get(runId) ?? Number.POSITIVE_INFINITY;
|
|
649
|
+
|
|
650
|
+
return (
|
|
651
|
+
sequence < first ||
|
|
652
|
+
(payload._tag === "ToolCallSettled" &&
|
|
653
|
+
(declarationByResultSequence.get(sequence) ?? Number.POSITIVE_INFINITY) < first)
|
|
654
|
+
);
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const compactions = metadata.compactions.filter(({ sequence, payload }) =>
|
|
658
|
+
isInRunView(sequence, payload, ownerRunId),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const recordsForRun = records.pipe(
|
|
662
|
+
Stream.filter(({ sequence, record: { payload } }) =>
|
|
663
|
+
isInRunView(sequence, payload, ownerRunId),
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const isCovered = (envelope: CanonicalRecordEnvelope, compaction: CompactionCreated): boolean =>
|
|
668
|
+
envelope.sequence <= compaction.coversThrough &&
|
|
669
|
+
isInRunView(envelope.sequence, envelope.record.payload, compaction.runId);
|
|
670
|
+
|
|
640
671
|
const settledCoverage = compactions.reduce(
|
|
641
672
|
(through, { payload }) =>
|
|
642
673
|
payload.kind !== "summarize" ? Math.max(through, payload.coversThrough) : through,
|
|
@@ -671,7 +702,7 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
671
702
|
: yield* decodePromptMessages(seed.protectedContext);
|
|
672
703
|
|
|
673
704
|
if (settledCoverage > 0) {
|
|
674
|
-
yield* Stream.runForEach(
|
|
705
|
+
yield* Stream.runForEach(recordsForRun, (envelope) =>
|
|
675
706
|
Effect.gen(function* () {
|
|
676
707
|
const payload = envelope.record.payload;
|
|
677
708
|
|
|
@@ -727,21 +758,54 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
727
758
|
incompleteResponseSequences.some(
|
|
728
759
|
(response) =>
|
|
729
760
|
response.sequence <= coversThrough &&
|
|
761
|
+
isInRunView(
|
|
762
|
+
response.sequence,
|
|
763
|
+
{ _tag: "ModelResponseRecorded", runId: response.runId },
|
|
764
|
+
runId,
|
|
765
|
+
) &&
|
|
730
766
|
!isTerminalPriorRun(response.runId, runId, ownSequence),
|
|
731
767
|
)
|
|
732
768
|
)
|
|
733
769
|
return false;
|
|
734
770
|
for (const span of settledSpans) {
|
|
735
|
-
if (
|
|
771
|
+
if (
|
|
772
|
+
span.from <= coversThrough &&
|
|
773
|
+
coversThrough < span.to &&
|
|
774
|
+
isInRunView(span.from, { _tag: "ModelResponseRecorded", runId: span.runId }, runId)
|
|
775
|
+
)
|
|
776
|
+
return false;
|
|
736
777
|
}
|
|
737
778
|
|
|
738
779
|
return true;
|
|
739
780
|
};
|
|
740
781
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
let
|
|
744
|
-
let
|
|
782
|
+
type CompactionView = (typeof compactions)[number];
|
|
783
|
+
|
|
784
|
+
let replacements: ReadonlyArray<CompactionView> = [];
|
|
785
|
+
let clearings: ReadonlyArray<CompactionView> = [];
|
|
786
|
+
|
|
787
|
+
const retainCompaction = (views: ReadonlyArray<CompactionView>, next: CompactionView) => {
|
|
788
|
+
// Independent Run views overlap without containing one another. Retire an overlay only
|
|
789
|
+
// when the replacement actually contains its view, not merely a larger sequence number.
|
|
790
|
+
if (
|
|
791
|
+
views.some(
|
|
792
|
+
(prior) =>
|
|
793
|
+
prior.payload.coversThrough > next.payload.coversThrough &&
|
|
794
|
+
isInRunView(next.sequence, next.payload, prior.payload.runId),
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
return views;
|
|
798
|
+
|
|
799
|
+
return [
|
|
800
|
+
...views.filter(
|
|
801
|
+
(prior) =>
|
|
802
|
+
next.payload.coversThrough < prior.payload.coversThrough ||
|
|
803
|
+
!isInRunView(prior.sequence, prior.payload, next.payload.runId),
|
|
804
|
+
),
|
|
805
|
+
next,
|
|
806
|
+
];
|
|
807
|
+
};
|
|
808
|
+
|
|
745
809
|
let latestWindowId: string | undefined = seed?.contextWindowId;
|
|
746
810
|
let latestWindowSequence = seed?.throughSequence ?? -1;
|
|
747
811
|
let rolloverCoveredThrough = 0;
|
|
@@ -756,53 +820,48 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
756
820
|
rolloverCoveredThrough = Math.max(rolloverCoveredThrough, payload.coversThrough);
|
|
757
821
|
if (payload.kind === "summarize" || payload.kind === "rollover") {
|
|
758
822
|
if (payload.kind === "summarize" && payload.summary === undefined) continue;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
(payload.coversThrough === summarizeBound && sequence > summarizeSequence)
|
|
762
|
-
) {
|
|
763
|
-
summarizeBound = payload.coversThrough;
|
|
764
|
-
summarizeSequence = sequence;
|
|
765
|
-
replacement = payload;
|
|
766
|
-
}
|
|
767
|
-
} else if (payload.coversThrough > clearBound) {
|
|
768
|
-
clearBound = payload.coversThrough;
|
|
769
|
-
}
|
|
823
|
+
replacements = retainCompaction(replacements, { payload, sequence });
|
|
824
|
+
} else clearings = retainCompaction(clearings, { payload, sequence });
|
|
770
825
|
}
|
|
771
826
|
let summaryEmitted = false;
|
|
772
827
|
|
|
773
|
-
const retainedPrefix =
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
828
|
+
const retainedPrefix = replacements.some(
|
|
829
|
+
({ payload }) =>
|
|
830
|
+
payload.kind === "rollover" &&
|
|
831
|
+
payload.runId === ownerRunId &&
|
|
832
|
+
ownerPrefixSequence <= payload.coversThrough,
|
|
833
|
+
)
|
|
834
|
+
? (protectedContext?.content ?? [])
|
|
835
|
+
: [];
|
|
779
836
|
|
|
780
|
-
const replacementLength =
|
|
837
|
+
const replacementLength = retainedPrefix.length + replacements.length;
|
|
781
838
|
|
|
782
839
|
if (seed?.frontier !== undefined)
|
|
783
840
|
onBoundary?.({ ...seed.frontier, promptLength: replacementLength });
|
|
784
841
|
|
|
785
842
|
const emitSummary = () => {
|
|
786
|
-
if (summaryEmitted ||
|
|
843
|
+
if (summaryEmitted || replacements.length === 0) return;
|
|
787
844
|
summaryEmitted = true;
|
|
845
|
+
state.all.push(...retainedPrefix);
|
|
846
|
+
for (const { payload: replacement } of replacements) {
|
|
847
|
+
const message =
|
|
848
|
+
replacement.kind === "rollover"
|
|
849
|
+
? contextWindowMessage(
|
|
850
|
+
contextWindowId(replacement.runId, replacement.turn),
|
|
851
|
+
replacement.handoff,
|
|
852
|
+
)
|
|
853
|
+
: Prompt.makeMessage("user", {
|
|
854
|
+
content: [
|
|
855
|
+
Prompt.makePart("text", {
|
|
856
|
+
text: `${COMPACTION_SUMMARY_PREFIX}${replacement.summary}`,
|
|
857
|
+
}),
|
|
858
|
+
],
|
|
859
|
+
});
|
|
788
860
|
|
|
789
|
-
|
|
790
|
-
replacement.kind
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
replacement.handoff,
|
|
794
|
-
)
|
|
795
|
-
: Prompt.makeMessage("user", {
|
|
796
|
-
content: [
|
|
797
|
-
Prompt.makePart("text", {
|
|
798
|
-
text: `${COMPACTION_SUMMARY_PREFIX}${replacement.summary}`,
|
|
799
|
-
}),
|
|
800
|
-
],
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
state.all.push(...retainedPrefix, message);
|
|
804
|
-
if (replacement.kind !== "rollover" || replacement.runId !== ownerRunId)
|
|
805
|
-
state.before.push(message);
|
|
861
|
+
state.all.push(message);
|
|
862
|
+
if (replacement.kind !== "rollover" || replacement.runId !== ownerRunId)
|
|
863
|
+
state.before.push(message);
|
|
864
|
+
}
|
|
806
865
|
};
|
|
807
866
|
|
|
808
867
|
const modelUsage: Array<ModelCallUsage> = [];
|
|
@@ -986,7 +1045,7 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
986
1045
|
};
|
|
987
1046
|
});
|
|
988
1047
|
|
|
989
|
-
yield* Stream.runForEach(
|
|
1048
|
+
yield* Stream.runForEach(recordsForRun, (envelope) =>
|
|
990
1049
|
Effect.gen(function* () {
|
|
991
1050
|
const payload = envelope.record.payload;
|
|
992
1051
|
|
|
@@ -1020,8 +1079,8 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
1020
1079
|
(payload._tag === "ModelResponseRecorded" || payload._tag === "ToolCallSettled")
|
|
1021
1080
|
)
|
|
1022
1081
|
return;
|
|
1023
|
-
if (
|
|
1024
|
-
//
|
|
1082
|
+
if (replacements.some(({ payload }) => isCovered(envelope, payload))) {
|
|
1083
|
+
// Retiring Prompt payloads does not retire the owning Run's policy or usage accounting.
|
|
1025
1084
|
if (payload._tag === "ModelResponseRecorded" && payload.runId === ownerRunId) {
|
|
1026
1085
|
const messages = yield* decodePromptMessages(payload.messages);
|
|
1027
1086
|
|
|
@@ -1032,7 +1091,7 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
1032
1091
|
onBoundary?.({
|
|
1033
1092
|
sequence: envelope.sequence,
|
|
1034
1093
|
tag: payload._tag,
|
|
1035
|
-
promptLength: replacementLength,
|
|
1094
|
+
promptLength: Math.max(replacementLength, state.all.length),
|
|
1036
1095
|
...(isTerminalPriorRun(payload.runId, ownerRunId, Number.POSITIVE_INFINITY)
|
|
1037
1096
|
? { terminalPriorRun: true }
|
|
1038
1097
|
: {}),
|
|
@@ -1059,7 +1118,9 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
1059
1118
|
slot.parts[slot.partIndex] = Prompt.makePart("tool-result", {
|
|
1060
1119
|
id: payload.toolCallId,
|
|
1061
1120
|
name: payload.toolName,
|
|
1062
|
-
result:
|
|
1121
|
+
result: clearings.some(({ payload }) => isCovered(envelope, payload))
|
|
1122
|
+
? CLEARED_TOOL_RESULT
|
|
1123
|
+
: payload.result,
|
|
1063
1124
|
isFailure: payload.isFailure,
|
|
1064
1125
|
providerExecuted: false,
|
|
1065
1126
|
});
|
|
@@ -1086,7 +1147,10 @@ export const projectRunJournalStream = Effect.fn("RunJournal.projectRunJournalSt
|
|
|
1086
1147
|
) {
|
|
1087
1148
|
state = yield* flushTools(state);
|
|
1088
1149
|
}
|
|
1089
|
-
state.pendingTools.push({
|
|
1150
|
+
state.pendingTools.push({
|
|
1151
|
+
record: payload,
|
|
1152
|
+
cleared: clearings.some(({ payload }) => isCovered(envelope, payload)),
|
|
1153
|
+
});
|
|
1090
1154
|
state = {
|
|
1091
1155
|
...state,
|
|
1092
1156
|
pendingToolsForRun: payload.runId === ownerRunId,
|
|
@@ -85,6 +85,21 @@ export const SubmissionState = Schema.Literals([
|
|
|
85
85
|
|
|
86
86
|
export type SubmissionState = typeof SubmissionState.Type;
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Opt-in host policy for handling the next runnable same-Agent input in its own Run at a complete Turn
|
|
90
|
+
* boundary. This never joins inputs or changes their principals, grants, or receipts. The
|
|
91
|
+
* runtime selects the ordered candidate; policy cannot select an arbitrary Submission.
|
|
92
|
+
* Approval waits, child waits and incomplete Tool batches are not handoff boundaries. Hosts
|
|
93
|
+
* retaining independent replies must also leave those inputs unclaimed by `claimJoining`.
|
|
94
|
+
* In-flight model requests and Tool batches run to their existing safe boundary.
|
|
95
|
+
*/
|
|
96
|
+
export const SubmissionScheduling = Context.Reference<{
|
|
97
|
+
readonly yieldTo?: (request: {
|
|
98
|
+
readonly active: SubmissionSnapshot;
|
|
99
|
+
readonly next: SubmissionSnapshot;
|
|
100
|
+
}) => Effect.Effect<boolean, LedgerError>;
|
|
101
|
+
}>("@effect-agent/thread/SubmissionScheduling", { defaultValue: () => ({}) });
|
|
102
|
+
|
|
88
103
|
/**
|
|
89
104
|
* Default ownership lease duration (D5). The lease is a liveness hint that makes an abandoned
|
|
90
105
|
* claim reclaimable; correctness never depends on it because every canonical append is fenced by
|
|
@@ -289,9 +304,21 @@ export const AdmissionResolution = Schema.Union([
|
|
|
289
304
|
|
|
290
305
|
export type AdmissionResolution = typeof AdmissionResolution.Type;
|
|
291
306
|
|
|
307
|
+
/**
|
|
308
|
+
* A trusted runtime's advisory handoff after committing a complete Turn and closing its
|
|
309
|
+
* Attempt. The epoch fences the observation; an intervening claim invalidates it. Deferred
|
|
310
|
+
* Runs retain their identities and obligations. Losing this hint resumes ordinary FIFO.
|
|
311
|
+
*/
|
|
312
|
+
export class ClaimHandoff extends Schema.Class<ClaimHandoff>("@effect-agent/thread/ClaimHandoff")({
|
|
313
|
+
producerEpoch: ProducerEpoch,
|
|
314
|
+
deferredSubmissionIds: Schema.NonEmptyArray(SubmissionId),
|
|
315
|
+
submissionId: SubmissionId,
|
|
316
|
+
}) {}
|
|
317
|
+
|
|
292
318
|
export class ClaimRequest extends Schema.Class<ClaimRequest>("@effect-agent/thread/ClaimRequest")({
|
|
293
319
|
threadId: ThreadId,
|
|
294
320
|
producerId: ProducerId,
|
|
321
|
+
handoff: Schema.optionalKey(ClaimHandoff),
|
|
295
322
|
}) {}
|
|
296
323
|
|
|
297
324
|
/**
|
|
@@ -1019,6 +1046,11 @@ export type SubmissionLedgerFailure =
|
|
|
1019
1046
|
* `attemptId` and `ownershipToken`, and starts the ownership lease (D5: adapters default to
|
|
1020
1047
|
* `DEFAULT_OWNERSHIP_LEASE_DURATION`, configurable). An expired lease makes the head
|
|
1021
1048
|
* claimable; expiry alone never revokes correctness, only permission to assume liveness.
|
|
1049
|
+
* An optional trusted-runtime `handoff` can defer only the named `input-applied` prefix,
|
|
1050
|
+
* with no abort intent, to claim the exact next runnable Submission. It is checked atomically
|
|
1051
|
+
* against the current producer epoch and the same whole-Thread ownership fence. Admission
|
|
1052
|
+
* gaps, approval/child waits and joined work cannot be bypassed. Hints are not durable facts;
|
|
1053
|
+
* recovery resumes the same retained Runs and can select another complete-Turn handoff.
|
|
1022
1054
|
* - `renewOwnership` — extends the lease while the token still owns the lane; fails with
|
|
1023
1055
|
* `OwnershipLost` once superseded. Renewal after lease expiry succeeds if no other claim has
|
|
1024
1056
|
* taken the lane in between.
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
ChildSettledNotification,
|
|
46
46
|
ClaimJoiningRequest,
|
|
47
47
|
ClaimRequest,
|
|
48
|
+
ClaimHandoff,
|
|
48
49
|
IdempotencyKey,
|
|
49
50
|
JoinedToHost,
|
|
50
51
|
LedgerError,
|
|
@@ -4770,6 +4771,124 @@ const assignmentSettlement = conformanceCase(
|
|
|
4770
4771
|
}),
|
|
4771
4772
|
);
|
|
4772
4773
|
|
|
4774
|
+
// Native turn-boundary yielding retained the active FIFO head:
|
|
4775
|
+
// https://github.com/danieljvdm/effect-agent/commit/2259fc05eec3bfac2a92a8d055953f3482e54735
|
|
4776
|
+
const cooperativeHandoff = conformanceCase(
|
|
4777
|
+
"hands off complete Turns without bypassing ownership, ordering, cancellation or approval",
|
|
4778
|
+
({ ensure, expectSome }) =>
|
|
4779
|
+
Effect.gen(function* () {
|
|
4780
|
+
const ledger = yield* SubmissionLedger;
|
|
4781
|
+
|
|
4782
|
+
for (const scenario of [
|
|
4783
|
+
"ready",
|
|
4784
|
+
"live",
|
|
4785
|
+
"stale",
|
|
4786
|
+
"abort",
|
|
4787
|
+
"approval",
|
|
4788
|
+
"gap",
|
|
4789
|
+
"ready-prefix",
|
|
4790
|
+
"unapplied",
|
|
4791
|
+
"foreign",
|
|
4792
|
+
] as const) {
|
|
4793
|
+
const threadId = decodeThreadId(`ledger-handoff-${scenario}`);
|
|
4794
|
+
const first = yield* admitReady(threadId, "first", { work: "old" });
|
|
4795
|
+
const claim = yield* expectSome("original claim", yield* claimLane(threadId, PRODUCER_A));
|
|
4796
|
+
|
|
4797
|
+
if (scenario !== "unapplied")
|
|
4798
|
+
yield* ledger.markInputApplied(
|
|
4799
|
+
MarkInputAppliedRequest.make({
|
|
4800
|
+
submissionId: first.submissionId,
|
|
4801
|
+
ownershipToken: claim.ownershipToken,
|
|
4802
|
+
recordId: submissionInputRecordId(first.submissionId),
|
|
4803
|
+
sequence: decodeSequence(1),
|
|
4804
|
+
}),
|
|
4805
|
+
);
|
|
4806
|
+
if (scenario === "gap")
|
|
4807
|
+
yield* ledger.admit(yield* admissionRequest(threadId, "gap", { work: "not-ready" }));
|
|
4808
|
+
if (scenario === "ready-prefix") yield* admitReady(threadId, "prefix", { work: "earlier" });
|
|
4809
|
+
const next = yield* admitReady(threadId, "next", { work: "human" });
|
|
4810
|
+
|
|
4811
|
+
const handoff = ClaimHandoff.make({
|
|
4812
|
+
producerEpoch: claim.producerEpoch,
|
|
4813
|
+
deferredSubmissionIds: [first.submissionId],
|
|
4814
|
+
submissionId: scenario === "foreign" ? decodeSubmissionId("foreign") : next.submissionId,
|
|
4815
|
+
});
|
|
4816
|
+
|
|
4817
|
+
if (scenario === "approval") {
|
|
4818
|
+
yield* ledger.suspend(
|
|
4819
|
+
SuspendRequest.make({
|
|
4820
|
+
submissionId: first.submissionId,
|
|
4821
|
+
ownershipToken: claim.ownershipToken,
|
|
4822
|
+
reason: ApprovalPendingSuspension.make({
|
|
4823
|
+
toolCallIds: [decodeToolCallId("purchase")],
|
|
4824
|
+
}),
|
|
4825
|
+
}),
|
|
4826
|
+
);
|
|
4827
|
+
} else if (scenario !== "live") {
|
|
4828
|
+
yield* ledger.releaseOwnership(
|
|
4829
|
+
ReleaseOwnershipRequest.make({
|
|
4830
|
+
submissionId: first.submissionId,
|
|
4831
|
+
ownershipToken: claim.ownershipToken,
|
|
4832
|
+
}),
|
|
4833
|
+
);
|
|
4834
|
+
}
|
|
4835
|
+
if (scenario === "abort")
|
|
4836
|
+
yield* ledger.requestAbort(
|
|
4837
|
+
AbortCommand.make({
|
|
4838
|
+
submissionId: first.submissionId,
|
|
4839
|
+
author: "human",
|
|
4840
|
+
reason: "stop",
|
|
4841
|
+
}),
|
|
4842
|
+
);
|
|
4843
|
+
if (scenario === "stale") {
|
|
4844
|
+
const intervening = yield* expectSome(
|
|
4845
|
+
"intervening claim",
|
|
4846
|
+
yield* claimLane(threadId, PRODUCER_B),
|
|
4847
|
+
);
|
|
4848
|
+
|
|
4849
|
+
yield* ledger.releaseOwnership(
|
|
4850
|
+
ReleaseOwnershipRequest.make({
|
|
4851
|
+
submissionId: first.submissionId,
|
|
4852
|
+
ownershipToken: intervening.ownershipToken,
|
|
4853
|
+
}),
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
const selected = yield* ledger.claim(
|
|
4858
|
+
ClaimRequest.make({ threadId, producerId: PRODUCER_B, handoff }),
|
|
4859
|
+
);
|
|
4860
|
+
|
|
4861
|
+
if (scenario !== "ready") {
|
|
4862
|
+
yield* ensure(Option.isNone(selected), `Handoff must preserve the ${scenario} boundary`);
|
|
4863
|
+
continue;
|
|
4864
|
+
}
|
|
4865
|
+
const nextClaim = yield* expectSome("ordered handoff claim", selected);
|
|
4866
|
+
|
|
4867
|
+
yield* ensure(
|
|
4868
|
+
nextClaim.submissionId === next.submissionId &&
|
|
4869
|
+
nextClaim.producerEpoch > claim.producerEpoch,
|
|
4870
|
+
"Handoff must claim the next Submission with a new epoch",
|
|
4871
|
+
);
|
|
4872
|
+
yield* settleClaimed(next, nextClaim.ownershipToken);
|
|
4873
|
+
|
|
4874
|
+
const resumed = yield* expectSome(
|
|
4875
|
+
"original Run resumes",
|
|
4876
|
+
yield* claimLane(threadId, PRODUCER_A),
|
|
4877
|
+
);
|
|
4878
|
+
|
|
4879
|
+
yield* ensure(
|
|
4880
|
+
resumed.submissionId === first.submissionId,
|
|
4881
|
+
"The deferred Submission must retain its original obligation",
|
|
4882
|
+
);
|
|
4883
|
+
yield* ensure(
|
|
4884
|
+
(yield* recoverySnapshot(first.submissionId)).submission.receiptId === first.receiptId &&
|
|
4885
|
+
(yield* recoverySnapshot(next.submissionId)).submission.receiptId === next.receiptId,
|
|
4886
|
+
"Handoff must preserve both receipt identities",
|
|
4887
|
+
);
|
|
4888
|
+
}
|
|
4889
|
+
}),
|
|
4890
|
+
);
|
|
4891
|
+
|
|
4773
4892
|
/**
|
|
4774
4893
|
* The shared, adapter-parameterized SubmissionLedger contract suite (STORE-010). Every durable
|
|
4775
4894
|
* ledger adapter test suite must execute each case against its own ledger provisioning, inside
|
|
@@ -4788,6 +4907,7 @@ export const submissionLedgerConformanceCases: ReadonlyArray<SubmissionLedgerCon
|
|
|
4788
4907
|
admissionTupleBoundaries,
|
|
4789
4908
|
concurrentAdmissionFifo,
|
|
4790
4909
|
fifoHeadClaim,
|
|
4910
|
+
cooperativeHandoff,
|
|
4791
4911
|
leaseExpiryReclaim,
|
|
4792
4912
|
releaseMakesHeadClaimable,
|
|
4793
4913
|
inputAppliedIdempotency,
|
|
@@ -56,7 +56,7 @@ export class RecoveryCheckpointContents extends Schema.Class<RecoveryCheckpointC
|
|
|
56
56
|
digest: Digest,
|
|
57
57
|
}) {}
|
|
58
58
|
|
|
59
|
-
export const RECOVERY_ENGINE_VERSION = "effect-agent/recovery@
|
|
59
|
+
export const RECOVERY_ENGINE_VERSION = "effect-agent/recovery@2";
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Late evidence can invalidate an old compaction. Such histories use full canonical replay;
|
|
@@ -14,7 +14,11 @@ export interface JournalMetadata {
|
|
|
14
14
|
readonly firstSequenceByRun: ReadonlyMap<string, number>;
|
|
15
15
|
readonly lastResponseSequenceByRun: ReadonlyMap<string, number>;
|
|
16
16
|
readonly terminalSequenceByRun: ReadonlyMap<string, number>;
|
|
17
|
-
readonly settledSpans: ReadonlyArray<{
|
|
17
|
+
readonly settledSpans: ReadonlyArray<{
|
|
18
|
+
readonly from: number;
|
|
19
|
+
readonly to: number;
|
|
20
|
+
readonly runId: RunId;
|
|
21
|
+
}>;
|
|
18
22
|
readonly toolExecutionEvidence: ReadonlySet<string>;
|
|
19
23
|
readonly settledToolCallRecordIds: ReadonlySet<string>;
|
|
20
24
|
readonly settledById: ReadonlyMap<
|
|
@@ -45,7 +49,10 @@ export const makeJournalMetadata = (
|
|
|
45
49
|
if (seed?.firstSequence !== undefined) firstSequenceByRun.set(seed.runId, seed.firstSequence);
|
|
46
50
|
const lastResponseSequenceByRun = new Map<string, number>();
|
|
47
51
|
const terminalSequenceByRun = new Map<string, number>();
|
|
48
|
-
|
|
52
|
+
|
|
53
|
+
const settledSpans: Array<{ readonly from: number; readonly to: number; readonly runId: RunId }> =
|
|
54
|
+
[];
|
|
55
|
+
|
|
49
56
|
const toolExecutionEvidence = new Set<string>();
|
|
50
57
|
const settledToolCallRecordIds = new Set<string>();
|
|
51
58
|
|
|
@@ -98,7 +105,7 @@ export const makeJournalMetadata = (
|
|
|
98
105
|
const from = lastResponseSequenceByRun.get(payload.runId);
|
|
99
106
|
|
|
100
107
|
if (from !== undefined && from < envelope.sequence)
|
|
101
|
-
settledSpans.push({ from, to: envelope.sequence });
|
|
108
|
+
settledSpans.push({ from, to: envelope.sequence, runId: payload.runId });
|
|
102
109
|
}
|
|
103
110
|
},
|
|
104
111
|
snapshot: (): JournalMetadata => ({
|