effect-agent 0.1.0-beta.144 → 0.1.0-beta.149

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 (83) hide show
  1. package/dist/{Admin-CQMppCcy.d.mts → Admin-B8ISZyYf.d.mts} +37 -22
  2. package/dist/Agent-1DMSO_Fl.mjs.map +1 -1
  3. package/dist/{Agent-BWhD4Dvu.d.mts → Agent-Cjd_ZveM.d.mts} +7 -3
  4. package/dist/{AgentRuntime-xeCUXutN.mjs → AgentRuntime-D8U9gl9Q.mjs} +23 -18
  5. package/dist/AgentRuntime-D8U9gl9Q.mjs.map +1 -0
  6. package/dist/{AgentRuntime-B4kIzmSF.d.mts → AgentRuntime-sXpKcyxu.d.mts} +5 -5
  7. package/dist/{DurableAgentRuntime-D8GI66mP.mjs → DurableAgentRuntime-DLa4IwFi.mjs} +87 -49
  8. package/dist/DurableAgentRuntime-DLa4IwFi.mjs.map +1 -0
  9. package/dist/{EventSource-w9PonN9X.d.mts → EventSource-CkN05NT3.d.mts} +2 -2
  10. package/dist/{Messaging-Dnmk31yq.d.mts → Messaging-ccQURKCf.d.mts} +3 -3
  11. package/dist/{MessagingHost-CajpPDPG.d.mts → MessagingHost-D04wE-7W.d.mts} +2 -2
  12. package/dist/{Output-DhtWbBcw.d.mts → Output-CFD1CPfU.d.mts} +2 -2
  13. package/dist/{PreparedInputAdmission-BfcLy_k4.d.mts → PreparedInputAdmission-fctZOTnn.d.mts} +3 -3
  14. package/dist/{RunOptions-IlEXLG5F.d.mts → RunOptions-Bv9NpkPZ.d.mts} +10 -2
  15. package/dist/{Subagent-Cl8D1mFM.d.mts → Subagent-VHrANXFy.d.mts} +5 -5
  16. package/dist/{Subagent-8RzJkqJ9.mjs → Subagent-hG9lJ7tJ.mjs} +2 -2
  17. package/dist/{Subagent-8RzJkqJ9.mjs.map → Subagent-hG9lJ7tJ.mjs.map} +1 -1
  18. package/dist/{SubagentHost-B6f4PBC2.d.mts → SubagentHost-C-4A7tA2.d.mts} +2 -2
  19. package/dist/{Subscription-ClkC4_T0.d.mts → Subscription-B16A-48F.d.mts} +2 -2
  20. package/dist/{SubscriptionInput-CMjEKL1z.d.mts → SubscriptionInput-DIU7CiXE.d.mts} +2 -2
  21. package/dist/capabilities/Commands.d.mts +1 -1
  22. package/dist/capabilities/Messaging.d.mts +1 -1
  23. package/dist/capabilities/RunHooks.d.mts +1 -1
  24. package/dist/capabilities/Subagent.d.mts +1 -1
  25. package/dist/capabilities/Subagent.mjs +1 -1
  26. package/dist/core/Agent.d.mts +1 -1
  27. package/dist/durable/Admin.d.mts +1 -1
  28. package/dist/durable/AgentRegistration.d.mts +1 -1
  29. package/dist/durable/Certification.d.mts +1 -1
  30. package/dist/durable/CommittedActivity.d.mts +1 -1
  31. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  32. package/dist/durable/DurableAgentRuntime.mjs +1 -1
  33. package/dist/durable/EventSource.d.mts +1 -1
  34. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  35. package/dist/durable/LifecyclePublication.d.mts +2 -2
  36. package/dist/durable/LifecyclePublication.mjs +32 -12
  37. package/dist/durable/LifecyclePublication.mjs.map +1 -1
  38. package/dist/durable/MessageDelivery.d.mts +3 -3
  39. package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
  40. package/dist/durable/PersistentHistory.d.mts +1 -1
  41. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  42. package/dist/durable/Recovery.d.mts +1 -1
  43. package/dist/durable/RunJournal.d.mts +1 -1
  44. package/dist/durable/Scheduling.d.mts +1 -1
  45. package/dist/durable/SubmissionLedger.d.mts +1 -1
  46. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  47. package/dist/durable/SubmissionStatus.d.mts +1 -1
  48. package/dist/durable/Subscription.d.mts +1 -1
  49. package/dist/durable/SubscriptionInput.d.mts +1 -1
  50. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  51. package/dist/durable/SubscriptionTools.d.mts +2 -2
  52. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  53. package/dist/durable/Subscriptions.d.mts +5 -5
  54. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  55. package/dist/durable/ThreadInvariants.d.mts +1 -1
  56. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  57. package/dist/durable/ThreadStore.d.mts +1 -1
  58. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  59. package/dist/durable/WorkerHost.d.mts +25 -5
  60. package/dist/durable/WorkerHost.mjs.map +1 -1
  61. package/dist/engine/AgentRuntime.d.mts +1 -1
  62. package/dist/engine/AgentRuntime.mjs +1 -1
  63. package/dist/engine/MessagingHost.d.mts +1 -1
  64. package/dist/engine/Output.d.mts +1 -1
  65. package/dist/engine/Output.mjs +1 -1
  66. package/dist/engine/RunOptions.d.mts +1 -1
  67. package/dist/engine/RunOptions.mjs.map +1 -1
  68. package/dist/engine/SubagentHost.d.mts +1 -1
  69. package/dist/index.d.mts +13 -13
  70. package/dist/index.mjs +3 -3
  71. package/dist/{output-contract-CANJJGA5.mjs → output-contract-DS-S8mTv.mjs} +46 -10
  72. package/dist/{output-contract-CANJJGA5.mjs.map → output-contract-DS-S8mTv.mjs.map} +1 -1
  73. package/package.json +1 -1
  74. package/src/core/Agent.ts +6 -2
  75. package/src/durable/DurableAgentRuntime.ts +86 -78
  76. package/src/durable/LifecyclePublication.ts +79 -25
  77. package/src/durable/WorkerHost.ts +23 -1
  78. package/src/durable/internal/worker-host.ts +169 -17
  79. package/src/engine/RunOptions.ts +8 -0
  80. package/src/engine/internal/agent-runtime.ts +80 -18
  81. package/src/engine/internal/output-contract.ts +67 -8
  82. package/dist/AgentRuntime-xeCUXutN.mjs.map +0 -1
  83. package/dist/DurableAgentRuntime-D8GI66mP.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Clock, Context, Crypto, Effect, Layer, Option, Schema, type Scope } from "effect";
