effect-agent 0.1.0-beta.150 → 0.1.0-beta.152

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 (51) hide show
  1. package/dist/{Admin-B8ISZyYf.d.mts → Admin-CKIdJoP8.d.mts} +6 -5
  2. package/dist/{DurableAgentRuntime-DLa4IwFi.mjs → DurableAgentRuntime-DQFV26SB.mjs} +68 -82
  3. package/dist/DurableAgentRuntime-DQFV26SB.mjs.map +1 -0
  4. package/dist/{EventSource-CkN05NT3.d.mts → EventSource-CMbXXmwe.d.mts} +2 -2
  5. package/dist/{PreparedInputAdmission-fctZOTnn.d.mts → PreparedInputAdmission-Bt6rgtVu.d.mts} +3 -3
  6. package/dist/{Subscription-B16A-48F.d.mts → Subscription-BJAzYNw1.d.mts} +2 -2
  7. package/dist/{SubscriptionInput-DIU7CiXE.d.mts → SubscriptionInput-CkBJt0AS.d.mts} +2 -2
  8. package/dist/WorkerAdmission-CJ_oWRuS.mjs +171 -0
  9. package/dist/WorkerAdmission-CJ_oWRuS.mjs.map +1 -0
  10. package/dist/durable/Admin.d.mts +1 -1
  11. package/dist/durable/AgentRegistration.d.mts +1 -1
  12. package/dist/durable/Certification.d.mts +1 -1
  13. package/dist/durable/CommittedActivity.d.mts +1 -1
  14. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  15. package/dist/durable/DurableAgentRuntime.mjs +2 -1
  16. package/dist/durable/EventSource.d.mts +1 -1
  17. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  18. package/dist/durable/LifecyclePublication.d.mts +1 -1
  19. package/dist/durable/MessageDelivery.d.mts +41 -41
  20. package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
  21. package/dist/durable/PersistentHistory.d.mts +1 -1
  22. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  23. package/dist/durable/Recovery.d.mts +1 -1
  24. package/dist/durable/RunJournal.d.mts +1 -1
  25. package/dist/durable/Scheduling.d.mts +1 -1
  26. package/dist/durable/SubmissionLedger.d.mts +1 -1
  27. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  28. package/dist/durable/SubmissionStatus.d.mts +1 -1
  29. package/dist/durable/Subscription.d.mts +1 -1
  30. package/dist/durable/SubscriptionInput.d.mts +1 -1
  31. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  32. package/dist/durable/SubscriptionTools.d.mts +2 -2
  33. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  34. package/dist/durable/Subscriptions.d.mts +5 -5
  35. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  36. package/dist/durable/ThreadInvariants.d.mts +1 -1
  37. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  38. package/dist/durable/ThreadStore.d.mts +1 -1
  39. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  40. package/dist/durable/WorkerAdmission.d.mts +172 -0
  41. package/dist/durable/WorkerAdmission.mjs +2 -0
  42. package/dist/durable/WorkerHost.d.mts +1 -1
  43. package/dist/index.d.mts +7 -6
  44. package/dist/index.mjs +3 -2
  45. package/package.json +1 -1
  46. package/src/durable/DurableAgentRuntime.ts +79 -109
  47. package/src/durable/WorkerAdmission.ts +104 -0
  48. package/src/durable/internal/thread-initialization.ts +195 -0
  49. package/src/durable/internal/worker-host.ts +121 -123
  50. package/src/index.ts +1 -0
  51. package/dist/DurableAgentRuntime-DLa4IwFi.mjs.map +0 -1
@@ -163,6 +163,7 @@ import {
163
163
  } from "./internal/journal-checkpoint.ts";
164
164
  import { makeJournalMetadata, type JournalMetadata } from "./internal/journal-metadata.ts";
165
165
  import { makeMessagingRuntime } from "./internal/messaging-host.ts";
166
+ import * as ThreadInitialization from "./internal/thread-initialization.ts";
166
167
  import { makeWorkerRuntime, WorkerInputControl } from "./internal/worker-host.ts";
