effect-agent 0.1.0-beta.159 → 0.1.0-beta.160

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 (62) hide show
  1. package/dist/{Admin-DWDYlvvZ.d.mts → Admin-Dirux8PR.d.mts} +9 -2
  2. package/dist/{DurableAgentRuntime-C7A5guvq.mjs → DurableAgentRuntime-pr7cCaZw.mjs} +216 -55
  3. package/dist/DurableAgentRuntime-pr7cCaZw.mjs.map +1 -0
  4. package/dist/{EventSource-BfNZBG4A.d.mts → EventSource-DOXB0RJR.d.mts} +2 -2
  5. package/dist/{PreparedInputAdmission-VStpn8u2.d.mts → PreparedInputAdmission-B7r401xJ.d.mts} +3 -3
  6. package/dist/{RunJournal-ixFRCyW3.mjs → RunJournal-CA2bsvHn.mjs} +8 -5
  7. package/dist/RunJournal-CA2bsvHn.mjs.map +1 -0
  8. package/dist/{Subscription-Bx1ZRRVS.d.mts → Subscription-B3ScCBaN.d.mts} +2 -2
  9. package/dist/{SubscriptionInput-B5k5vb1S.d.mts → SubscriptionInput-BP5WVsX_.d.mts} +2 -2
  10. package/dist/{WorkerAdmission-N9hZV85Q.mjs → WorkerAdmission-Cmcxd6nS.mjs} +2 -2
  11. package/dist/{WorkerAdmission-N9hZV85Q.mjs.map → WorkerAdmission-Cmcxd6nS.mjs.map} +1 -1
  12. package/dist/durable/Admin.d.mts +1 -1
  13. package/dist/durable/AgentRegistration.d.mts +1 -1
  14. package/dist/durable/Certification.d.mts +1 -1
  15. package/dist/durable/CommittedActivity.d.mts +1 -1
  16. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  17. package/dist/durable/DurableAgentRuntime.mjs +2 -2
  18. package/dist/durable/EventSource.d.mts +1 -1
  19. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  20. package/dist/durable/LifecyclePublication.d.mts +1 -1
  21. package/dist/durable/MessageDelivery.d.mts +3 -3
  22. package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
  23. package/dist/durable/PersistentHistory.d.mts +1 -1
  24. package/dist/durable/PersistentHistory.mjs +1 -1
  25. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  26. package/dist/durable/Recovery.d.mts +1 -1
  27. package/dist/durable/RunJournal.d.mts +1 -1
  28. package/dist/durable/RunJournal.mjs +1 -1
  29. package/dist/durable/Scheduling.d.mts +1 -1
  30. package/dist/durable/SqlMemoryStore.d.mts +3 -0
  31. package/dist/durable/SqlMemoryStore.mjs +3 -2
  32. package/dist/durable/SqlMemoryStore.mjs.map +1 -1
  33. package/dist/durable/SubmissionLedger.d.mts +1 -1
  34. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  35. package/dist/durable/SubmissionLedgerConformance.mjs +1 -1
  36. package/dist/durable/SubmissionStatus.d.mts +1 -1
  37. package/dist/durable/Subscription.d.mts +1 -1
  38. package/dist/durable/SubscriptionInput.d.mts +1 -1
  39. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  40. package/dist/durable/SubscriptionTools.d.mts +2 -2
  41. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  42. package/dist/durable/Subscriptions.d.mts +5 -5
  43. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  44. package/dist/durable/ThreadInvariants.d.mts +1 -1
  45. package/dist/durable/ThreadInvariants.mjs +1 -1
  46. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  47. package/dist/durable/ThreadStore.d.mts +1 -1
  48. package/dist/durable/ThreadStore.mjs +1 -1
  49. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  50. package/dist/durable/ThreadStoreConformance.mjs +1 -1
  51. package/dist/durable/WorkerAdmission.d.mts +1 -1
  52. package/dist/durable/WorkerAdmission.mjs +1 -1
  53. package/dist/durable/WorkerHost.d.mts +1 -1
  54. package/dist/index.d.mts +5 -5
  55. package/dist/index.mjs +3 -3
  56. package/package.json +1 -1
  57. package/src/durable/DurableAgentRuntime.ts +371 -84
  58. package/src/durable/RunJournal.ts +16 -4
  59. package/src/durable/SqlMemoryStore.ts +7 -3
  60. package/src/durable/internal/journal-checkpoint.ts +57 -3
  61. package/dist/DurableAgentRuntime-C7A5guvq.mjs.map +0 -1
  62. package/dist/RunJournal-ixFRCyW3.mjs.map +0 -1