1
+ import { Cause, Clock, Context, Crypto, Effect, Layer, Option, Schema, type Scope } from "effect";
2
2
 
3
3
  import { SubmissionId, ThreadId } from "../core/Identifiers.ts";
4
4
  import { Receipt } from "../core/Receipt.ts";
@@ -122,37 +122,70 @@ export class LifecyclePublicationError extends Schema.TaggedError<LifecyclePubli
122
122
  },
123
123
  ) {}
124
124
 
125
- /** Native recovery state, not a second application record or launch queue. */
125
+ /** Bound both retained facts and the immutable admission evidence resolved before dispatch. */
126
+ export const lifecyclePublicationBatchMaxFacts = 8;
127
+
128
+ /** One owner's bounded pending prefix, strictly ordered by its durable ordinals. */
129
+ export const LifecyclePublicationBatch = Schema.NonEmptyArray(LifecyclePublication).check(
130
+ Schema.isMaxLength(lifecyclePublicationBatchMaxFacts),
131
+ Schema.makeFilter(
132
+ (facts) =>
133
+ facts.every((fact, index) => {
134
+ const previous = facts[index - 1];
135
+
136
+ return (
137
+ fact.ownerThreadId === facts[0].ownerThreadId &&
138
+ (previous === undefined || fact.ordinal > previous.ordinal)
139
+ );
140
+ }),
141
+ { expected: "One owner with strictly increasing publication ordinals" },
142
+ ),
143
+ );
144
+
145
+ export type LifecyclePublicationBatch = typeof LifecyclePublicationBatch.Type;
146
+
147
+ /** Native recovery state, independent of Attempts. Hosts serialize drains per storage owner. */
126
148
  export interface LifecyclePublicationStorage {
149
+ /** Select up to `limit` due owners, with a bounded pending prefix for each selected owner. */
127
150
  readonly pending: (
128
151
  nowMillis: number,
129
152
  limit: number,
130
- ) => Effect.Effect<ReadonlyArray<LifecyclePublication>, LifecyclePublicationError>;
153
+ ) => Effect.Effect<ReadonlyArray<LifecyclePublicationBatch>, LifecyclePublicationError>;
154
+ /** Atomically acknowledge the exact batch, retaining its identities and fingerprints. */
131
155
  readonly acknowledge: (
132
- publication: LifecyclePublication,
133
- ) => Effect.Effect<void, LifecyclePublicationError>;
134
- readonly defer: (
135
- publication: LifecyclePublication,
136
- untilMillis: number,
156
+ publications: LifecyclePublicationBatch,
137
157
  ) => Effect.Effect<void, LifecyclePublicationError>;