167
168
  import { WorkerRuntime } from "./internal/worker-runtime.ts";
168
169
  import {
@@ -181,7 +182,6 @@ import {
181
182
  CanonicalBatch,
182
183
  CanonicalSequence,
183
184
  CompactionCreated,
184
- ThreadCreated,
185
185
  DefinitionDigests,
186
186
  ModelResponseInterrupted,
187
187
  PersistedJson,
@@ -357,6 +357,7 @@ import {
357
357
  } from "./ThreadStore.ts";
358
358
  import { PreparedToolCallEvidence, ToolReconciler } from "./ToolReconciler.ts";
359
359
  import { WakeScheduler } from "./WakeScheduler.ts";
360
+ import { WorkerAdmissionPort, WorkerAdmissionRequest } from "./WorkerAdmission.ts";
360
361
 
361
362
  // Capture the tracing call site once; each application still creates a fresh Attempt span.
362
363
  const withThreadHeadSpan = Effect.withSpan("DurableAgentRuntime.processThreadHead");
@@ -536,13 +537,7 @@ const childUsageReportsOf = (state: SubagentCallRecords): ReadonlyArray<ChildRun
536
537
  });
537
538
  });
538
539
 
539
- /** Deterministic batch identity of one Thread's initial `ThreadCreated` append. */
540
- export const threadCreatedBatchId = (threadId: ThreadId): BatchId =>
541
- decodeBatchId(`thread-created:${threadId}`);
542
-
543
- /** Deterministic record identity of one Thread's `ThreadCreated` record. */
544
- export const threadCreatedRecordId = (threadId: ThreadId): RecordId =>
545
- decodeRecordId(`thread-created:${threadId}`);
540
+ export { threadCreatedBatchId, threadCreatedRecordId } from "./internal/thread-initialization.ts";
546
541
 
547
542
  /** Deterministic batch identity of one executed recovery decision's audit append (DUR-013). */
548
543
  export const recoveryRepairBatchId = (submissionId: SubmissionId, decisionTag: string): BatchId =>
@@ -1262,6 +1257,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1262
1257
  bindings: ReadonlyArray<ResolvedBinding>,