@@ -159,8 +159,10 @@ import {
159
159
  JournalCheckpointSeed,
160
160
  RecoveryCheckpointState,
161
161
  RecoveryCheckpointContents,
162
+ ThreadContextCheckpoint,
162
163
  RECOVERY_ENGINE_VERSION,
163
164
  checkpointSuffixCompatible,
165
+ makeThreadContextCertificate,
164
166
  } from "./internal/journal-checkpoint.ts";
165
167
  import { makeJournalMetadata, type JournalMetadata } from "./internal/journal-metadata.ts";
166
168
  import { makeMessagingRuntime } from "./internal/messaging-host.ts";
@@ -359,6 +361,8 @@ import {
359
361
  LoadCheckpointRequest,
360
362
  ThreadCheckpoint,
361
363
  SaveRecoveryCheckpointRequest,
364
+ getRecord,
365
+ getRunInput,
362
366
  } from "./ThreadStore.ts";
363
367
  import { PreparedToolCallEvidence, ToolReconciler } from "./ToolReconciler.ts";
364
368
  import { WakeScheduler } from "./WakeScheduler.ts";
@@ -1277,6 +1281,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1277
1281
  readonly through: CanonicalSequence;
1278
1282
  readonly runId: RunId;
1279
1283
  readonly seedThrough: CanonicalSequence | undefined;
1284
+ readonly contextThrough: CanonicalSequence | undefined;
1280
1285
  readonly journal: RunJournalProjection;
1281
1286
  readonly boundaries: ReadonlyArray<JournalBoundary>;
1282
1287
  }
@@ -1563,54 +1568,41 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1563
1568
  );
1564
1569
  });
1565
1570
 
