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.
Files changed (55) hide show
  1. package/dist/{Admin-C34cVKlT.d.mts → Admin-CUja0lmO.d.mts} +21 -3
  2. package/dist/{DurableAgentRuntime-BydyN_n-.mjs → DurableAgentRuntime-DH2baLGe.mjs} +13 -21
  3. package/dist/DurableAgentRuntime-DH2baLGe.mjs.map +1 -0
  4. package/dist/{EventSource-Bac0SxKL.d.mts → EventSource-DoiZF3xv.d.mts} +2 -2
  5. package/dist/{PreparedInputAdmission-DcQGQ0Yp.d.mts → PreparedInputAdmission-B1bzyIuV.d.mts} +3 -3
  6. package/dist/{Subscription-Bk2Smmqc.d.mts → Subscription-m_yNfNeN.d.mts} +2 -2
  7. package/dist/{SubscriptionInput-ekbJz3pF.d.mts → SubscriptionInput-ON3-b8Sl.d.mts} +2 -2
  8. package/dist/durable/Admin.d.mts +1 -1
  9. package/dist/durable/AgentRegistration.d.mts +1 -1
  10. package/dist/durable/Certification.d.mts +1 -1
  11. package/dist/durable/CommittedActivity.d.mts +1 -1
  12. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  13. package/dist/durable/DurableAgentRuntime.mjs +1 -1
  14. package/dist/durable/EventSource.d.mts +1 -1
  15. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  16. package/dist/durable/MessageDelivery.d.mts +41 -41
  17. package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
  18. package/dist/durable/PersistentHistory.d.mts +1 -1
  19. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  20. package/dist/durable/Recovery.d.mts +1 -1
  21. package/dist/durable/RunJournal.d.mts +1 -1
  22. package/dist/durable/Scheduling.d.mts +1 -1
  23. package/dist/durable/SqlStorageV2Upgrade.d.mts +1 -1
  24. package/dist/durable/SqlSubscriptionStore.d.mts +1 -1
  25. package/dist/durable/SqlThreadNativeReads.d.mts +2 -1
  26. package/dist/durable/SqlThreadNativeReads.mjs +75 -38
  27. package/dist/durable/SqlThreadNativeReads.mjs.map +1 -1
  28. package/dist/durable/SubmissionLedger.d.mts +1 -1
  29. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  30. package/dist/durable/SubmissionStatus.d.mts +1 -1
  31. package/dist/durable/Subscription.d.mts +1 -1
  32. package/dist/durable/SubscriptionInput.d.mts +1 -1
  33. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  34. package/dist/durable/SubscriptionTools.d.mts +2 -2
  35. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  36. package/dist/durable/Subscriptions.d.mts +5 -5
  37. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  38. package/dist/durable/ThreadInvariants.d.mts +1 -1
  39. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  40. package/dist/durable/ThreadStore.d.mts +2 -2
  41. package/dist/durable/ThreadStore.mjs +30 -2
  42. package/dist/durable/ThreadStore.mjs.map +1 -1
  43. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  44. package/dist/durable/ThreadStoreConformance.mjs +34 -3
  45. package/dist/durable/ThreadStoreConformance.mjs.map +1 -1
  46. package/dist/durable/WorkerHost.d.mts +1 -1
  47. package/dist/index.d.mts +5 -5
  48. package/dist/index.mjs +2 -2
  49. package/package.json +1 -1
  50. package/src/durable/SqlThreadNativeReads.ts +75 -2
  51. package/src/durable/ThreadStore.ts +63 -1
  52. package/src/durable/ThreadStoreConformance.ts +113 -1
  53. package/src/durable/internal/agent-updates.ts +4 -0
  54. package/src/durable/internal/worker-host.ts +17 -18
  55. 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 { runIdForSubmission, toolCallPreparedRecordId } from "./RunJournal.ts";
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("inspect-record-1", "first")]),
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
- const tail = yield* deps.store
496
- .inspectTail(ThreadTailRequest.make({ threadId }))
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
- const tail = yield* deps.store.inspectTail(ThreadTailRequest.make({ threadId }));
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
- // Never append against a tail newer than the prefix whose capacity was checked.
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: tail.producerEpoch,
554
+ producerEpoch,
556
555
  expectedTailSequence: current.tailSequence,
557
556
  expectedTailDigest: current.tailDigest,
558
557
  batch: CanonicalBatch.make({