1263
1258
  ) {
1264
1259
  const registeredBindings = [...bindings];
1260
+ const workerAdmissionPort = yield* WorkerAdmissionPort;
1265
1261
  const ledger = yield* SubmissionLedger;
1266
1262
  const submissionScheduling = yield* SubmissionScheduling;
1267
1263
  const store = yield* ThreadStore;
@@ -2380,67 +2376,17 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
2380
2376
  .materialize(ThreadMaterialization.make({ threadId, producerEpoch }))
2381
2377
  .pipe(Effect.catchTag("FenceRejected", () => Effect.void));
2382
2378
 
2383
- /**
2384
- * Coordinator invariant: every Thread's first canonical record is `ThreadCreated`,
2385
- * so `tailSequence >= 1` is the deterministic already-created check. A lost race (conflict or
2386
- * fence) is verified against that invariant instead of being trusted blindly. An admitted
2387
- * child can be claimed before it is runnable; that advances the shared storage fence without
2388
- * appending anything. Re-read and retry that initialization race with the current fence.
2389
- */
2390
- const ensureThreadCreated = Effect.fn("DurableAgentRuntime.ensureThreadCreated")(
2391
- function* (
2392
- threadId: ThreadId,
2393
- agentId: AgentId,
2394
- definitions: DefinitionDigests,
2395
- ): Effect.fn.Return<
2396
- void,
2397
- ThreadStoreError | ThreadNotMaterialized | AppendConflict | FenceRejected
2398
- > {
2399
- const tail = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
2400
-
2401
- if (tail.tailSequence > 0) return;
2402
-
2403
- const record = yield* makeEnvelope(
2404
- threadCreatedRecordId(threadId),
2405
- ThreadCreated.make({ agentId, definitions }),
2406
- );
2407
-
2408
- yield* store
2409
- .append(
2410
- FencedAppendRequest.make({
2411
- threadId,
2412
- batch: CanonicalBatch.make({
2413
- batchId: threadCreatedBatchId(threadId),
2414
- producerId: config.producerId,
2415
- records: [record],
2416
- }),
2417
- expectedTailSequence: tail.tailSequence,
2418
- expectedTailDigest: tail.tailDigest,
2419
- producerEpoch: tail.producerEpoch,
2420
- }),
2421
- )
2422
- .pipe(
2423
- Effect.catchTag(["AppendConflict", "FenceRejected"], (error) =>
2424
- store
2425
- .inspectTail(ThreadTailRequest.make({ threadId }))
2426
- .pipe(
2427
- Effect.flatMap((current) =>
2428
- current.tailSequence > 0 ? Effect.void : Effect.fail(error),
2429
- ),
2430
- ),
2431
- ),
2432
- Effect.andThen(wake.notify(threadId)),
2433
- Effect.asVoid,
2434
- );
2435
- },
2436
- (effect) =>
2437
- effect.pipe(
2438
- Effect.retry({
2439
- times: 7,
2440
- while: (error) => error._tag === "AppendConflict" || error._tag === "FenceRejected",
2441
- }),
2442
- ),
2443
- );
2379
+ const ensureThreadCreated = (
2380
+ threadId: ThreadId,
2381
+ agentId: AgentId,
2382
+ definitions: DefinitionDigests,
2383
+ ) =>
2384
+ ThreadInitialization.ensureThreadCreated(
2385
+ { producerId: config.producerId, deploymentId: config.deploymentId },
2386
+ threadId,
2387
+ agentId,
2388
+ definitions,
2389
+ ).pipe(Effect.provideService(ThreadStore, store), Effect.provideService(WakeScheduler, wake));
2444
2390
 
2445
2391
  const attemptContextFor = Effect.fn("DurableAgentRuntime.attemptContextFor")(function* (
2446
2392
  threadId: ThreadId,
@@ -7068,7 +7014,17 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
7068
7014
  : {}),
7069
7015
  approval,
7070
7016
  toolAuthorization,
7071
- ...(journal.toolSelection === undefined ? {} : { toolSelection: journal.toolSelection }),
7017
+ ...(journal.toolSelection === undefined
7018
+ ? {}
7019
+ : {
7020
+ toolSelection: {
7021
+ // A settled discovery receipt survives deployment; its selection only
7022
+ // names current tools. Keep validation of fresh selections fail-closed.
7023
+ toolNames: journal.toolSelection.toolNames.filter((name) =>
7024
+ Object.hasOwn(agent.definition.toolkit.tools, name),
7025
+ ),
7026
+ },
7027
+ }),
7072
7028
  durability,
7073
7029
  subagent,
7074
7030
  delegationDepth,
@@ -10109,41 +10065,64 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10109
10065
  });
10110
10066
  }
10111
10067
 