1566
- /** A checkpoint is useful only for its explicitly retained submissions and exact definitions. */
1567
- const recoveryView = Effect.fn("DurableAgentRuntime.recoveryView")(function* (
1571
+ const loadRecoveryCheckpoint = Effect.fn("DurableAgentRuntime.loadRecoveryCheckpoint")(function* (
1568
1572
  threadId: ThreadId,
1569
1573
  throughSequence: CanonicalSequence,
1570
- submissionIds: ReadonlyArray<SubmissionId>,
1571
- journalOwner?: RunId,
1572
- ): Effect.fn.Return<
1573
- {
1574
- readonly canonical: Stream.Stream<
1575
- CanonicalRecordEnvelope,
1576
- ThreadStoreError | ThreadNotMaterialized
1577
- >;
1578
- readonly seed?: JournalCheckpointSeed;
1579
- },
1580
- ThreadStoreError | ThreadNotMaterialized
1581
- > {
1582
- const full = { canonical: canonicalRange(threadId, throughSequence) };
1583
-
1584
- if (store.recoveryCheckpoints === undefined) return full;
1574
+ ) {
1575
+ if (store.recoveryCheckpoints === undefined) return Option.none();
1585
1576
 
1586
1577
  const loaded = yield* store.recoveryCheckpoints
1587
1578
  .load(LoadCheckpointRequest.make({ threadId, atOrBeforeSequence: throughSequence }))
1588
1579
  .pipe(Effect.catchTag("CheckpointRejected", () => Effect.succeed(Option.none())));
1589
1580
 
1590
- if (Option.isNone(loaded)) return full;
1581
+ if (Option.isNone(loaded)) return Option.none();
1591
1582
  const checkpoint = loaded.value;
1592
1583
 
1593
1584
  if (
1594
1585
  checkpoint.engineVersion !== RECOVERY_ENGINE_VERSION ||
1595
1586
  checkpoint.threadId !== threadId ||
1596
- checkpoint.throughSequence > throughSequence ||
1597
- throughSequence - checkpoint.throughSequence > 4_096
1587
+ checkpoint.throughSequence > throughSequence
1598
1588
  )
1599
- return full;
1589
+ return Option.none();
1600
1590
 
1601
1591
  const decoded = yield* Schema.decodeUnknownEffect(RecoveryCheckpointContents)(
1602
1592
  checkpoint.state,
1603
1593
  ).pipe(Effect.option);
1604
1594
 
1605
- if (Option.isNone(decoded)) return full;
1595
+ if (Option.isNone(decoded)) return Option.none();
1606
1596
  const { state, digest } = decoded.value;
1607
1597
 
1608
1598
  if (
1609
- !submissionIds.every((id) => state.submissionIds.includes(id)) ||
1610
- state.seed.runId !== runIdForSubmission(state.submissionId) ||
1611
- (journalOwner !== undefined && state.seed.runId !== journalOwner)
1599
+ (state.seed === undefined && state.context === undefined) ||
1600
+ state.records.some(
1601
+ (record) => record.threadId !== threadId || record.sequence > checkpoint.throughSequence,
1602
+ ) ||
1603
+ (state.context !== undefined && state.context.throughSequence !== checkpoint.throughSequence)
1612
1604
  )
1613
- return full;
1605
+ return Option.none();
1614
1606
 
1615
1607
  const encoded = yield* Schema.encodeEffect(RecoveryCheckpointState)(state).pipe(
1616
1608
  Effect.mapError((cause) =>
@@ -1632,7 +1624,15 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1632
1624
  ),
1633
1625
  );
1634
1626
 
1635
- if (digest !== actualDigest) return full;
1627
+ if (digest !== actualDigest) return Option.none();
1628
+
1629
+ if (state.context !== undefined) {
1630
+ const prompt = yield* Schema.decodeUnknownEffect(Prompt.Prompt)(state.context.prompt).pipe(
1631
+ Effect.option,
1632
+ );
1633
+
1634
+ if (Option.isNone(prompt)) return Option.none();
1635
+ }
1636
1636
 
1637
1637
  const owner = yield* ledger
1638
1638
  .lookup(SubmissionLookupById.make({ submissionId: state.submissionId }))
@@ -1652,6 +1652,130 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1652
1652
  owner.value.agentDigests.agent !== checkpoint.agentDefinitionDigest ||
1653
1653
  owner.value.agentDigests.model !== checkpoint.modelDigest ||
1654
1654
  owner.value.agentDigests.tools !== checkpoint.toolDigest
1655
+ )
1656
+ return Option.none();
1657
+
1658
+ return Option.some({ checkpoint, state });
1659
+ });
1660
+
1661
+ const retainThreadIdentity = ({ record }: CanonicalRecordEnvelope): boolean =>
1662
+ record.payload._tag === "ThreadCreated" ||
1663
+ record.payload._tag === "SubagentLineageRecorded" ||
1664
+ record.payload._tag === "WorkerOriginRecorded" ||
1665
+ record.recordId.startsWith("subagent-lineage:");
1666
+
1667
+ /** Late/foreign evidence and new compactions require the original canonical proof. */
1668
+ const contextSuffixCompatible = (
1669
+ records: ReadonlyArray<CanonicalRecordEnvelope>,
1670
+ submissionId: SubmissionId,
1671
+ ): boolean => {
1672
+ const runId = runIdForSubmission(submissionId);
1673
+ const certificate = makeThreadContextCertificate();
1674
+
1675
+ for (const entry of records) {
1676
+ const { payload, recordId } = entry.record;
1677
+
1678
+ if (payload._tag === "CompactionCreated" || retainThreadIdentity(entry)) return false;
1679
+ if ("runId" in payload && payload.runId !== undefined) {
1680
+ if (payload.runId !== runId) return false;
1681
+ } else if (payload._tag === "AbortRequested") {
1682
+ if (
1683
+ payload.submissionId !== submissionId ||
1684
+ recordId !== submissionAbortRecordId(submissionId)
1685
+ )
1686
+ return false;
1687
+ } else if (payload._tag === "SubmissionSettled") {
1688
+ if (
1689
+ payload.submissionId !== submissionId ||
1690
+ recordId !== submissionSettlementRecordId(submissionId)
1691
+ )
1692
+ return false;
1693
+ } else return false;
1694
+ certificate.add(entry);
1695
+ }
1696
+
1697
+ return certificate.isValid();
1698
+ };
1699
+
1700
+ /** Run seeds retain their owner; certified Thread context serves only provably later Runs. */
1701
+ const recoveryView = Effect.fn("DurableAgentRuntime.recoveryView")(function* (
1702
+ threadId: ThreadId,
1703
+ throughSequence: CanonicalSequence,
1704
+ submissionIds: ReadonlyArray<SubmissionId>,
1705
+ journalOwner?: RunId,
1706
+ ): Effect.fn.Return<
1707
+ {
1708
+ readonly canonical: Stream.Stream<
1709
+ CanonicalRecordEnvelope,
1710
+ ThreadStoreError | ThreadNotMaterialized
1711
+ >;
1712
+ readonly seed?: JournalCheckpointSeed;
1713
+ readonly context?: ThreadContextCheckpoint;
1714
+ },
1715
+ ThreadStoreError | ThreadNotMaterialized
1716
+ > {
1717
+ const full = { canonical: canonicalRange(threadId, throughSequence) };
1718
+ const loaded = yield* loadRecoveryCheckpoint(threadId, throughSequence);
1719
+
1720
+ if (Option.isNone(loaded)) return full;
1721
+ const { checkpoint, state } = loaded.value;
1722
+
1723
+ if (throughSequence - checkpoint.throughSequence > 4_096) return full;
1724
+
1725
+ const submissionId = submissionIds.length === 1 ? submissionIds[0] : undefined;
1726
+
1727
+ if (
1728
+ state.context !== undefined &&
1729
+ submissionId !== undefined &&
1730
+ !submissionId.includes(":") &&
1731
+ (journalOwner === undefined || journalOwner === runIdForSubmission(submissionId))
1732
+ ) {
1733
+ const runId = runIdForSubmission(submissionId);
1734
+
1735
+ const input = yield* getRunInput({ threadId, runId }).pipe(
1736
+ Effect.provideService(ThreadStore, store),
1737
+ );
1738
+
1739
+ const controls = yield* Effect.forEach(
1740
+ [
1741
+ submissionInputRecordId(submissionId),
1742
+ submissionAbortRecordId(submissionId),
1743
+ submissionSettlementRecordId(submissionId),
1744
+ runStartedRecordId(runId),
1745
+ runCompletedRecordId(runId),
1746
+ ],
1747
+ (recordId) =>
1748
+ getRecord({ threadId, recordId }).pipe(Effect.provideService(ThreadStore, store)),
1749
+ );
1750
+
1751
+ const fresh = [input, ...controls].every(
1752
+ (record) =>
1753
+ Option.isNone(record) ||
1754
+ (record.value.sequence > checkpoint.throughSequence &&
1755
+ record.value.sequence <= throughSequence),
1756
+ );
1757
+
1758
+ if (fresh) {
1759
+ const suffix = yield* Stream.runCollect(
1760
+ canonicalRange(threadId, throughSequence, checkpoint.throughSequence),
1761
+ );
1762
+
1763
+ if (contextSuffixCompatible(suffix, submissionId))
1764
+ return {
1765
+ canonical: Stream.fromIterable([
1766
+ ...state.records.filter(retainThreadIdentity),
1767
+ ...suffix,
1768
+ ]),
1769
+ context: state.context,
1770
+ };
1771
+ }
1772
+ }
1773
+
1774
+ if (
1775
+ state.seed === undefined ||
1776
+ !submissionIds.every((id) => state.submissionIds.includes(id)) ||
1777
+ state.seed.runId !== runIdForSubmission(state.submissionId) ||
1778
+ (journalOwner !== undefined && state.seed.runId !== journalOwner)
1655
1779
  )
1656
1780
  return full;
1657
1781
  const replacement = state.seed.compaction;
@@ -1663,9 +1787,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1663
1787
  replacement.record.payload.kind === "clear-tool-results" ||
1664
1788
  replacement.record.payload.coversThrough < state.seed.throughSequence ||
1665
1789
  replacement.record.payload.coversThrough >= replacement.sequence ||
1666
- state.records.some(
1667
- (record) => record.threadId !== threadId || record.sequence > checkpoint.throughSequence,
1668
- ) ||
1669
1790
  !state.records.some(
1670
1791
  (record) =>
1671
1792
  record.sequence === replacement.sequence &&
@@ -1683,6 +1804,165 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
1683
1804
  return { canonical: Stream.fromIterable([...state.records, ...suffix]), seed: state.seed };
1684
1805
  });
1685
1806
 
1807
+ const persistRecoveryCheckpoint = Effect.fn("DurableAgentRuntime.persistRecoveryCheckpoint")(
1808
+ function* (
1809
+ ctx: AttemptAppendContext,
1810
+ submission: SubmissionSnapshot,
1811
+ state: RecoveryCheckpointState,
1812
+ tail: { readonly sequence: CanonicalSequence; readonly digest: Digest },
1813
+ createdAt: DateTime.Utc,
1814
+ ): Effect.fn.Return<void, DurableWorkerFailure> {
1815
+ if (store.recoveryCheckpoints === undefined) return;
1816
+
1817
+ const encoded = yield* Schema.encodeEffect(RecoveryCheckpointState)(state).pipe(
1818
+ Effect.option,
1819
+ );
1820
+
1821
+ if (Option.isNone(encoded)) return;
1822
+ const digest = yield* withCrypto(digestJson(encoded.value));
1823
+
1824
+ // Serialization removes shared in-memory references before applying persisted JSON bounds.
1825
+ const contents = yield* Schema.encodeEffect(
1826
+ Schema.fromJsonString(RecoveryCheckpointContents),
1827
+ )(RecoveryCheckpointContents.make({ state, digest })).pipe(
1828
+ Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(PersistedJson))),
1829
+ Effect.option,
1830
+ );
1831
+
1832
+ if (Option.isNone(contents)) return;
1833
+
1834
+ const checkpoint = ThreadCheckpoint.make({
1835
+ schemaVersion: 1,
1836
+ threadId: ctx.threadId,
1837
+ throughSequence: tail.sequence,
1838
+ tailDigest: tail.digest,
1839
+ engineVersion: RECOVERY_ENGINE_VERSION,
1840
+ agentDefinitionDigest: submission.agentDigests.agent,
1841
+ modelDigest: submission.agentDigests.model,
1842
+ toolDigest: submission.agentDigests.tools,
1843
+ state: contents.value,
1844
+ createdAt,
1845
+ });
1846
+
1847
+ yield* hit("checkpoint:before-save");
1848
+ yield* store.recoveryCheckpoints
1849
+ .save(SaveRecoveryCheckpointRequest.make({ checkpoint, producerEpoch: ctx.producerEpoch }))
1850
+ .pipe(Effect.catchTag("CheckpointRejected", () => Effect.void));
1851
+ yield* hit("checkpoint:after-save");
1852
+ },
1853
+ );
1854
+
1855
+ /** Refresh before settlement finalization, so completed processing includes the cache's cost. */
1856
+ const saveThreadContext = Effect.fn("DurableAgentRuntime.saveThreadContext")(function* (
1857
+ ctx: AttemptAppendContext,
1858
+ submission: SubmissionSnapshot,
1859
+ priorContext?: ThreadContextCheckpoint,
1860
+ ): Effect.fn.Return<void, DurableWorkerFailure> {
1861
+ if (store.recoveryCheckpoints === undefined) return;
1862
+ const tail = yield* Ref.get(ctx.tailRef);
1863
+ const loaded = yield* loadRecoveryCheckpoint(ctx.threadId, tail.sequence);
1864
+
1865
+ // Start certification only after compaction has produced an eligible runtime checkpoint.
1866
+ if (Option.isNone(loaded)) return;
1867
+ const previous = loaded.value;
1868
+
1869
+ let identities: ReadonlyArray<CanonicalRecordEnvelope> =
1870
+ previous.state.records.filter(retainThreadIdentity);
1871
+
1872
+ let projection: RunJournalProjection | undefined;
1873
+
1874
+ if (priorContext !== undefined && tail.sequence - priorContext.throughSequence <= 4_096) {
1875
+ const suffix = yield* Stream.runCollect(
1876
+ canonicalRange(ctx.threadId, tail.sequence, priorContext.throughSequence),
1877
+ );
1878
+
1879
+ if (contextSuffixCompatible(suffix, submission.submissionId)) {
1880
+ projection = yield* projectRunJournalStream(
1881
+ Stream.fromIterable(suffix),
1882
+ undefined,
1883
+ undefined,
1884
+ undefined,
1885
+ undefined,
1886
+ priorContext,
1887
+ ).pipe(Effect.catchTag("RunJournalError", () => Effect.succeed(undefined)));
1888
+ }
1889
+ }
1890
+
1891
+ if (projection === undefined) {
1892
+ const source = canonicalRange(ctx.threadId, tail.sequence);
1893
+ const certificate = makeThreadContextCertificate();
1894
+ const metadata = makeJournalMetadata(undefined);
1895
+ const retained: Array<CanonicalRecordEnvelope> = [];
1896
+
1897
+ yield* Stream.runForEach(source, (entry) =>
1898
+ Effect.sync(() => {
1899
+ certificate.add(entry);
1900
+ metadata.add(entry);
1901
+ if (retainThreadIdentity(entry) && retained.length <= 4_096) retained.push(entry);
1902
+ }),
1903
+ );
1904
+ if (!certificate.isValid() || retained.length > 4_096) return;
1905
+ identities = retained;
1906
+ projection = yield* projectRunJournalStream(
1907
+ source,
1908
+ undefined,
1909
+ undefined,
1910
+ undefined,
1911
+ metadata.snapshot(),
1912
+ ).pipe(Effect.catchTag("RunJournalError", () => Effect.succeed(undefined)));
1913
+ }
1914
+
1915
+ if (projection === undefined) return;
1916
+
1917
+ const prompt = yield* Schema.encodeEffect(Prompt.Prompt)(projection.prompt).pipe(
1918
+ Effect.flatMap(decodePersisted),
1919
+ Effect.option,
1920
+ );
1921
+
1922
+ if (Option.isNone(prompt)) return;
1923
+ let seed: JournalCheckpointSeed | undefined;
1924
+ let records = identities;
1925
+ let submissionIds: ReadonlyArray<SubmissionId> = [submission.submissionId];
1926
+
1927
+ if (
1928
+ previous.state.submissionId === submission.submissionId &&
1929
+ previous.state.seed !== undefined &&
1930
+ tail.sequence - previous.checkpoint.throughSequence + previous.state.records.length <= 4_096
1931
+ ) {
1932
+ const suffix = yield* Stream.runCollect(
1933
+ canonicalRange(ctx.threadId, tail.sequence, previous.checkpoint.throughSequence),
1934
+ );
1935
+
1936
+ if (checkpointSuffixCompatible(previous.state.seed, suffix, previous.state.records)) {
1937
+ seed = previous.state.seed;
1938
+ records = [...previous.state.records, ...suffix];
1939
+ submissionIds = previous.state.submissionIds;
1940
+ }
1941
+ }
1942
+
1943
+ yield* persistRecoveryCheckpoint(
1944
+ ctx,
1945
+ submission,
1946
+ RecoveryCheckpointState.make({
1947
+ schemaVersion: 2,
1948
+ policyAccountingVersion: 1,
1949
+ submissionId: submission.submissionId,
1950
+ submissionIds,
1951
+ ...(seed === undefined ? {} : { seed }),
1952
+ context: ThreadContextCheckpoint.make({
1953
+ throughSequence: tail.sequence,
1954
+ prompt: prompt.value,
1955
+ ...(projection.contextWindowId === undefined
1956
+ ? {}
1957
+ : { contextWindowId: projection.contextWindowId }),
1958
+ }),
1959
+ records,
1960
+ }),
1961
+ tail,
1962
+ yield* DateTime.now,
1963
+ );
1964
+ });
1965
+
1686
1966
  const readAllTolerant = Effect.fn("DurableAgentRuntime.readAllTolerant")(
1687
1967
  (
1688
1968
  threadId: ThreadId,
@@ -3353,6 +3633,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
3353
3633
  tokenRef: Ref.Ref<OwnershipToken>,
3354
3634
  outcome: AttemptOutcome,
3355
3635
  includeRunId: boolean,
3636
+ afterCanonical?: Effect.Effect<void, DurableWorkerFailure>,
3356
3637
  ): Effect.fn.Return<Settlement, DurableWorkerFailure> {
3357
3638
  const submissionId = submission.submissionId;
3358
3639
  const settlementId = submissionSettlementId(submissionId);
@@ -3420,6 +3701,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
3420
3701
  );
3421
3702
  yield* hit("terminalize:after-canonical-append");
3422
3703
  yield* notifyParentOfChildSettlement(submission, reserved.record);
3704
+ if (afterCanonical !== undefined) yield* afterCanonical;
3423
3705
 
3424
3706
  const settlement = yield* ledger.finalizeSettlement(
3425
3707
  SettlementFinalization.make({ submissionId, settlementId }),
@@ -4634,6 +4916,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4634
4916
  canonical: Stream.Stream<CanonicalRecordEnvelope, ThreadStoreError | ThreadNotMaterialized>,
4635
4917
  canonicalThrough: CanonicalSequence,
4636
4918
  journalSeed: JournalCheckpointSeed | undefined,
4919
+ priorContext: ThreadContextCheckpoint | undefined,
4637
4920
  journalMetadata: JournalMetadata | undefined,
4638
4921
  lineage: AttemptLineage,
4639
4922
  approvalDecisions: ReadonlyArray<ApprovalDecisionIntent>,
@@ -4654,7 +4937,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4654
4937
  cached.threadId === ctx.threadId &&
4655
4938
  cached.through === canonicalThrough &&
4656
4939
  cached.runId === runId &&
4657
- cached.seedThrough === journalSeed?.throughSequence
4940
+ cached.seedThrough === journalSeed?.throughSequence &&
4941
+ cached.contextThrough === priorContext?.throughSequence
4658
4942
  ? (boundaries.push(...cached.boundaries), cached.journal)
4659
4943
  : yield* projectRunJournalStream(
4660
4944
  canonical,
@@ -4662,6 +4946,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4662
4946
  (boundary) => boundaries.push(boundary),
4663
4947
  journalSeed,
4664
4948
  journalMetadata,
4949
+ priorContext,
4665
4950
  );
4666
4951
 
4667
4952
  // Do not retain the metadata snapshot across model or Tool waits, including cache hits.
@@ -4673,6 +4958,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4673
4958
  through: canonicalThrough,
4674
4959
  runId,
4675
4960
  seedThrough: journalSeed?.throughSequence,
4961
+ contextThrough: priorContext?.throughSequence,
4676
4962
  journal,
4677
4963
  boundaries,
4678
4964
  };
@@ -4682,10 +4968,13 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4682
4968
  if (store.recoveryCheckpoints === undefined) return;
4683
4969
  const tail = yield* Ref.get(ctx.tailRef);
4684
4970
 
4685
- const source = Stream.concat(
4686
- canonical,
4687
- canonicalRange(ctx.threadId, tail.sequence, canonicalThrough),
4688
- );
4971
+ const source =
4972
+ priorContext === undefined
4973
+ ? Stream.concat(
4974
+ canonical,
4975
+ canonicalRange(ctx.threadId, tail.sequence, canonicalThrough),
4976
+ )
4977
+ : canonicalRange(ctx.threadId, tail.sequence);
4689
4978
 
4690
4979
  let compaction: CanonicalRecordEnvelope | undefined;
4691
4980
  let latestResponse: CanonicalSequence | undefined;
@@ -4802,7 +5091,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4802
5091
  const validated = yield* Effect.try({
4803
5092
  try: () =>
4804
5093
  RecoveryCheckpointState.make({
4805
- schemaVersion: 1,
5094
+ schemaVersion: 2,
4806
5095
  policyAccountingVersion: 1,
4807
5096
  submissionId,
4808
5097
  submissionIds: [...ids],
@@ -4894,44 +5183,13 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4894
5183
  )
4895
5184
  return;
4896
5185
 
4897
- const encoded = yield* Schema.encodeEffect(RecoveryCheckpointState)(validated.value).pipe(
4898
- Effect.option,
4899
- );
4900
-
4901
- if (Option.isNone(encoded)) return;
4902
- const digest = yield* withCrypto(digestJson(encoded.value));
4903
-
4904
- // JSON serialization duplicates shared in-memory references without weakening the
4905
- // persisted depth/node/byte guard (which rejects cyclic/shared object graphs).
4906
- const contents = yield* Schema.encodeEffect(
4907
- Schema.fromJsonString(RecoveryCheckpointContents),
4908
- )(RecoveryCheckpointContents.make({ state: validated.value, digest })).pipe(
4909
- Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(PersistedJson))),
4910
- Effect.option,
5186
+ yield* persistRecoveryCheckpoint(
5187
+ ctx,
5188
+ submission,
5189
+ validated.value,
5190
+ tail,
5191
+ replacement.record.createdAt,
4911
5192
  );
4912
-
4913
- if (Option.isNone(contents)) return;
4914
-
4915
- const checkpoint = ThreadCheckpoint.make({
4916
- schemaVersion: 1,
4917
- threadId: ctx.threadId,
4918
- throughSequence: tail.sequence,
4919
- tailDigest: tail.digest,
4920
- engineVersion: RECOVERY_ENGINE_VERSION,
4921
- agentDefinitionDigest: submission.agentDigests.agent,
4922
- modelDigest: submission.agentDigests.model,
4923
- toolDigest: submission.agentDigests.tools,
4924
- state: contents.value,
4925
- createdAt: replacement.record.createdAt,
4926
- });
4927
-
4928
- yield* hit("checkpoint:before-save");
4929
- yield* store.recoveryCheckpoints
4930
- .save(
4931
- SaveRecoveryCheckpointRequest.make({ checkpoint, producerEpoch: ctx.producerEpoch }),
4932
- )
4933
- .pipe(Effect.catchTag("CheckpointRejected", () => Effect.void));
4934
- yield* hit("checkpoint:after-save");
4935
5193
  },
4936
5194
  );
4937
5195
 
@@ -4980,6 +5238,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
4980
5238
  runId,
4981
5239
  undefined,
4982
5240
  journalSeed,
5241
+ undefined,
5242
+ priorContext,
4983
5243
  );
