effect-agent 0.1.0-beta.131 → 0.1.0-beta.133
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-C34cVKlT.d.mts → Admin-CUja0lmO.d.mts} +21 -3
- package/dist/{DurableAgentRuntime-BydyN_n-.mjs → DurableAgentRuntime-DH2baLGe.mjs} +13 -21
- package/dist/DurableAgentRuntime-DH2baLGe.mjs.map +1 -0
- package/dist/{EventSource-Bac0SxKL.d.mts → EventSource-DoiZF3xv.d.mts} +2 -2
- package/dist/{PreparedInputAdmission-DcQGQ0Yp.d.mts → PreparedInputAdmission-B1bzyIuV.d.mts} +3 -3
- package/dist/{Subscription-Bk2Smmqc.d.mts → Subscription-m_yNfNeN.d.mts} +2 -2
- package/dist/{SubscriptionInput-ekbJz3pF.d.mts → SubscriptionInput-ON3-b8Sl.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/PreparedInputAdmission.d.mts +1 -1
- package/dist/durable/Recovery.d.mts +1 -1
- package/dist/durable/RunJournal.d.mts +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 +2 -1
- package/dist/durable/SqlThreadNativeReads.mjs +75 -38
- package/dist/durable/SqlThreadNativeReads.mjs.map +1 -1
- package/dist/durable/SubmissionLedger.d.mts +1 -1
- package/dist/durable/SubmissionLedgerConformance.d.mts +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/ThreadProjectionMaintenance.d.mts +1 -1
- package/dist/durable/ThreadStore.d.mts +2 -2
- package/dist/durable/ThreadStore.mjs +30 -2
- package/dist/durable/ThreadStore.mjs.map +1 -1
- package/dist/durable/ThreadStoreConformance.d.mts +1 -1
- package/dist/durable/ThreadStoreConformance.mjs +34 -3
- package/dist/durable/ThreadStoreConformance.mjs.map +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/SqlThreadNativeReads.ts +75 -2
- package/src/durable/ThreadStore.ts +63 -1
- package/src/durable/ThreadStoreConformance.ts +113 -1
- package/src/durable/internal/agent-updates.ts +4 -0
- package/src/durable/internal/worker-host.ts +17 -18
- package/dist/DurableAgentRuntime-BydyN_n-.mjs.map +0 -1
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
ProducerEpoch,
|
|
14
14
|
RecordId,
|
|
15
15
|
} from "./Records.ts";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
runIdForSubmission,
|
|
18
|
+
subagentLineageRecordId,
|
|
19
|
+
toolCallPreparedRecordId,
|
|
20
|
+
workerOriginRecordId,
|
|
21
|
+
} from "./RunJournal.ts";
|
|
17
22
|
import { SubmissionLedger, SubmissionLookupById } from "./SubmissionLedger.ts";
|
|
18
23
|
|
|
19
24
|
export const MAX_THREAD_EXPORT_RECORDS = 131_072;
|
|
@@ -383,6 +388,60 @@ export class ThreadTail extends Schema.Class<ThreadTail>("@effect-agent/thread/T
|
|
|
383
388
|
producerEpoch: ProducerEpoch,
|
|
384
389
|
}) {}
|
|
385
390
|
|
|
391
|
+
export class ThreadIdentityRequest extends Schema.Class<ThreadIdentityRequest>(
|
|
392
|
+
"@effect-agent/thread/ThreadIdentityRequest",
|
|
393
|
+
)({ threadId: ThreadId }) {}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* One consistent canonical snapshot: tail/epoch, first record, exact worker origin,
|
|
397
|
+
* then exact subagent lineage. A row selected twice appears only once. Missing
|
|
398
|
+
* identity records remain absent; the snapshot itself grants no execution authority.
|
|
399
|
+
*/
|
|
400
|
+
export class ThreadIdentity extends Schema.Class<ThreadIdentity>(
|
|
401
|
+
"@effect-agent/thread/ThreadIdentity",
|
|
402
|
+
)(
|
|
403
|
+
Schema.Struct({
|
|
404
|
+
...ThreadTail.fields,
|
|
405
|
+
records: Schema.Array(CanonicalRecordEnvelope).check(Schema.isMaxLength(3)),
|
|
406
|
+
}).check(
|
|
407
|
+
Schema.makeFilter((snapshot) => {
|
|
408
|
+
if (snapshot.tailSequence === 0) return snapshot.records.length === 0;
|
|
409
|
+
if (snapshot.records[0]?.sequence !== 1) return false;
|
|
410
|
+
const origin = workerOriginRecordId(snapshot.threadId);
|
|
411
|
+
const lineage = subagentLineageRecordId(snapshot.threadId);
|
|
412
|
+
const ids = new Set<RecordId>();
|
|
413
|
+
const sequences = new Set<CanonicalSequence>();
|
|
414
|
+
let previous = -1;
|
|
415
|
+
|
|
416
|
+
for (const entry of snapshot.records) {
|
|
417
|
+
const rank =
|
|
418
|
+
entry.sequence === 1
|
|
419
|
+
? 0
|
|
420
|
+
: entry.record.recordId === origin
|
|
421
|
+
? 1
|
|
422
|
+
: entry.record.recordId === lineage
|
|
423
|
+
? 2
|
|
424
|
+
: -1;
|
|
425
|
+
|
|
426
|
+
if (
|
|
427
|
+
entry.threadId !== snapshot.threadId ||
|
|
428
|
+
entry.sequence < 1 ||
|
|
429
|
+
entry.sequence > snapshot.tailSequence ||
|
|
430
|
+
rank <= previous ||
|
|
431
|
+
ids.has(entry.record.recordId) ||
|
|
432
|
+
sequences.has(entry.sequence)
|
|
433
|
+
)
|
|
434
|
+
return false;
|
|
435
|
+
ids.add(entry.record.recordId);
|
|
436
|
+
sequences.add(entry.sequence);
|
|
437
|
+
previous = rank;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return true;
|
|
441
|
+
}),
|
|
442
|
+
),
|
|
443
|
+
) {}
|
|
444
|
+
|
|
386
445
|
/** Maximum canonical records represented by one Thread export. */
|
|
387
446
|
|
|
388
447
|
export class ThreadExport extends Schema.Class<ThreadExport>("@effect-agent/thread/ThreadExport")({
|
|
@@ -560,6 +619,9 @@ export class ThreadStore extends Context.Service<
|
|
|
560
619
|
readonly inspectTail: (
|
|
561
620
|
request: ThreadTailRequest,
|
|
562
621
|
) => Effect.Effect<ThreadTail, ThreadStoreError | ThreadNotMaterialized>;
|
|
622
|
+
readonly readIdentity: (
|
|
623
|
+
request: ThreadIdentityRequest,
|
|
624
|
+
) => Effect.Effect<ThreadIdentity, ThreadStoreError | ThreadNotMaterialized>;
|
|
563
625
|
/** Absent when this adapter does not support disposable checkpoints. */
|
|
564
626
|
readonly checkpoints?: ThreadCheckpoints | undefined;
|
|
565
627
|
readonly recoveryCheckpoints?: ThreadRecoveryCheckpoints | undefined;
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
SubtreeBudgetReserved,
|
|
41
41
|
PeerMessagePrepared,
|
|
42
42
|
} from "./Records.ts";
|
|
43
|
+
import { subagentLineageRecordId, workerOriginRecordId } from "./RunJournal.ts";
|
|
43
44
|
import {
|
|
44
45
|
type AppendResult,
|
|
45
46
|
AppendConflict,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
ThreadCheckpoint,
|
|
48
49
|
ThreadExportRequest,
|
|
49
50
|
ThreadMaterialization,
|
|
51
|
+
ThreadIdentityRequest,
|
|
50
52
|
ThreadNotMaterialized,
|
|
51
53
|
ThreadObservation,
|
|
52
54
|
ThreadRead,
|
|
@@ -641,10 +643,19 @@ const tailInspection = conformanceCase(
|
|
|
641
643
|
empty.producerEpoch === EPOCH_ONE,
|
|
642
644
|
"An empty Thread must report the zero tail and its registered epoch",
|
|
643
645
|
);
|
|
646
|
+
const emptyIdentity = yield* store.readIdentity(ThreadIdentityRequest.make({ threadId }));
|
|
647
|
+
|
|
648
|
+
yield* ensure(
|
|
649
|
+
emptyIdentity.tailSequence === ZERO_SEQUENCE &&
|
|
650
|
+
emptyIdentity.tailDigest === EMPTY_TAIL_DIGEST &&
|
|
651
|
+
emptyIdentity.producerEpoch === EPOCH_ONE &&
|
|
652
|
+
emptyIdentity.records.length === 0,
|
|
653
|
+
"A materialized empty identity remains distinct from a missing Thread",
|
|
654
|
+
);
|
|
644
655
|
|
|
645
656
|
const first = yield* append(
|
|
646
657
|
threadId,
|
|
647
|
-
batch("inspect-batch-1", [record(
|
|
658
|
+
batch("inspect-batch-1", [record(workerOriginRecordId(threadId), "first")]),
|
|
648
659
|
);
|
|
649
660
|
|
|
650
661
|
const afterAppend = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
|
|
@@ -654,6 +665,18 @@ const tailInspection = conformanceCase(
|
|
|
654
665
|
afterAppend.tailDigest === first.tailDigest,
|
|
655
666
|
"Tail inspection must match the latest AppendResult",
|
|
656
667
|
);
|
|
668
|
+
// Regression: https://github.com/danieljvdm/effect-agent/commit/6a4f4f870
|
|
669
|
+
const identity = yield* store.readIdentity(ThreadIdentityRequest.make({ threadId }));
|
|
670
|
+
|
|
671
|
+
yield* ensure(
|
|
672
|
+
identity.threadId === threadId &&
|
|
673
|
+
identity.tailSequence === first.lastSequence &&
|
|
674
|
+
identity.tailDigest === first.tailDigest &&
|
|
675
|
+
identity.producerEpoch === EPOCH_ONE &&
|
|
676
|
+
identity.records.length === 1 &&
|
|
677
|
+
identity.records[0]?.record.recordId === workerOriginRecordId(threadId),
|
|
678
|
+
"Identity captures the canonical tail and deduplicates a first record at the origin locator",
|
|
679
|
+
);
|
|
657
680
|
|
|
658
681
|
const resumed = yield* append(
|
|
659
682
|
threadId,
|
|
@@ -667,6 +690,22 @@ const tailInspection = conformanceCase(
|
|
|
667
690
|
"An append composed from the inspected tail must commit without exporting the log",
|
|
668
691
|
);
|
|
669
692
|
|
|
693
|
+
const staleIdentity = yield* expectFailure(
|
|
694
|
+
"appending from an identity snapshot after a tail advance",
|
|
695
|
+
append(
|
|
696
|
+
threadId,
|
|
697
|
+
batch("inspect-stale-identity", [record("inspect-record-3", "third")]),
|
|
698
|
+
{ lastSequence: identity.tailSequence, tailDigest: identity.tailDigest },
|
|
699
|
+
identity.producerEpoch,
|
|
700
|
+
),
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
yield* ensure(
|
|
704
|
+
isAppendConflict(staleIdentity) && staleIdentity.reason === "tail",
|
|
705
|
+
"Identity reads do not bypass the canonical append tail fence",
|
|
706
|
+
);
|
|
707
|
+
const beforeTakeover = yield* store.readIdentity(ThreadIdentityRequest.make({ threadId }));
|
|
708
|
+
|
|
670
709
|
yield* materialize(threadId, EPOCH_TWO);
|
|
671
710
|
const afterTakeover = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
|
|
672
711
|
|
|
@@ -675,6 +714,43 @@ const tailInspection = conformanceCase(
|
|
|
675
714
|
afterTakeover.tailSequence === resumed.lastSequence,
|
|
676
715
|
"Tail inspection must reflect epoch takeover without disturbing the tail",
|
|
677
716
|
);
|
|
717
|
+
|
|
718
|
+
const fencedIdentity = yield* expectFailure(
|
|
719
|
+
"appending from an identity snapshot after an epoch-only takeover",
|
|
720
|
+
append(
|
|
721
|
+
threadId,
|
|
722
|
+
batch("inspect-stale-epoch", [record("inspect-record-3", "third")]),
|
|
723
|
+
{ lastSequence: beforeTakeover.tailSequence, tailDigest: beforeTakeover.tailDigest },
|
|
724
|
+
beforeTakeover.producerEpoch,
|
|
725
|
+
),
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
yield* ensure(
|
|
729
|
+
isFenceRejected(fencedIdentity),
|
|
730
|
+
"An epoch-only takeover rejects the identity snapshot's captured producer fence",
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
const afterTakeoverIdentity = yield* store.readIdentity(
|
|
734
|
+
ThreadIdentityRequest.make({ threadId }),
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const retried = yield* append(
|
|
738
|
+
threadId,
|
|
739
|
+
batch("inspect-retry-identity", [record("inspect-record-3", "third")]),
|
|
740
|
+
{
|
|
741
|
+
lastSequence: afterTakeoverIdentity.tailSequence,
|
|
742
|
+
tailDigest: afterTakeoverIdentity.tailDigest,
|
|
743
|
+
},
|
|
744
|
+
afterTakeoverIdentity.producerEpoch,
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
yield* ensure(
|
|
748
|
+
afterTakeoverIdentity.producerEpoch === EPOCH_TWO &&
|
|
749
|
+
afterTakeoverIdentity.tailSequence === resumed.lastSequence &&
|
|
750
|
+
afterTakeoverIdentity.records.length === 1 &&
|
|
751
|
+
retried.lastSequence === resumed.lastSequence + 1,
|
|
752
|
+
"Rereading identity after takeover retains the bounded facts and permits the current producer",
|
|
753
|
+
);
|
|
678
754
|
}),
|
|
679
755
|
);
|
|
680
756
|
|
|
@@ -705,6 +781,10 @@ const notMaterializedOperations = conformanceCase(
|
|
|
705
781
|
"inspecting the tail of an unmaterialized Thread",
|
|
706
782
|
store.inspectTail(ThreadTailRequest.make({ threadId })),
|
|
707
783
|
),
|
|
784
|
+
yield* expectFailure(
|
|
785
|
+
"reading the identity of an unmaterialized Thread",
|
|
786
|
+
store.readIdentity(ThreadIdentityRequest.make({ threadId })),
|
|
787
|
+
),
|
|
708
788
|
];
|
|
709
789
|
|
|
710
790
|
yield* Effect.forEach(
|
|
@@ -1040,6 +1120,14 @@ const nativeWorkerAccounting = conformanceCase(
|
|
|
1040
1120
|
Option.isSome(yield* getRecord({ threadId, recordId: decodeRecordId("requested") })),
|
|
1041
1121
|
"Proven completion retires the input while retaining its canonical reservation",
|
|
1042
1122
|
);
|
|
1123
|
+
// Regression: https://github.com/danieljvdm/effect-agent/commit/6a4f4f870
|
|
1124
|
+
// Payloads do not select identity: the exact lineage locator precedes the
|
|
1125
|
+
// exact origin locator in the log, while replies retain first/origin/lineage order.
|
|
1126
|
+
tail = yield* append(
|
|
1127
|
+
threadId,
|
|
1128
|
+
batch("identity-lineage", [record(subagentLineageRecordId(threadId), "lineage")]),
|
|
1129
|
+
tail,
|
|
1130
|
+
);
|
|
1043
1131
|
// Cross the recovery horizon with irrelevant conversation records. Native worker
|
|
1044
1132
|
// accounting must return the same families and scope, while capturing the full CAS tail.
|
|
1045
1133
|
for (let base = 0; base < 4097; base += 256) {
|
|
@@ -1060,6 +1148,30 @@ const nativeWorkerAccounting = conformanceCase(
|
|
|
1060
1148
|
tail,
|
|
1061
1149
|
);
|
|
1062
1150
|
}
|
|
1151
|
+
tail = yield* append(
|
|
1152
|
+
threadId,
|
|
1153
|
+
batch("identity-origin", [record(workerOriginRecordId(threadId), "origin")]),
|
|
1154
|
+
tail,
|
|
1155
|
+
);
|
|
1156
|
+
const identity = yield* store.readIdentity(ThreadIdentityRequest.make({ threadId }));
|
|
1157
|
+
|
|
1158
|
+
yield* ensure(
|
|
1159
|
+
identity.threadId === threadId &&
|
|
1160
|
+
identity.tailSequence === tail.lastSequence &&
|
|
1161
|
+
identity.tailDigest === tail.tailDigest &&
|
|
1162
|
+
identity.producerEpoch === EPOCH_ONE &&
|
|
1163
|
+
identity.records.length === 3 &&
|
|
1164
|
+
identity.records.map(({ record }) => record.recordId).join() ===
|
|
1165
|
+
[
|
|
1166
|
+
"requested",
|
|
1167
|
+
workerOriginRecordId(threadId),
|
|
1168
|
+
subagentLineageRecordId(threadId),
|
|
1169
|
+
].join() &&
|
|
1170
|
+
identity.records.every(
|
|
1171
|
+
(entry) => entry.threadId === threadId && entry.sequence <= identity.tailSequence,
|
|
1172
|
+
),
|
|
1173
|
+
"Identity selects the exact canonical locators in semantic order beyond the recovery horizon",
|
|
1174
|
+
);
|
|
1063
1175
|
|
|
1064
1176
|
const budgetRow = (id: string, sourceSubmissionId: SubmissionId) =>
|
|
1065
1177
|
envelope(
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
ThreadStore,
|
|
35
35
|
ThreadTailRequest,
|
|
36
36
|
} from "../ThreadStore.ts";
|
|
37
|
+
import { WakeScheduler } from "../WakeScheduler.ts";
|
|
37
38
|
import { WorkerRuntime } from "./worker-runtime.ts";
|
|
38
39
|
|
|
39
40
|
export const lastWorkerReportMessageId = (records: ReadonlyArray<CanonicalRecordEnvelope>) => {
|
|
@@ -62,6 +63,7 @@ export const makeAgentUpdateRuntime = Effect.fn("AgentUpdates.makeDurable")(func
|
|
|
62
63
|
}) {
|
|
63
64
|
const store = yield* ThreadStore;
|
|
64
65
|
const workers = yield* WorkerRuntime;
|
|
66
|
+
const wake = yield* WakeScheduler;
|
|
65
67
|
const deliveries = yield* Effect.serviceOption(MessageDeliveryStore);
|
|
66
68
|
const crypto = yield* Crypto.Crypto;
|
|
67
69
|
const failpoint = yield* DurableRuntimeFailpoint;
|
|
@@ -290,6 +292,8 @@ export const makeAgentUpdateRuntime = Effect.fn("AgentUpdates.makeDurable")(func
|
|
|
290
292
|
|
|
291
293
|
if (!appended) continue;
|
|
292
294
|
yield* failpoint.hit("update:after-canonical-append");
|
|
295
|
+
// Canonical progress is observable even when no parent report is selected.
|
|
296
|
+
yield* wake.notify(request.submission.threadId);
|
|
293
297
|
yield* insert(payload);
|
|
294
298
|
|
|
295
299
|
return update;
|
|
@@ -84,7 +84,6 @@ import {
|
|
|
84
84
|
workerInputRecordId,
|
|
85
85
|
agentUpdateRecordId,
|
|
86
86
|
firstWorkerInputRecordId,
|
|
87
|
-
subagentLineageRecordId,
|
|
88
87
|
workerOriginRecordId,
|
|
89
88
|
workerReportRecordId,
|
|
90
89
|
} from "../RunJournal.ts";
|
|
@@ -108,6 +107,7 @@ import {
|
|
|
108
107
|
import type { SubmissionStatus } from "../SubmissionStatus.ts";
|
|
109
108
|
import { PreparedInput } from "../Subscription.ts";
|
|
110
109
|
import {
|
|
110
|
+
ThreadIdentityRequest,
|
|
111
111
|
ThreadStore,
|
|
112
112
|
getRecord,
|
|
113
113
|
getRunInput,
|
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
type ThreadExport,
|
|
118
118
|
MAX_THREAD_EXPORT_RECORDS,
|
|
119
119
|
ThreadRead,
|
|
120
|
+
type ThreadTail,
|
|
120
121
|
ThreadTailRequest,
|
|
121
122
|
} from "../ThreadStore.ts";
|
|
122
123
|
import {
|
|
@@ -492,18 +493,9 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
|
|
|
492
493
|
threadId: ThreadId,
|
|
493
494
|
operation: WorkerError["operation"],
|
|
494
495
|
) {
|
|
495
|
-
|
|
496
|
-
.
|
|
496
|
+
return yield* deps.store
|
|
497
|
+
.readIdentity(ThreadIdentityRequest.make({ threadId }))
|
|
497
498
|
.pipe(Effect.mapError(storageFailure(operation)));
|
|
498
|
-
|
|
499
|
-
const first = yield* deps.store
|
|
500
|
-
.read(ThreadRead.make({ threadId, limit: 1 }))
|
|
501
|
-
.pipe(Stream.runCollect, Effect.mapError(storageFailure(operation)));
|
|
502
|
-
|
|
503
|
-
const worker = yield* exactRecord(threadId, workerOriginRecordId(threadId), operation);
|
|
504
|
-
const attached = yield* exactRecord(threadId, subagentLineageRecordId(threadId), operation);
|
|
505
|
-
|
|
506
|
-
return { ...tail, records: [...first, ...Option.toArray(worker), ...Option.toArray(attached)] };
|
|
507
499
|
});
|
|
508
500
|
|
|
509
501
|
const readWorkerState = Effect.fn("WorkerHost.readWorkerState")(function* (
|
|
@@ -536,23 +528,30 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
|
|
|
536
528
|
| WorkerReportPrepared
|
|
537
529
|
| WorkerReportRefused
|
|
538
530
|
| SubtreeBudgetReserved,
|
|
539
|
-
current: Pick<ThreadExport, "tailSequence" | "tailDigest" | "records"
|
|
531
|
+
current: Pick<ThreadExport, "tailSequence" | "tailDigest" | "records"> &
|
|
532
|
+
Partial<Pick<ThreadTail, "producerEpoch">>,
|
|
540
533
|
phase: "source" | "origin" | "completion" | "subtree" | "report" | "stop",
|
|
541
534
|
acknowledgements: ReadonlyArray<WorkerInputCompleted> = [],
|
|
542
535
|
) {
|
|
543
536
|
const operation = "start";
|
|
544
|
-
|
|
537
|
+
let producerEpoch = current.producerEpoch;
|
|
538
|
+
|
|
539
|
+
if (producerEpoch === undefined) {
|
|
540
|
+
const tail = yield* deps.store.inspectTail(ThreadTailRequest.make({ threadId }));
|
|
541
|
+
|
|
542
|
+
if (tail.tailSequence !== current.tailSequence || tail.tailDigest !== current.tailDigest)
|
|
543
|
+
return false;
|
|
544
|
+
producerEpoch = tail.producerEpoch;
|
|
545
|
+
}
|
|
545
546
|
|
|
546
|
-
//
|
|
547
|
-
if (tail.tailSequence !== current.tailSequence || tail.tailDigest !== current.tailDigest)
|
|
548
|
-
return false;
|
|
547
|
+
// The store atomically fences the validated prefix and its producer epoch.
|
|
549
548
|
yield* hit(`worker:before-${phase}-append`, operation);
|
|
550
549
|
|
|
551
550
|
const result = yield* deps.store
|
|
552
551
|
.append(
|
|
553
552
|
FencedAppendRequest.make({
|
|
554
553
|
threadId,
|
|
555
|
-
producerEpoch
|
|
554
|
+
producerEpoch,
|
|
556
555
|
expectedTailSequence: current.tailSequence,
|
|
557
556
|
expectedTailDigest: current.tailDigest,
|
|
558
557
|
batch: CanonicalBatch.make({
|