158
+ /**
159
+ * Claim a due owner batch and persist its retry before dispatch. Eight automatic attempts;
160
+ * 1s exponential backoff capped at 60s, after the dispatch timeout. The final attempt parks
161
+ * the payload before dispatch so process loss cannot renew its budget. False means not due.
162
+ */
163
+ readonly claim: (
164
+ publications: LifecyclePublicationBatch,
165
+ nowMillis: number,
166
+ timeoutMillis: number,
167
+ ) => Effect.Effect<boolean, LifecyclePublicationError>;
138
168
  readonly pendingDeadline: Effect.Effect<Option.Option<number>, LifecyclePublicationError>;
139
- /** Earliest retained owner obligation, including deferred facts that still gate execution. */
140
- readonly pendingDeadlineFor: (
169
+ /** Explicit operator retry after repairing a parked owner's destination. */
170
+ readonly retryParked: (
141
171
  ownerThreadId: ThreadId,
142
- ) => Effect.Effect<Option.Option<number>, LifecyclePublicationError>;
172
+ nowMillis: number,
173
+ ) => Effect.Effect<void, LifecyclePublicationError>;
143
174
  }
144
175
 
145
176
  /**
146
- * Publish an idempotent domain command, including its authorization, exact receipt and delivery
147
- * intent. Return only after that command commits. Treat private input/update/result fields as
148
- * private; the application must select its declared public fields. Revocation/deletion is an
149
- * acknowledged domain decision, not an indefinitely retryable infrastructure failure.
177
+ * Publish one owner's ordered batch in one idempotent host transaction, including authorization,
178
+ * records, receipts and delivery intents. Larger backlogs continue in later batches.
179
+ * Return only after the entire batch commits. Retries
180
+ * may include already committed identities plus later facts; deduplicate each fact's `id`.
181
+ * Private input/update/result fields remain private; select declared public fields explicitly.
182
+ * Revocation/deletion is an acknowledged domain decision, not an infrastructure retry.
150
183
  */
151
184
  export class LifecyclePublicationHandler extends Context.Service<
152
185
  LifecyclePublicationHandler,
153
186
  {
154
187
  readonly publish: (
155
- publication: LifecyclePublication,
188
+ publications: LifecyclePublicationBatch,
156
189
  ) => Effect.Effect<void, LifecyclePublicationError, Scope.Scope>;
157
190
  }
158
191
  >()("@effect-agent/thread/LifecyclePublicationHandler") {}