4984
5244
 
4985
5245
  const rolloverOperation =
@@ -5952,19 +6212,25 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
5952
6212
  let sourceJournal = journal;
5953
6213
  let sourceBoundaries = boundaries;
5954
6214
 
5955
- if (commit.kind !== "summarize") {
6215
+ if (commit.kind !== "summarize" || priorContext !== undefined) {
5956
6216
  // Results must be canonical before pruning or rollover can cover them. Newly committed
5957
6217
  // compactions remain overlays on this Attempt's append-only source, so omit those
5958
6218
  // overlays while reconstructing the exact source-to-record mapping.
5959
- yield* recordHalt(commitPendingTurn);
6219
+ if (commit.kind !== "summarize") yield* recordHalt(commitPendingTurn);
5960
6220
  const tail = yield* Ref.get(ctx.tailRef);
5961
6221
 
5962
6222
  sourceBoundaries = [];
5963
6223
  sourceJournal = yield* recordHalt(
5964
6224
  projectRunJournalStream(
5965
- Stream.concat(
5966
- canonical,
5967
- canonicalRange(ctx.threadId, tail.sequence, canonicalThrough),
6225
+ (priorContext === undefined
6226
+ ? Stream.concat(
6227
+ canonical,
6228
+ canonicalRange(ctx.threadId, tail.sequence, canonicalThrough),
6229
+ )
6230
+ : canonicalRange(
6231
+ ctx.threadId,
6232
+ commit.kind === "summarize" ? canonicalThrough : tail.sequence,
6233
+ )
5968
6234
  ).pipe(
5969
6235
  Stream.filter(
5970
6236
  (envelope) =>
@@ -8552,14 +8818,21 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8552
8818
  };
8553
8819
 
8554
8820
  let approvalDecisionIntents = snapshot.approvalDecisions;
8821
+ let priorContext = initialView.context;
8555
8822
 
8556
8823
  while (true) {
8557
8824
  const tail = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
8558
8825
 
8559
- const canonical = Stream.concat(
8560
- initialView.canonical,
8561
- canonicalRange(threadId, tail.tailSequence, initialThrough),
8562
- );
8826
+ // An immediate resume may include a newly committed compaction. Rebuild its canonical
8827
+ // proof rather than carrying the initial Thread context across that boundary.
8828
+ const fullContextReplay = initialView.context !== undefined && priorContext === undefined;
8829
+
8830
+ const canonical = fullContextReplay
8831
+ ? canonicalRange(threadId, tail.tailSequence)
8832
+ : Stream.concat(
8833
+ initialView.canonical,
8834
+ canonicalRange(threadId, tail.tailSequence, initialThrough),
8835
+ );
8563
8836
 
8564
8837
  const currentRecords = yield* refreshControl(tail.tailSequence);
8565
8838
 
@@ -8573,7 +8846,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8573
8846
  canonical,
8574
8847
  tail.tailSequence,
8575
8848
  initialView.seed,
8576
- takeJournalMetadata(),
8849
+ priorContext,
8850
+ fullContextReplay ? undefined : takeJournalMetadata(),
8577
8851
  lineage,
8578
8852
  approvalDecisionIntents,
8579
8853
  currentContracts,
@@ -8581,6 +8855,8 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8581
8855
  yieldAfter,
8582
8856
  );
8583
8857
 
8858
+ priorContext = undefined;
8859
+
8584
8860
  if (outcome._tag === "yielded") {
8585
8861
  if (outcome.nextSubmissionId !== undefined) onHandoff(outcome.nextSubmissionId);
8586
8862
 
@@ -8736,7 +9012,18 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
8736
9012
  return Option.none<Settlement>();
8737
9013
  }
8738
9014
 
8739
- return Option.some(yield* terminalize(ctx, submission, tokenRef, outcome, true));
9015
+ return Option.some(
9016
+ yield* terminalize(
9017
+ ctx,
9018
+ submission,
9019
+ tokenRef,
9020
+ outcome,
9021
+ true,
9022
+ outcome._tag === "completed"
9023
+ ? saveThreadContext(ctx, submission, initialView.context)
9024
+ : undefined,
9025
+ ),
9026
+ );
8740
9027
  }
8741
9028
  }).pipe(Effect.scoped);
8742
9029