10112
- const admitted = yield* ledger.admit(
10113
- yield* Schema.decodeEffect(AdmissionRequest)({
10114
- threadId: options.threadId,
10115
- principal: options.principal,
10116
- idempotencyKey: options.idempotencyKey,
10117
- ...(options.admissionGroup === undefined ? {} : { admissionGroup: options.admissionGroup }),
10118
- ...(options.admissionFence === undefined ? {} : { admissionFence: options.admissionFence }),
10119
- agentId: agent.definition.id,
10120
- agentDigests: options.definitions,
10121
- deploymentId: config.deploymentId,
10122
- inputPayload,
10123
- inputDigest,
10124
- ...(workerAdmission === undefined
10125
- ? {}
10126
- : {
10127
- workerAdmission: yield* Schema.encodeEffect(WorkerAdmission)(workerAdmission).pipe(
10128
- Effect.mapError(() =>
10129
- LedgerError.make({
10130
- operation: "submit",
10131
- message: "Worker admission cannot be encoded",
10132
- }),
10133
- ),
10068
+ const request = yield* Schema.decodeEffect(AdmissionRequest)({
10069
+ threadId: options.threadId,
10070
+ principal: options.principal,
10071
+ idempotencyKey: options.idempotencyKey,
10072
+ ...(options.admissionGroup === undefined ? {} : { admissionGroup: options.admissionGroup }),
10073
+ ...(options.admissionFence === undefined ? {} : { admissionFence: options.admissionFence }),
10074
+ agentId: agent.definition.id,
10075
+ agentDigests: options.definitions,
10076
+ deploymentId: config.deploymentId,
10077
+ inputPayload,
10078
+ inputDigest,
10079
+ ...(workerAdmission === undefined
10080
+ ? {}
10081
+ : {
10082
+ workerAdmission: yield* Schema.encodeEffect(WorkerAdmission)(workerAdmission).pipe(
10083
+ Effect.mapError(() =>
10084
+ LedgerError.make({
10085
+ operation: "submit",
10086
+ message: "Worker admission cannot be encoded",
10087
+ }),
10134
10088
  ),
10135
- }),
10136
- ...(messageAdmission === undefined ? {} : { messageAdmission }),
10137
- }).pipe(
10138
- Effect.mapError(() =>
10139
- LedgerError.make({
10140
- operation: "submit",
10141
- message: "Admission fields do not satisfy the ledger contract",
10089
+ ),
10142
10090
  }),
10143
- ),
10091
+ ...(messageAdmission === undefined ? {} : { messageAdmission }),
10092
+ }).pipe(
10093
+ Effect.mapError(() =>
10094
+ LedgerError.make({
10095
+ operation: "submit",
10096
+ message: "Admission fields do not satisfy the ledger contract",
10097
+ }),
10144
10098
  ),
10145
10099
  );
10146
10100
 
10101
+ if (workerAdmission !== undefined) {
10102
+ const admitted = yield* workerAdmissionPort
10103
+ .admit(
10104
+ WorkerAdmissionRequest.make({
10105
+ ...request,
10106
+ workerAdmission,
10107
+ producerId: config.producerId,
10108
+ }),
10109
+ )
10110
+ .pipe(
10111
+ Effect.provideService(SubmissionLedger, ledger),
10112
+ Effect.provideService(ThreadStore, store),
10113
+ Effect.provideService(WakeScheduler, wake),
10114
+ Effect.provideService(DurableRuntimeFailpoint, failpoint),
10115
+ );
10116
+
10117
+ return Receipt.make({
10118
+ receiptId: admitted.receiptId,
10119
+ submissionId: admitted.submissionId,
10120
+ threadId: request.threadId,
10121
+ queueSequence: admitted.queueSequence,
10122
+ });
10123
+ }
10124
+ const admitted = yield* ledger.admit(request);
10125
+
10147
10126
  yield* hit("submit:after-admit");
10148
10127
 
10149
10128
  const receipt = Receipt.make({
@@ -10160,15 +10139,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
10160
10139
  }
10161
10140
  yield* materializeAtLeast(options.threadId, ZERO_EPOCH);
10162
10141
  yield* ensureThreadCreated(options.threadId, agent.definition.id, options.definitions);
10163
- if (workerAdmission !== undefined) {
10164
- yield* workerRuntime
10165
- .ensureOrigin(workerAdmission.origin)
10166
- .pipe(
10167
- Effect.mapError((cause) =>
10168
- LedgerError.make({ operation: "worker-origin", message: cause.reason, cause }),
10169
- ),
10170
- );
10171
- }
10172
10142
  yield* hit("submit:after-materialize");
10173
10143
  yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
10174
10144
  yield* wake.notify(options.threadId);
@@ -0,0 +1,104 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+
3
+ import { DurableRuntimeFailpoint, DurableRuntimeFailpointError } from "./DurableFailpoint.ts";
4
+ import { ensureThreadCreated, ensureWorkerOrigin } from "./internal/thread-initialization.ts";
5
+ import { ProducerEpoch, ProducerId, WorkerAdmission } from "./Records.ts";
6
+ import {
7
+ AdmissionConflict,
8
+ AdmissionPolicyError,
9
+ AdmissionRequest,
10
+ LedgerError,
11
+ MarkReadyRequest,
12
+ SubmissionLedger,
13
+ } from "./SubmissionLedger.ts";
14
+ import {
15
+ AppendConflict,
16
+ FenceRejected,
17
+ ThreadMaterialization,
18
+ ThreadNotMaterialized,
19
+ ThreadStore,
20
+ ThreadStoreError,
21
+ } from "./ThreadStore.ts";
22
+ import { WakeScheduler } from "./WakeScheduler.ts";
23
+
24
+ /**
25
+ * Storage admission after source authorization, input validation and capacity reservation.
26
+ * This is an internal host port, not an authorization entrypoint. Hosts route the whole
27
+ * operation to the destination owner and enroll recovery before executing it.
28
+ */
29
+ export class WorkerAdmissionRequest extends Schema.Class<WorkerAdmissionRequest>(
30
+ "@effect-agent/thread/WorkerAdmissionRequest",
31
+ )({
32
+ ...AdmissionRequest.fields,
33
+ workerAdmission: WorkerAdmission,
34
+ producerId: ProducerId,
35
+ }) {}
36
+
37
+ export const WorkerAdmissionFailure = Schema.Union([
38
+ AdmissionConflict,
39
+ AdmissionPolicyError,
40
+ LedgerError,
41
+ ThreadStoreError,
42
+ ThreadNotMaterialized,
43
+ AppendConflict,
44
+ FenceRejected,
45
+ DurableRuntimeFailpointError,
46
+ ]);
47
+
48
+ /** Local implementation shared by every host; retries finish the same admitted row. */
49
+ export const admitWorker = Effect.fn("WorkerAdmission.admitWorker")(function* (
50
+ request: WorkerAdmissionRequest,
51
+ ) {
52
+ const origin = request.workerAdmission.origin;
53
+
54
+ if (
55
+ origin.worker.threadId !== request.threadId ||
56
+ origin.worker.targetAgentId !== request.agentId ||
57
+ request.parentLinkage !== undefined
58
+ )
59
+ return yield* LedgerError.make({
60
+ operation: "admit worker",
61
+ message: "Worker admission does not belong to the destination Thread",
62
+ });
63
+
64
+ const ledger = yield* SubmissionLedger;
65
+ const store = yield* ThreadStore;
66
+ const wake = yield* WakeScheduler;
67
+ const failpoint = yield* DurableRuntimeFailpoint;
68
+ const admitted = yield* ledger.admit(AdmissionRequest.make(request));
69
+
70
+ yield* failpoint.hit("submit:after-admit");
71
+ if (admitted.replayed && admitted.state !== "admitted") return admitted;
72
+
73
+ yield* store
74
+ .materialize(
75
+ ThreadMaterialization.make({
76
+ threadId: request.threadId,
77
+ producerEpoch: Schema.decodeSync(ProducerEpoch)(0),
78
+ }),
79
+ )
80
+ .pipe(Effect.catchTag("FenceRejected", () => Effect.void));
81
+
82
+ const config = {
83
+ producerId: request.producerId,
84
+ deploymentId: request.deploymentId,
85
+ };
86
+
87
+ yield* ensureThreadCreated(config, request.threadId, request.agentId, request.agentDigests);
88
+ yield* ensureWorkerOrigin(config, origin).pipe(
89
+ Effect.mapError((cause) =>
90
+ LedgerError.make({ operation: "worker-origin", message: cause.reason, cause }),
91
+ ),
92
+ );
93
+ yield* failpoint.hit("submit:after-materialize");
94
+ yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
95
+ yield* wake.notify(request.threadId);
96
+
97
+ return admitted;
98
+ });
99
+
100
+ /** Placement adapter for the composite operation; local hosts use the same implementation. */
101
+ export const WorkerAdmissionPort = Context.Reference<{ readonly admit: typeof admitWorker }>(
102
+ "@effect-agent/thread/WorkerAdmissionPort",
103
+ { defaultValue: () => ({ admit: admitWorker }) },
104
+ );
@@ -0,0 +1,195 @@
1
+ import { DateTime, Effect, Schema } from "effect";
2
+
3
+ import type { AgentId, ThreadId } from "../../core/Identifiers.ts";
4
+ import { WorkerError } from "../../core/Worker.ts";
5
+ import { DurableRuntimeFailpoint } from "../DurableFailpoint.ts";
6
+ import {
7
+ BatchId,
8
+ CanonicalBatch,
9
+ RecordEnvelope,
10
+ RecordId,
11
+ ThreadCreated,
12
+ WorkerOrigin,
13
+ WorkerOriginRecorded,
14
+ type DefinitionDigests,
15
+ type DeploymentId,
16
+ type ProducerId,
17
+ } from "../Records.ts";
18
+ import { workerOriginRecordId } from "../RunJournal.ts";
19
+ import type {
20
+ AppendConflict,
21
+ FenceRejected,
22
+ ThreadNotMaterialized,
23
+ ThreadStoreError,
24
+ } from "../ThreadStore.ts";
25
+ import {
26
+ FencedAppendRequest,
27
+ ThreadIdentityRequest,
28
+ ThreadTailRequest,
29
+ ThreadStore,
30
+ } from "../ThreadStore.ts";
31
+ import { WakeScheduler } from "../WakeScheduler.ts";
32
+ import { definitionDigestsEqual } from "./agent-registration.ts";
33
+
34
+ interface InitializationConfig {
35
+ readonly producerId: ProducerId;
36
+ readonly deploymentId: DeploymentId;
37
+ }
38
+
39
+ /** Stable identities retained across admission retries and recovery. */
40
+ export const threadCreatedBatchId = (threadId: ThreadId): BatchId =>
41
+ Schema.decodeSync(BatchId)(`thread-created:${threadId}`);
42
+
43
+ export const threadCreatedRecordId = (threadId: ThreadId): RecordId =>
44
+ Schema.decodeSync(RecordId)(`thread-created:${threadId}`);
45
+
46
+ /**
47
+ * Coordinator invariant: every Thread's first canonical record is `ThreadCreated`,
48
+ * so `tailSequence >= 1` is the deterministic already-created check. A lost race (conflict or
49
+ * fence) is verified against that invariant instead of being trusted blindly. An admitted
50
+ * child can be claimed before it is runnable; that advances the shared storage fence without
51
+ * appending anything. Re-read and retry that initialization race with the current fence.
52
+ */
53
+ export const ensureThreadCreated = Effect.fn("DurableAgentRuntime.ensureThreadCreated")(
54
+ function* (
55
+ config: InitializationConfig,
56
+ threadId: ThreadId,
57
+ agentId: AgentId,
58
+ definitions: DefinitionDigests,
59
+ ): Effect.fn.Return<
60
+ void,
61
+ ThreadStoreError | ThreadNotMaterialized | AppendConflict | FenceRejected,
62
+ ThreadStore | WakeScheduler
63
+ > {
64
+ const store = yield* ThreadStore;
65
+ const wake = yield* WakeScheduler;
66
+ const tail = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
67
+
68
+ if (tail.tailSequence > 0) return;
69
+
70
+ const record = yield* Effect.map(DateTime.now, (createdAt) =>
71
+ RecordEnvelope.make({
72
+ recordId: threadCreatedRecordId(threadId),
73
+ family: "thread",
74
+ schemaVersion: 1,
75
+ createdAt,
76
+ deploymentId: config.deploymentId,
77
+ payload: ThreadCreated.make({ agentId, definitions }),
78
+ }),
79
+ );
80
+
81
+ yield* store
82
+ .append(
83
+ FencedAppendRequest.make({
84
+ threadId,
85
+ batch: CanonicalBatch.make({
86
+ batchId: threadCreatedBatchId(threadId),
87
+ producerId: config.producerId,
88
+ records: [record],
89
+ }),
90
+ expectedTailSequence: tail.tailSequence,
91
+ expectedTailDigest: tail.tailDigest,
92
+ producerEpoch: tail.producerEpoch,
93
+ }),
94
+ )
95
+ .pipe(
96
+ Effect.catchTag(["AppendConflict", "FenceRejected"], (error) =>
97
+ store
98
+ .inspectTail(ThreadTailRequest.make({ threadId }))
99
+ .pipe(
100
+ Effect.flatMap((current) =>
101
+ current.tailSequence > 0 ? Effect.void : Effect.fail(error),
102
+ ),
103
+ ),
104
+ ),
105
+ Effect.andThen(wake.notify(threadId)),
106
+ Effect.asVoid,
107
+ );
108
+ },
109
+ (effect) =>
110
+ effect.pipe(
111
+ Effect.retry({
112
+ times: 7,
113
+ while: (error) => error._tag === "AppendConflict" || error._tag === "FenceRejected",
114
+ }),
115
+ ),
116
+ );
117
+
118
+ const sameOrigin = Schema.toEquivalence(WorkerOrigin);
119
+
120
+ export const ensureWorkerOrigin = Effect.fn("WorkerHost.ensureOrigin")(
121
+ function* (config: InitializationConfig, origin: WorkerOrigin) {
122
+ const store = yield* ThreadStore;
123
+ const failpoint = yield* DurableRuntimeFailpoint;
124
+
125
+ for (let attempt = 0; attempt < 16; attempt++) {
126
+ const current = yield* store.readIdentity(
127
+ ThreadIdentityRequest.make({ threadId: origin.worker.threadId }),
128
+ );
129
+
130
+ const existing = current.records.find(
131
+ ({ record }) => record.payload._tag === "WorkerOriginRecorded",
132
+ )?.record.payload;
133
+
134
+ if (existing?._tag === "WorkerOriginRecorded") {
135
+ if (!sameOrigin(existing.origin, origin))
136
+ return yield* WorkerError.make({ operation: "start", reason: "worker-mismatch" });
137
+
138
+ return;
139
+ }
140
+ const first = current.records[0]?.record.payload;
141
+
142
+ if (
143
+ first?._tag !== "ThreadCreated" ||
144
+ first.agentId !== origin.worker.targetAgentId ||
145
+ !definitionDigestsEqual(first.definitions, origin.targetDigests) ||
146
+ current.records.some(({ record }) => record.payload._tag === "SubagentLineageRecorded")
147
+ )
148
+ return yield* WorkerError.make({ operation: "start", reason: "worker-mismatch" });
149
+ const id = workerOriginRecordId(origin.worker.threadId);
150
+
151
+ yield* failpoint.hit("worker:before-origin-append");
152
+
153
+ const appended = yield* store
154
+ .append(
155
+ FencedAppendRequest.make({
156
+ threadId: origin.worker.threadId,
157
+ producerEpoch: current.producerEpoch,
158
+ expectedTailSequence: current.tailSequence,
159
+ expectedTailDigest: current.tailDigest,
160
+ batch: CanonicalBatch.make({
161
+ batchId: Schema.decodeSync(BatchId)(id),
162
+ producerId: config.producerId,
163
+ records: [
164
+ RecordEnvelope.make({
165
+ recordId: id,
166
+ family: "thread",
167
+ schemaVersion: 1,
168
+ createdAt: DateTime.makeUnsafe(origin.createdAtMillis),
169
+ deploymentId: config.deploymentId,
170
+ payload: WorkerOriginRecorded.make({ origin }),
171
+ }),
172
+ ],
173
+ }),
174
+ }),
175
+ )
176
+ .pipe(
177
+ Effect.as(true),
178
+ Effect.catchTag(["AppendConflict", "FenceRejected"], () => Effect.succeed(false)),
179
+ );
180
+
181
+ if (appended) {
182
+ yield* failpoint.hit("worker:after-origin-append");
183
+
184
+ return;
185
+ }
186
+ }
187
+
188
+ return yield* WorkerError.make({ operation: "start", reason: "storage" });
189
+ },
190
+ Effect.mapError((cause) =>
191
+ cause._tag === "WorkerError"
192
+ ? cause
193
+ : WorkerError.make({ operation: "start", reason: "storage", cause }),
194
+ ),
195
+ );