@@ -218,7 +251,8 @@ const withSource = Effect.fn("LifecyclePublication.withSource")(
218
251
 
219
252
  /**
220
253
  * One finite wave for the host's existing maintenance coordinator. Persist the next deadline
221
- * before dispatch, so interruption and process loss retain the same fact without another timer.
254
+ * before dispatch, so interruption and process loss retain the same facts without another timer.
255
+ * Finish independent owner batches before surfacing failures; interruption stops the wave.
222
256
  * Acknowledgements are exact and idempotent. No producer or external Tool is re-executed here.
223
257
  */
224
258
  export const drainLifecyclePublications = Effect.fn("LifecyclePublication.drain")(function* (
@@ -229,16 +263,36 @@ export const drainLifecyclePublications = Effect.fn("LifecyclePublication.drain"
229
263
  const handler = yield* LifecyclePublicationHandler;
230
264
  const pending = yield* storage.pending(yield* Clock.currentTimeMillis, limit);
231
265
 
232
- for (const publication of pending) {
233
- yield* storage.defer(publication, (yield* Clock.currentTimeMillis) + timeoutMillis);
234
- yield* Effect.scoped(withSource(publication).pipe(Effect.flatMap(handler.publish))).pipe(
235
- Effect.timeoutOrElse({
236
- duration: timeoutMillis,
237
- orElse: () => LifecyclePublicationError.make({ reason: "unavailable" }),
238
- }),
266
+ let failures: Cause.Cause<LifecyclePublicationError> = Cause.empty;
267
+
268
+ for (const batch of pending) {
269
+ yield* Effect.gen(function* () {
270
+ if (!(yield* storage.claim(batch, yield* Clock.currentTimeMillis, timeoutMillis))) return;
271
+ yield* Effect.scoped(
272
+ Effect.gen(function* () {
273
+ const publications = yield* Effect.forEach(batch, withSource);
274
+
275
+ yield* handler.publish(publications);
276
+ }),
277
+ ).pipe(
278
+ Effect.timeoutOrElse({
279
+ duration: timeoutMillis,
280
+ orElse: () => LifecyclePublicationError.make({ reason: "unavailable" }),
281
+ }),
282
+ );
283
+ yield* storage.acknowledge(batch);
284
+ }).pipe(
285
+ Effect.catchCauseIf(
286
+ (cause) => !Cause.hasInterrupts(cause),
287
+ (cause) => {
288
+ failures = Cause.combine(failures, cause);
289
+
290
+ return Effect.void;
291
+ },
292
+ ),
239
293
  );
240
- yield* storage.acknowledge(publication);
241
294
  }
295
+ if (failures.reasons.length > 0) return yield* Effect.failCause(failures);
242
296
 
243
297
  return pending.length;
244
298
  });
@@ -2,10 +2,11 @@ import { Context, Effect, Option, Schema } from "effect";
2
2
 
3
3
  import type * as Agent from "../core/Agent.ts";
4
4
  import type { AgentPolicy } from "../core/AgentPolicy.ts";
5
- import type { SubmissionId, ThreadId } from "../core/Identifiers.ts";
5
+ import type { AgentId, SubmissionId, ThreadId } from "../core/Identifiers.ts";
6
6
  import type { SubagentBudgetReservation } from "../core/SubagentContract.ts";
7
7
  import { WorkerError, type WorkerRef, type WorkerSource } from "../core/Worker.ts";
8
8
  import type {
9
+ CanonicalSequence,
9
10
  DefinitionDigests,
10
11
  Digest,
11
12
  PersistedJson,
@@ -14,6 +15,24 @@ import type {
14
15
  } from "./Records.ts";
15
16
  import type { Principal, SubmissionSnapshot } from "./SubmissionLedger.ts";
16
17
 
18
+ /**
19
+ * One successful start authorization at an exact source tail. This host-created hint is
20
+ * available only during that start's preparation and synchronous admission; it is never
21
+ * persisted or transported. Hooks may reuse their admission decision, but must retain
22
+ * current access checks at commit boundaries. A retry or changed tail gets no old grant.
23
+ */
24
+ export interface WorkerStartAdmission {
25
+ readonly sourceThreadId: ThreadId;
26
+ readonly sourceSubmissionId?: SubmissionId;
27
+ readonly requestedPrincipal: Principal;
28
+ readonly principal: Principal;
29
+ readonly targetAgentId: AgentId;
30
+ readonly continuationOf?: WorkerContinuation;
31
+ readonly access: "send";
32
+ readonly tailSequence: CanonicalSequence;
33
+ readonly tailDigest: Digest;
34
+ }
35
+
17
36
  /** Host-verified source input; absence never means the latest input in the Thread. */
18
37
  export interface WorkerPolicySource {
19
38
  readonly threadId: ThreadId;
@@ -30,6 +49,7 @@ export type WorkerPolicyTarget = {
30
49
  } & (
31
50
  | {
32
51
  readonly _tag: "InitialInput";
52
+ readonly admission?: WorkerStartAdmission;
33
53
  readonly continuationOf?: WorkerContinuation;
34
54
  readonly sourceSubmission?: SubmissionSnapshot;
35
55
  readonly input: PersistedJson;
@@ -136,6 +156,7 @@ export const WorkerHostConfig = Context.Reference<WorkerHostLimits>(
136
156
  * identifiers alone never confer any of them.
137
157
  */
138
158
  export interface WorkerHostAuthorizationRequest {
159
+ readonly admission?: WorkerStartAdmission;
139
160
  readonly sourceThreadId: ThreadId;
140
161
  /** An explicitly selected owner input; authorize this locator as well as the Thread. */
141
162
  readonly sourceSubmissionId?: SubmissionId;
@@ -170,6 +191,7 @@ export const WorkerHostAuthorizer = Context.Reference<{
170
191
  export const WorkerBudgetAuthorizer = Context.Reference<{
171
192
  readonly authorize: (request: {
172
193
  readonly source: WorkerSource;
194
+ readonly admission?: WorkerStartAdmission;
173
195
  readonly principal: Principal;
174
196
  readonly worker: WorkerRef;
175
197
  readonly continuationOf?: WorkerContinuation;
@@ -128,6 +128,7 @@ import {
128
128
  WorkerHostConfig,
129
129
  WorkerPolicyResolver,
130
130
  type WorkerPolicyTarget,
131
+ type WorkerStartAdmission,
131
132
  } from "../WorkerHost.ts";
132
133
  import {
133
134
  definitionDigestsEqual,
@@ -263,6 +264,20 @@ export class WorkerInputControl extends Context.Service<
263
264
  }
264
265
  >()("@effect-agent/thread/internal/WorkerInputControl") {}
265
266
 
267
+ const CurrentStartPreparation = Context.Reference<WorkerStartAdmission | undefined>(
268
+ "@effect-agent/thread/internal/CurrentStartPreparation",
269
+ { defaultValue: () => undefined },
270
+ );
271
+
272
+ const CurrentStartDelivery = Context.Reference<
273
+ | {
274
+ readonly authorization: WorkerStartAdmission;
275
+ readonly admission: WorkerAdmission;
276
+ readonly inputDigest: Digest;
277
+ }
278
+ | undefined
279
+ >("@effect-agent/thread/internal/CurrentStartDelivery", { defaultValue: () => undefined });
280
+
266
281
  /** No in-memory ownership: every mutation is reserved in source canonical history with CAS. */
267
282
  export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
268
283
  options: WorkerRuntimeOptions,
@@ -290,9 +305,40 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
290
305
  const withCrypto = <A, E>(effect: Effect.Effect<A, E, Crypto.Crypto>) =>
291
306
  Effect.provideService(effect, Crypto.Crypto, deps.crypto);
292
307
 
308
+ const currentStart = Effect.fnUntraced(function* (
309
+ admission: WorkerStartAdmission | undefined,
310
+ sourceThreadId: ThreadId,
311
+ sourceSubmissionId: SubmissionId | undefined,
312
+ principal: Principal,
313
+ targetAgentId: AgentId,
314
+ continuationOf: WorkerContinuation | undefined,
315
+ ) {
316
+ if (
317
+ admission === undefined ||
318
+ admission.sourceThreadId !== sourceThreadId ||
319
+ admission.sourceSubmissionId !== sourceSubmissionId ||
320
+ (admission.principal !== principal && admission.requestedPrincipal !== principal) ||
321
+ admission.targetAgentId !== targetAgentId ||
322
+ !Schema.toEquivalence(Schema.UndefinedOr(WorkerContinuation))(
323
+ admission.continuationOf,
324
+ continuationOf,
325
+ )
326
+ )
327
+ return undefined;
328
+
329
+ const tail = yield* deps.store
330
+ .inspectTail(ThreadTailRequest.make({ threadId: sourceThreadId }))
331
+ .pipe(Effect.mapError(storageFailure("start")));
332
+
333
+ return tail.tailSequence === admission.tailSequence && tail.tailDigest === admission.tailDigest
334
+ ? admission
335
+ : undefined;
336
+ });
337
+
293
338
  const authorizeBudget = Effect.fn("WorkerHost.authorizeBudget")(function* (
294
339
  origin: WorkerOrigin,
295
340
  principal: Principal,
341
+ admission?: WorkerStartAdmission,
296
342
  ) {
297
343
  yield* verifyContinuation(origin);
298
344
  if (origin.budgetScope !== "worker-run") return;
@@ -300,6 +346,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
300
346
  if (origin.depth !== 1) return yield* failure("start", "denied");
301
347
 
302
348
  yield* deps.budgetAuthorizer.authorize({
349
+ ...(admission === undefined ? {} : { admission }),
303
350
  source: origin.source,
304
351
  principal,
305
352
  worker: origin.worker,
@@ -1049,6 +1096,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1049
1096
  input: PersistedJson,
1050
1097
  principal: Principal,
1051
1098
  retainedFrameworkInput: boolean,
1099
+ authorization?: WorkerStartAdmission,
1052
1100
  ): Effect.fn.Return<void, WorkerError> {
1053
1101
  const origin = admission.origin;
1054
1102
 
@@ -1133,6 +1181,11 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1133
1181
  ? {
1134
1182
  ...targetRequest,
1135
1183
  _tag: "InitialInput",
1184
+ ...(authorization === undefined ||
1185
+ authorization.tailSequence !== current.tailSequence ||
1186
+ authorization.tailDigest !== current.tailDigest
1187
+ ? {}
1188
+ : { admission: authorization }),
1136
1189
  ...(origin.continuationOf === undefined
1137
1190
  ? {}
1138
1191
  : { continuationOf: origin.continuationOf }),
@@ -1351,7 +1404,24 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1351
1404
  }
1352
1405
 
1353
1406
  yield* verifyContinuation(admission.origin);
1407
+ const delivery = yield* CurrentStartDelivery;
1408
+
1409
+ const authorization =
1410
+ delivery !== undefined &&
1411
+ sameAdmission(delivery.admission, admission) &&
1412
+ delivery.inputDigest === inputDigest
1413
+ ? yield* currentStart(
1414
+ delivery.authorization,
1415
+ admission.origin.source.threadId,
1416
+ admission.sourceSubmissionId,
1417
+ options.principal,
1418
+ agentId,
1419
+ admission.origin.continuationOf,
1420
+ )
1421
+ : undefined;
1422
+
1354
1423
  yield* deps.authorizer.authorize({
1424
+ ...(authorization === undefined ? {} : { admission: authorization }),
1355
1425
  sourceThreadId: admission.origin.source.threadId,
1356
1426
  ...(admission.sourceSubmissionId === undefined
1357
1427
  ? {}
@@ -1364,7 +1434,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1364
1434
  ? {}
1365
1435
  : { continuationOf: admission.origin.continuationOf }),
1366
1436
  });
1367
- yield* authorizeBudget(admission.origin, options.principal);
1437
+ yield* authorizeBudget(admission.origin, options.principal, authorization);
1368
1438
  if (
1369
1439
  admission.origin.worker.threadId !== options.threadId ||
1370
1440
  admission.origin.worker.targetAgentId !== agentId ||
@@ -1378,7 +1448,14 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1378
1448
 
1379
1449
  if (retainedFrameworkInput && admission.reportKind !== "update")
1380
1450
  yield* validateCompletion(options.messageAdmission, options, agentId, inputDigest);
1381
- yield* reservation(admission, inputDigest, input, options.principal, retainedFrameworkInput);
1451
+ yield* reservation(
1452
+ admission,
1453
+ inputDigest,
1454
+ input,
1455
+ options.principal,
1456
+ retainedFrameworkInput,
1457
+ authorization,
1458
+ );
1382
1459
 
1383
1460
  return admission;
1384
1461
  });
@@ -1866,13 +1943,17 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1866
1943
  principal: Principal,
1867
1944
  sourceSubmissionId?: SubmissionId,
1868
1945
  ): SubagentHost["Service"] => {
1946
+ const requestedPrincipal = principal;
1947
+
1869
1948
  const authorize = (
1870
1949
  operation: WorkerError["operation"],
1871
1950
  access: "context" | "read" | "send" | "control",
1872
1951
  worker?: WorkerRef,
1873
1952
  continuationOf?: WorkerContinuation,
1953
+ admission?: WorkerStartAdmission,
1874
1954
  ) =>
1875
1955
  deps.authorizer.authorize({
1956
+ ...(admission === undefined ? {} : { admission }),
1876
1957
  sourceThreadId: context.source.threadId,
1877
1958
  ...(sourceSubmissionId === undefined ? {} : { sourceSubmissionId }),
1878
1959
  principal,
@@ -1894,6 +1975,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1894
1975
  target: Agent.AnyDefinition,
1895
1976
  encodedInput: unknown,
1896
1977
  continuationOf?: WorkerContinuation,
1978
+ authorization?: WorkerStartAdmission,
1897
1979
  ) {
1898
1980
  const resolved = yield* binding(target, "start");
1899
1981
 
@@ -1908,8 +1990,18 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1908
1990
 
1909
1991
  const source = yield* sourceAuthority(context.source.threadId, sourceSubmissionId);
1910
1992
 
1993
+ const admission = yield* currentStart(
1994
+ authorization,
1995
+ context.source.threadId,
1996
+ sourceSubmissionId,
1997
+ principal,
1998
+ target.id,
1999
+ continuationOf,
2000
+ );
2001
+
1911
2002
  const policy = yield* resolveTargetPolicy({
1912
2003
  _tag: "InitialInput",
2004
+ ...(admission === undefined ? {} : { admission }),
1913
2005
  ...(continuationOf === undefined ? {} : { continuationOf }),
1914
2006
  definition: resolved.definition,
1915
2007
  definitions: resolved.digests,
@@ -2064,6 +2156,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2064
2156
  encodedParameters: unknown,
2065
2157
  principal: Principal,
2066
2158
  operation: "start" | "followUp",
2159
+ authorization?: WorkerStartAdmission,
2067
2160
  ) {
2068
2161
  if (Option.isNone(deps.deliveries)) return yield* failure(operation, "unavailable");
2069
2162
  const deliveries = deps.deliveries.value;
@@ -2146,17 +2239,25 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2146
2239
  Effect.provideService(MessageDeliveryStore, deliveries),
2147
2240
  Effect.provideService(PreparedInputAdmission, {
2148
2241
  submit: (prepared) =>
2149
- deps
2150
- .submit(prepared)
2151
- .pipe(
2152
- Effect.mapError((error) =>
2153
- error._tag === "AdmissionConflict" || error._tag === "AgentInputError"
2154
- ? ScheduledInputRefused.make({ code: error._tag, cause: error })
2155
- : error._tag === "AdmissionPolicyError" && error.reason === "refused"
2156
- ? ScheduledInputRefused.make({ code: error.code, cause: error })
2157
- : ScheduledInputRetryable.make({ reason: "storage", cause: error }),
2158
- ),
2242
+ deps.submit(prepared).pipe(
2243
+ Effect.provideService(
2244
+ CurrentStartDelivery,
2245
+ authorization !== undefined &&
2246
+ prepared.workerAdmission !== undefined &&
2247
+ prepared.admissionKey === messageId &&
2248
+ prepared.inputDigest === inputDigest &&
2249
+ sameOrigin(prepared.workerAdmission.origin, origin)
2250
+ ? { authorization, admission: prepared.workerAdmission, inputDigest }
2251
+ : undefined,
2159
2252
  ),
2253
+ Effect.mapError((error) =>
2254
+ error._tag === "AdmissionConflict" || error._tag === "AgentInputError"
2255
+ ? ScheduledInputRefused.make({ code: error._tag, cause: error })
2256
+ : error._tag === "AdmissionPolicyError" && error.reason === "refused"
2257
+ ? ScheduledInputRefused.make({ code: error.code, cause: error })
2258
+ : ScheduledInputRetryable.make({ reason: "storage", cause: error }),
2259
+ ),
2260
+ ),
2160
2261
  submissionStatus: (receipt) =>
2161
2262
  deps
2162
2263
  .status(receipt)
@@ -2393,10 +2494,23 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2393
2494
  request.continuationOf.delegationId,
2394
2495
  );
2395
2496
 
2396
- yield* authorize("start", "send", undefined, previous?.evidence);
2497
+ const admission = yield* currentStart(
2498
+ yield* CurrentStartPreparation,
2499
+ context.source.threadId,
2500
+ sourceSubmissionId,
2501
+ principal,
2502
+ request.target.id,
2503
+ previous?.evidence,
2504
+ );
2505
+
2506
+ yield* authorize("start", "send", undefined, previous?.evidence, admission);
2397
2507
 
2398
- return (yield* preparedTarget(request.target, request.encodedInput, previous?.evidence))
2399
- .policy;
2508
+ return (yield* preparedTarget(
2509
+ request.target,
2510
+ request.encodedInput,
2511
+ previous?.evidence,
2512
+ admission,
2513
+ )).policy;
2400
2514
  }),
2401
2515
  start: Effect.fn("WorkerHost.start")(function* <E = never, R = never>(
2402
2516
  command: StartWorkerRequest | DeferredStartWorkerRequest<E, R>,
@@ -2411,8 +2525,26 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2411
2525
  command.delegationId,
2412
2526
  );
2413
2527
 
2528
+ // Bind the decision to the tail before authorization. If it changes while the
2529
+ // authorizer reads current state, currentStart rejects reuse of this decision.
2530
+ const tail = yield* deps.store
2531
+ .inspectTail(ThreadTailRequest.make({ threadId: context.source.threadId }))
2532
+ .pipe(Effect.mapError(storageFailure("start")));
2533
+
2414
2534
  const principal = yield* authorize("start", "send", undefined, previous?.evidence);
2415
2535
 
2536
+ const startAdmission: WorkerStartAdmission = {
2537
+ sourceThreadId: context.source.threadId,
2538
+ ...(sourceSubmissionId === undefined ? {} : { sourceSubmissionId }),
2539
+ requestedPrincipal,
2540
+ principal,
2541
+ targetAgentId: command.target.id,
2542
+ ...(previous === undefined ? {} : { continuationOf: previous.evidence }),
2543
+ access: "send",
2544
+ tailSequence: tail.tailSequence,
2545
+ tailDigest: tail.tailDigest,
2546
+ };
2547
+
2416
2548
  if (context.depth !== 0 && sourceSubmissionId === undefined)
2417
2549
  return yield* failure("start", "denied");
2418
2550
 
@@ -2507,7 +2639,14 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2507
2639
 
2508
2640
  return yield* Effect.gen(function* () {
2509
2641
  const request =
2510
- "prepare" in command ? { ...(yield* command.prepare), ...command } : command;
2642
+ "prepare" in command
2643
+ ? {
2644
+ ...(yield* command.prepare.pipe(
2645
+ Effect.provideService(CurrentStartPreparation, startAdmission),
2646
+ )),
2647
+ ...command,
2648
+ }
2649
+ : command;
2511
2650
 
2512
2651
  const currentPrevious =
2513
2652
  command.continuationOf === undefined
@@ -2535,6 +2674,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2535
2674
  request.target,
2536
2675
  request.encodedInput,
2537
2676
  currentPrevious?.evidence,
2677
+ startAdmission,
2538
2678
  );
2539
2679
 
2540
2680
  const resolved = prepared.resolved;
@@ -2596,7 +2736,18 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2596
2736
  "start",
2597
2737
  );
2598
2738
 
2599
- yield* authorizeBudget(origin, principal);
2739
+ yield* authorizeBudget(
2740
+ origin,
2741
+ principal,
2742
+ yield* currentStart(
2743
+ startAdmission,
2744
+ context.source.threadId,
2745
+ sourceSubmissionId,
2746
+ principal,
2747
+ request.target.id,
2748
+ currentPrevious?.evidence,
2749
+ ),
2750
+ );
2600
2751
 
2601
2752
  const targetPolicy = Option.isSome(prepared.policy)
2602
2753
  ? prepared.policy.value
@@ -2623,6 +2774,7 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
2623
2774
  request.encodedParameters,
2624
2775
  principal,
2625
2776
  "start",
2777
+ startAdmission,
2626
2778
  );
2627
2779
 
2628
2780
  return { worker: origin.worker, delivery };
@@ -210,6 +210,14 @@ export interface RunContextRequest {
210
210
  * identities rather than infer durable state from this Prompt alone.
211
211
  */
212
212
  readonly source: Prompt.Prompt;
213
+ /**
214
+ * Durable coordinator's exact prior-Run prefix length in `source`. A preparation hook
215
+ * can replace `source.content.slice(0, priorRunPrefixLength)` with application context
216
+ * while preserving this Run's instructions, input, updates and recovered Tool exchanges.
217
+ * This is a model-only transformation: canonical records and retry receipts are retained.
218
+ * Absent for non-durable Runs; never infer this boundary from message roles or text.
219
+ */
220
+ readonly priorRunPrefixLength?: number | undefined;
213
221
  /**
214
222
  * The exact model-visible final-output contract the engine appends to the
215
223
  * prepared prompt after this hook returns (RUN-028), or