@farukada/aws-langgraph-dynamodb-ts 0.7.0 → 0.9.0

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 (208) hide show
  1. package/README.md +52 -9
  2. package/dist/checkpointer/actions/delete-thread.d.ts +3 -4
  3. package/dist/checkpointer/actions/delete-thread.d.ts.map +1 -1
  4. package/dist/checkpointer/actions/delete-thread.js +21 -41
  5. package/dist/checkpointer/actions/delete-thread.js.map +1 -1
  6. package/dist/checkpointer/actions/list.d.ts.map +1 -1
  7. package/dist/checkpointer/actions/list.js +38 -4
  8. package/dist/checkpointer/actions/list.js.map +1 -1
  9. package/dist/checkpointer/actions/put-writes.d.ts.map +1 -1
  10. package/dist/checkpointer/actions/put-writes.js +15 -24
  11. package/dist/checkpointer/actions/put-writes.js.map +1 -1
  12. package/dist/checkpointer/internal/item-reader.d.ts +45 -0
  13. package/dist/checkpointer/internal/item-reader.d.ts.map +1 -1
  14. package/dist/checkpointer/internal/item-reader.js +63 -1
  15. package/dist/checkpointer/internal/item-reader.js.map +1 -1
  16. package/dist/checkpointer/internal/item-writer.d.ts +0 -8
  17. package/dist/checkpointer/internal/item-writer.d.ts.map +1 -1
  18. package/dist/checkpointer/internal/item-writer.js +16 -21
  19. package/dist/checkpointer/internal/item-writer.js.map +1 -1
  20. package/dist/checkpointer/internal/keys.d.ts +19 -3
  21. package/dist/checkpointer/internal/keys.d.ts.map +1 -1
  22. package/dist/checkpointer/internal/keys.js +41 -6
  23. package/dist/checkpointer/internal/keys.js.map +1 -1
  24. package/dist/checkpointer/internal/special-write-cas.d.ts +30 -0
  25. package/dist/checkpointer/internal/special-write-cas.d.ts.map +1 -0
  26. package/dist/checkpointer/internal/special-write-cas.js +104 -0
  27. package/dist/checkpointer/internal/special-write-cas.js.map +1 -0
  28. package/dist/checkpointer/internal/special-write-cleanup.d.ts +18 -12
  29. package/dist/checkpointer/internal/special-write-cleanup.d.ts.map +1 -1
  30. package/dist/checkpointer/internal/special-write-cleanup.js +31 -86
  31. package/dist/checkpointer/internal/special-write-cleanup.js.map +1 -1
  32. package/dist/checkpointer/internal/special-write-verify.d.ts +54 -0
  33. package/dist/checkpointer/internal/special-write-verify.d.ts.map +1 -0
  34. package/dist/checkpointer/internal/special-write-verify.js +65 -0
  35. package/dist/checkpointer/internal/special-write-verify.js.map +1 -0
  36. package/dist/checkpointer/internal/validation.d.ts +8 -4
  37. package/dist/checkpointer/internal/validation.d.ts.map +1 -1
  38. package/dist/checkpointer/internal/validation.js +12 -10
  39. package/dist/checkpointer/internal/validation.js.map +1 -1
  40. package/dist/checkpointer/internal/write-guard.d.ts +13 -0
  41. package/dist/checkpointer/internal/write-guard.d.ts.map +1 -0
  42. package/dist/checkpointer/internal/write-guard.js +39 -0
  43. package/dist/checkpointer/internal/write-guard.js.map +1 -0
  44. package/dist/checkpointer/internal/write-index.d.ts +37 -0
  45. package/dist/checkpointer/internal/write-index.d.ts.map +1 -0
  46. package/dist/checkpointer/internal/write-index.js +42 -0
  47. package/dist/checkpointer/internal/write-index.js.map +1 -0
  48. package/dist/checkpointer/types.d.ts +8 -0
  49. package/dist/checkpointer/types.d.ts.map +1 -1
  50. package/dist/history/actions/add-messages.d.ts.map +1 -1
  51. package/dist/history/actions/add-messages.js +24 -4
  52. package/dist/history/actions/add-messages.js.map +1 -1
  53. package/dist/history/actions/clear.d.ts +4 -6
  54. package/dist/history/actions/clear.d.ts.map +1 -1
  55. package/dist/history/actions/clear.js +18 -33
  56. package/dist/history/actions/clear.js.map +1 -1
  57. package/dist/history/actions/get-messages.d.ts +2 -1
  58. package/dist/history/actions/get-messages.d.ts.map +1 -1
  59. package/dist/history/actions/get-messages.js +28 -2
  60. package/dist/history/actions/get-messages.js.map +1 -1
  61. package/dist/history/actions/list-sessions.d.ts +2 -1
  62. package/dist/history/actions/list-sessions.d.ts.map +1 -1
  63. package/dist/history/actions/list-sessions.js +21 -2
  64. package/dist/history/actions/list-sessions.js.map +1 -1
  65. package/dist/history/actions/reconcile-count.d.ts.map +1 -1
  66. package/dist/history/actions/reconcile-count.js +2 -2
  67. package/dist/history/actions/reconcile-count.js.map +1 -1
  68. package/dist/history/internal/append-saga.d.ts.map +1 -1
  69. package/dist/history/internal/append-saga.js +2 -78
  70. package/dist/history/internal/append-saga.js.map +1 -1
  71. package/dist/history/internal/compensation.d.ts +21 -0
  72. package/dist/history/internal/compensation.d.ts.map +1 -0
  73. package/dist/history/internal/compensation.js +84 -0
  74. package/dist/history/internal/compensation.js.map +1 -0
  75. package/dist/history/internal/keys.d.ts +9 -1
  76. package/dist/history/internal/keys.d.ts.map +1 -1
  77. package/dist/history/internal/keys.js +21 -4
  78. package/dist/history/internal/keys.js.map +1 -1
  79. package/dist/history/internal/message-chunker.d.ts.map +1 -1
  80. package/dist/history/internal/message-chunker.js +13 -5
  81. package/dist/history/internal/message-chunker.js.map +1 -1
  82. package/dist/history/internal/session-count.d.ts +21 -0
  83. package/dist/history/internal/session-count.d.ts.map +1 -1
  84. package/dist/history/internal/session-count.js +51 -0
  85. package/dist/history/internal/session-count.js.map +1 -1
  86. package/dist/history/internal/session-title.d.ts +20 -0
  87. package/dist/history/internal/session-title.d.ts.map +1 -0
  88. package/dist/history/internal/session-title.js +44 -0
  89. package/dist/history/internal/session-title.js.map +1 -0
  90. package/dist/history/internal/setup.d.ts +2 -1
  91. package/dist/history/internal/setup.d.ts.map +1 -1
  92. package/dist/history/internal/setup.js +1 -0
  93. package/dist/history/internal/setup.js.map +1 -1
  94. package/dist/history/internal/title-generator.d.ts +8 -2
  95. package/dist/history/internal/title-generator.d.ts.map +1 -1
  96. package/dist/history/internal/title-generator.js +12 -3
  97. package/dist/history/internal/title-generator.js.map +1 -1
  98. package/dist/history/internal/validation.d.ts +9 -0
  99. package/dist/history/internal/validation.d.ts.map +1 -0
  100. package/dist/history/internal/validation.js +16 -0
  101. package/dist/history/internal/validation.js.map +1 -0
  102. package/dist/history/types.d.ts +12 -0
  103. package/dist/history/types.d.ts.map +1 -1
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/shared/codec/s3/offloader.d.ts +7 -1
  108. package/dist/shared/codec/s3/offloader.d.ts.map +1 -1
  109. package/dist/shared/codec/s3/offloader.js +14 -1
  110. package/dist/shared/codec/s3/offloader.js.map +1 -1
  111. package/dist/shared/constants.d.ts +15 -0
  112. package/dist/shared/constants.d.ts.map +1 -1
  113. package/dist/shared/constants.js +16 -1
  114. package/dist/shared/constants.js.map +1 -1
  115. package/dist/shared/dynamodb/conditional-put.d.ts +51 -0
  116. package/dist/shared/dynamodb/conditional-put.d.ts.map +1 -0
  117. package/dist/shared/dynamodb/conditional-put.js +59 -0
  118. package/dist/shared/dynamodb/conditional-put.js.map +1 -0
  119. package/dist/shared/dynamodb/partition-delete.d.ts +33 -0
  120. package/dist/shared/dynamodb/partition-delete.d.ts.map +1 -0
  121. package/dist/shared/dynamodb/partition-delete.js +70 -0
  122. package/dist/shared/dynamodb/partition-delete.js.map +1 -0
  123. package/dist/shared/dynamodb/retry-classifier.d.ts.map +1 -1
  124. package/dist/shared/dynamodb/retry-classifier.js +9 -1
  125. package/dist/shared/dynamodb/retry-classifier.js.map +1 -1
  126. package/dist/shared/logging/redaction-walk.d.ts +23 -0
  127. package/dist/shared/logging/redaction-walk.d.ts.map +1 -0
  128. package/dist/shared/logging/redaction-walk.js +92 -0
  129. package/dist/shared/logging/redaction-walk.js.map +1 -0
  130. package/dist/shared/logging/redaction.d.ts +26 -17
  131. package/dist/shared/logging/redaction.d.ts.map +1 -1
  132. package/dist/shared/logging/redaction.js +24 -48
  133. package/dist/shared/logging/redaction.js.map +1 -1
  134. package/dist/shared/logging/secret-patterns.d.ts +74 -0
  135. package/dist/shared/logging/secret-patterns.d.ts.map +1 -0
  136. package/dist/shared/logging/secret-patterns.js +107 -0
  137. package/dist/shared/logging/secret-patterns.js.map +1 -0
  138. package/dist/shared/validation/primitives.d.ts +9 -1
  139. package/dist/shared/validation/primitives.d.ts.map +1 -1
  140. package/dist/shared/validation/primitives.js +13 -0
  141. package/dist/shared/validation/primitives.js.map +1 -1
  142. package/dist/store/actions/get.d.ts.map +1 -1
  143. package/dist/store/actions/get.js +15 -1
  144. package/dist/store/actions/get.js.map +1 -1
  145. package/dist/store/actions/list-namespaces.d.ts.map +1 -1
  146. package/dist/store/actions/list-namespaces.js +1 -0
  147. package/dist/store/actions/list-namespaces.js.map +1 -1
  148. package/dist/store/actions/put.d.ts.map +1 -1
  149. package/dist/store/actions/put.js +24 -18
  150. package/dist/store/actions/put.js.map +1 -1
  151. package/dist/store/actions/search.d.ts.map +1 -1
  152. package/dist/store/actions/search.js +4 -35
  153. package/dist/store/actions/search.js.map +1 -1
  154. package/dist/store/internal/backend-search.d.ts +5 -0
  155. package/dist/store/internal/backend-search.d.ts.map +1 -0
  156. package/dist/store/internal/backend-search.js +68 -0
  157. package/dist/store/internal/backend-search.js.map +1 -0
  158. package/dist/store/internal/filter.d.ts.map +1 -1
  159. package/dist/store/internal/filter.js +46 -5
  160. package/dist/store/internal/filter.js.map +1 -1
  161. package/dist/store/internal/index-reconcile.d.ts.map +1 -1
  162. package/dist/store/internal/index-reconcile.js +41 -5
  163. package/dist/store/internal/index-reconcile.js.map +1 -1
  164. package/dist/store/internal/item-mapper.d.ts +1 -0
  165. package/dist/store/internal/item-mapper.d.ts.map +1 -1
  166. package/dist/store/internal/item-mapper.js +2 -0
  167. package/dist/store/internal/item-mapper.js.map +1 -1
  168. package/dist/store/internal/keys.d.ts +1 -1
  169. package/dist/store/internal/keys.d.ts.map +1 -1
  170. package/dist/store/internal/keys.js +8 -2
  171. package/dist/store/internal/keys.js.map +1 -1
  172. package/dist/store/internal/overwrite-swap.d.ts +33 -0
  173. package/dist/store/internal/overwrite-swap.d.ts.map +1 -0
  174. package/dist/store/internal/overwrite-swap.js +62 -0
  175. package/dist/store/internal/overwrite-swap.js.map +1 -0
  176. package/dist/store/internal/persist.d.ts +22 -6
  177. package/dist/store/internal/persist.d.ts.map +1 -1
  178. package/dist/store/internal/persist.js +41 -20
  179. package/dist/store/internal/persist.js.map +1 -1
  180. package/dist/store/internal/read-existing.d.ts +19 -0
  181. package/dist/store/internal/read-existing.d.ts.map +1 -0
  182. package/dist/store/internal/read-existing.js +29 -0
  183. package/dist/store/internal/read-existing.js.map +1 -0
  184. package/dist/store/internal/score-direction.d.ts +32 -0
  185. package/dist/store/internal/score-direction.d.ts.map +1 -0
  186. package/dist/store/internal/score-direction.js +39 -0
  187. package/dist/store/internal/score-direction.js.map +1 -0
  188. package/dist/store/internal/search-filter.d.ts +4 -0
  189. package/dist/store/internal/search-filter.d.ts.map +1 -0
  190. package/dist/store/internal/search-filter.js +11 -0
  191. package/dist/store/internal/search-filter.js.map +1 -0
  192. package/dist/store/internal/setup.d.ts +13 -1
  193. package/dist/store/internal/setup.d.ts.map +1 -1
  194. package/dist/store/internal/setup.js +57 -1
  195. package/dist/store/internal/setup.js.map +1 -1
  196. package/dist/store/internal/validation.d.ts +8 -2
  197. package/dist/store/internal/validation.d.ts.map +1 -1
  198. package/dist/store/internal/validation.js +15 -6
  199. package/dist/store/internal/validation.js.map +1 -1
  200. package/dist/store/internal/write-verify.d.ts +31 -6
  201. package/dist/store/internal/write-verify.d.ts.map +1 -1
  202. package/dist/store/internal/write-verify.js +48 -8
  203. package/dist/store/internal/write-verify.js.map +1 -1
  204. package/dist/store/types.d.ts +16 -0
  205. package/dist/store/types.d.ts.map +1 -1
  206. package/dist/store/vector-backend.d.ts +17 -0
  207. package/dist/store/vector-backend.d.ts.map +1 -1
  208. package/package.json +1 -1
package/README.md CHANGED
@@ -57,7 +57,7 @@ npm install @langchain/aws # e.g. Bedrock Titan embeddings
57
57
 
58
58
  ## Table schema
59
59
 
60
- Every adapter uses the **same simple key schema**: a string partition key `PK`, a string sort key `SK`, and an optional Number `ttl` attribute for expiry. Because the key spaces never collide, **a single table can back all three adapters**, or you can use a separate table per adapter — your choice via the `tableName` option.
60
+ Every adapter uses the **same simple key schema**: a string partition key `PK`, a string sort key `SK`, and an optional Number `ttl` attribute for expiry. **A single table can back all three adapters**, or you can use a separate table per adapter — your choice via the `tableName` option.
61
61
 
62
62
  | Attribute | Type | Role |
63
63
  | --- | --- | --- |
@@ -67,9 +67,13 @@ Every adapter uses the **same simple key schema**: a string partition key `PK`,
67
67
 
68
68
  How each adapter lays out keys (informational — you don't manage this):
69
69
 
70
- - **Checkpointer** — `PK = <thread_id>`; `SK` = `META#<ns>#<checkpoint_id>` (metadata), `PAYLOAD#<ns>#<checkpoint_id>` (checkpoint), `WRITE#<ns>#<checkpoint_id>#<task>#<idx>` (pending writes).
71
- - **Store** — `PK = <namespace[0]>` (the scope root); `SK = <namespace[1..]>#<key>`. This makes a scoped prefix search a native `Query` (`PK = root AND begins_with(SK, …)`); only a rootless "search everything" falls back to a `Scan`.
72
- - **Chat history** — `PK = <sessionId>`; one item per message at `SK = HISTORY#MSG#<ULID>` (ordered, append-only) plus one `SK = HISTORY#SESSION` metadata item. The `HISTORY#` tag keeps these keys from colliding with a store item on a table shared via `createAll()` (a bare `SESSION` sort key is an ordinary, easy-to-produce store key — `store.put([sessionId], 'SESSION', …)`).
70
+ - **Checkpointer** — `PK = CHKPT#<thread_id>`; `SK` = `META#<ns>#<checkpoint_id>` (metadata), `PAYLOAD#<ns>#<checkpoint_id>` (checkpoint), `WRITE#<ns>#<checkpoint_id>#<task>#<idx>#<channel>` (pending writes).
71
+ - **Store** — `PK = STORE#<namespace[0]>` (the scope root); `SK = <namespace[1..]>#<key>`. This makes a scoped prefix search a native `Query` (`PK = root AND begins_with(SK, …)`); only a rootless "search everything" falls back to a `Scan`.
72
+ - **Chat history** — `PK = HIST#<sessionId>`; one item per message at `SK = HISTORY#MSG#<ULID>` (ordered, append-only) plus one `SK = HISTORY#SESSION` metadata item.
73
+
74
+ **Why the key spaces cannot collide.** Each adapter tags its partition key with its own prefix, and those three tags differ in their very first character, so no `CHKPT#…` can ever equal a `STORE#…` or `HIST#…` — whatever identifiers you pass. That matters because reusing one id across adapters (a "conversation id" used as both a `thread_id` and a `sessionId`) is an entirely ordinary design: without the tags it put unrelated adapters' rows in one partition, where `deleteThread()`/`history.clear()` would delete each other's data and identically-composed sort keys could silently overwrite one another.
75
+
76
+ Two further guards back that up, for a table holding hand-written rows or rows written before an upgrade: `deleteThread()`/`clear()` delete only rows whose sort key belongs to the calling adapter and log anything they leave in place, and every read narrows a row's shape before decoding it rather than trusting the key it was found at.
73
77
 
74
78
  ## Quick start
75
79
 
@@ -189,8 +193,9 @@ All adapters share a common base. Provide **either** a prebuilt `client` (which
189
193
  | `compression` | `CompressionConfig` | all | `{ enabled, minSizeBytes?, level?, maxDecompressedBytes? }` |
190
194
  | `s3` | `S3OffloadConfig` | all | offload large payloads to S3 (see below) |
191
195
  | `serde` | `SerializerProtocol` | all | serializer override (checkpointer defaults to LangGraph's; store/history to JSON) |
196
+ | `onCorruptMessage` | `'skip' \| 'throw'` | history only | what `getMessages` does with an item it cannot decode (default `skip`: drop it, log at `error`, return the rest) |
192
197
  | `index` | `IndexConfig` | store only | `{ dims, embeddings, fields? }` for semantic search |
193
- | `vectorBackend` | `VectorBackend` | store only | delegate similarity search to an external index; DynamoDB keeps the canonical item |
198
+ | `vectorBackend` | `VectorBackend` | store only | delegate similarity search to an external index; DynamoDB keeps the canonical item. **Requires `index`** — constructing a store with one and not the other throws |
194
199
  | `maxSearchCandidates` | `number` | store only | cap for the in-DB ranker before it errors (default 1000) |
195
200
  | `maxScanItems` | `number` | store only | cap on items scanned into memory during a plain (non-semantic) `search()` before it errors (default 10000, the shared in-memory cap used by every paginated read) |
196
201
 
@@ -202,7 +207,7 @@ When `keyPrefix` is omitted, each adapter defaults to its own sub-prefix under t
202
207
 
203
208
  **Gzip compression** — set `compression: { enabled: true }`. Payloads at or above `minSizeBytes` (default 1 KB) are gzipped transparently; decompression auto-detects on read and is guarded against decompression-bomb expansion (`maxDecompressedBytes`, default 50 MiB).
204
209
 
205
- **S3 offloading** — set `s3: { bucketName }`. Any serialized payload at or above `thresholdBytes` (default 350 KB) is written to S3, with only a reference stored in DynamoDB; reads rehydrate transparently. Requires the optional `@aws-sdk/client-s3` peer. Deleting a checkpoint thread / chat session also best-effort deletes its offloaded objects. When a `ttl` is also configured, call `ensureS3LifecycleRule()` once (e.g. during deployment) to best-effort install a matching S3 lifecycle expiration rule (logged, never fatal) — this is opt-in rather than automatic, since it requires the broader `s3:PutLifecycleConfiguration` bucket-level permission and is not safe to fire on every adapter construction. If you configure `ttl` + `s3` but never call it, nothing reclaims objects that best-effort cleanup misses (a failed delete, or a write that lost the checkpointer's first-write-wins race) — they stay in the bucket until you remove them or add a lifecycle rule yourself. The store has an analogous, smaller-scale race: two concurrent `put`s to the same key that both read the previous descriptor before either commits each upload to their own distinct (nonced) key and each then try to clean up that same previous descriptor — the loser's own upload becomes an orphan (bounded at one per race), the same class of leak, reclaimed the same way.
210
+ **S3 offloading** — set `s3: { bucketName }`. Any serialized payload at or above `thresholdBytes` (default 350 KB) is written to S3, with only a reference stored in DynamoDB; reads rehydrate transparently. Requires the optional `@aws-sdk/client-s3` peer. Deleting a checkpoint thread / chat session also best-effort deletes its offloaded objects. When a `ttl` is also configured, call `ensureS3LifecycleRule()` once (e.g. during deployment) to best-effort install a matching S3 lifecycle expiration rule (logged, never fatal) — this is opt-in rather than automatic, since it requires the broader `s3:PutLifecycleConfiguration` bucket-level permission and is not safe to fire on every adapter construction. If you configure `ttl` + `s3` but never call it, nothing reclaims objects that best-effort cleanup misses — they stay in the bucket until you remove them or add a lifecycle rule yourself. Both the store's concurrent-`put` overwrite race and the checkpointer's *special*-write overwrite race (`__error__`, `__interrupt__`, `__resume__`, `__scheduled__`) are now **prevented** by a compare-and-swap: each overwrite pins the previous descriptor it observed and re-reads on rejection, so it deletes exactly the payload it actually superseded instead of racing another writer for the same one. A leak from either path is now possible only in these residual cases, still backstopped by `ensureS3LifecycleRule()`: the bounded compare-and-swap (3 attempts) is exhausted under pathological contention, which falls back to an unconditional overwrite and logs a `warn`; a best-effort delete genuinely fails; or one double-fault interleaving — a write that loses the swap and then exhausts its transient-error retries on an attempt that actually landed — leaves cleanup targeting the stale descriptor rather than the one it truly superseded, orphaning one object (it never deletes a live object). Separately, and unchanged by any of the above, the checkpointer's *regular* (non-special) writes still resolve a genuine race first-write-wins with no compare-and-swap, so the loser's own upload there remains an orphan reclaimed only by best-effort cleanup and `ensureS3LifecycleRule()`.
206
211
 
207
212
  **TTL expiry** — set `ttl: { days }` or `ttl: { seconds }`. The `ttl` attribute is written as a Unix-epoch-seconds timestamp; enable DynamoDB TTL on the `ttl` attribute for automatic deletion. Chat history anchors a single **uniform whole-conversation TTL** on the session's metadata row, shared by every message: normally it's set once, at session creation, via `if_not_exists`; but if the previously-stored anchor is ever found missing or already expired (DynamoDB's own TTL sweep can lag up to ~48h), the next append heals it with a plain overwrite instead of staying stuck. Every message written at any point in time shares whatever the current anchor is; expired messages are also filtered out on read. If the append that triggers a stale-anchor heal is itself later rolled back (a later chunk in the same call failed), the healed ttl is not reverted — the session simply keeps the fresher, never-shorter expiry rather than risk regressing a value a concurrent legitimate extension may have since written; this is a deliberate, self-healing tradeoff, not a bug.
208
213
 
@@ -251,7 +256,18 @@ const logger: Logger = {
251
256
  const store = new DynamoDBStore({ tableName: 'langgraph', logger: redactLogger(logger) });
252
257
  ```
253
258
 
254
- `redactLogger` wraps a logger so secret-looking fields (access keys, tokens, passwords, …) are replaced with `[REDACTED]` in structured log arguments. `redactSecrets` exposes the same redaction for arbitrary objects.
259
+ `redactLogger` wraps a logger so secret-looking fields (access keys, tokens, passwords, …) are replaced with `[REDACTED]` in structured log arguments. It also scans **string values, including an error's `message` and `stack`**, for recognisable credential shapes — AWS access key ids, `Bearer` tokens, JWTs, and `password=`/`token=` assignments — replacing just the matched substring so the text stays readable. Pass `extraKeys` to add field names and `extraValuePatterns` to add shapes. `redactSecrets` exposes the same redaction for arbitrary objects.
260
+
261
+ **What the library logs.** Nothing at all until you inject a logger — that is deliberate, a library should not write to your console uninvited. Once one is attached, the events worth alerting on are:
262
+
263
+ | Level | Event |
264
+ | --- | --- |
265
+ | `info` | `deleteThread` / `history.clear` completing, with rows deleted and rows skipped |
266
+ | `warn` | a row left in place by a delete because it belongs to another adapter |
267
+ | `warn` | a row skipped on read because its shape is not this adapter's (`store.get`, checkpointer `list`) |
268
+ | `warn` | a pending-write guard rejection whose existing row holds an unexpected channel |
269
+ | `warn` | a `vectorBackend` returning ascending scores, or a match this store cannot address |
270
+ | `error` | a chat message skipped because it could not be decoded, with its sort key |
255
271
 
256
272
  ## Infrastructure setup
257
273
 
@@ -315,6 +331,33 @@ When S3 offloading is enabled, on the bucket/objects: `s3:GetObject`, `s3:PutObj
315
331
 
316
332
  ## Migrating from earlier versions
317
333
 
334
+ **0.7.x → 0.8.0**: **every adapter's partition key is now adapter-tagged** —
335
+ `PK = <thread_id>` → `CHKPT#<thread_id>`, `PK = <namespace[0]>` →
336
+ `STORE#<namespace[0]>`, `PK = <sessionId>` → `HIST#<sessionId>` — and the
337
+ checkpointer's pending-write sort key gains a trailing channel segment
338
+ (`WRITE#<ns>#<id>#<task>#<idx>` → `…#<idx>#<channel>`).
339
+
340
+ This closes two real defects on a table shared via `createAll()`. Reusing one
341
+ identifier across adapters — a "conversation id" used as both a `thread_id`
342
+ and a `sessionId`, an entirely ordinary choice — put unrelated adapters' rows
343
+ in a single partition, where `deleteThread()` silently deleted the chat
344
+ history (and vice versa), and identically-composed sort keys let one adapter
345
+ overwrite another's item or hand its payload back on read. The channel segment
346
+ separately stops a retried task whose write mix changed from silently losing a
347
+ write; a per-call write group, also stored on each row, keeps such a retry from
348
+ replaying a channel twice. See the CHANGELOG for the full mechanism.
349
+
350
+ **Data written by 0.7.x is not found after upgrading**, for all three
351
+ adapters. Back up and recreate the table before upgrading.
352
+
353
+ Three behaviour changes are also worth checking before you upgrade:
354
+ constructing a `DynamoDBStore` with a `vectorBackend` but no `index` now
355
+ throws instead of silently returning unranked results; `getMessages` skips and
356
+ logs an undecodable message instead of failing the whole read (pass
357
+ `onCorruptMessage: 'throw'` to keep the old behaviour); and the store's
358
+ `$gt`/`$gte`/`$lt`/`$lte` filters no longer coerce across types, so a stored
359
+ `'10'` no longer matches `{ $gt: 5 }`.
360
+
318
361
  **0.6.x → 0.7.0**: chat-history's sort keys now carry a `HISTORY#` item-kind
319
362
  tag — `SK = SESSION` → `SK = HISTORY#SESSION`, `SK = MSG#<ULID>` →
320
363
  `SK = HISTORY#MSG#<ULID>`. This closes a real key collision on a table
@@ -339,9 +382,9 @@ unaffected.
339
382
 
340
383
  ## Production notes
341
384
 
342
- - **Sharing one table** across all three adapters is supported — their key spaces don't collide, and table-wide reads filter to their own items. Checkpointer, chat-history, and *scoped* store reads are all partition-scoped (`Query`/`GetItem`).
385
+ - **Sharing one table** across all three adapters is supported — adapter-tagged partition keys make the key spaces provably disjoint (see [Table schema](#table-schema)), and table-wide reads filter to their own items. Checkpointer, chat-history, and *scoped* store reads are all partition-scoped (`Query`/`GetItem`).
343
386
  - **Scoped reads are `Query`s.** `store.search`/`store.listNamespaces` with a concrete namespace prefix and `history.getMessages` are native `Query`s. Only a rootless `store.search([])` / unprefixed `listNamespaces` and `history.listSessions` fall back to `Scan` (cost scales with table size) — keep those rare or use a dedicated table. `listSessions` accepts an optional `{ maxIterations }` override for tables where non-session rows dominate the scan.
344
- - **Hot partitions.** The store's partition key is `namespace[0]` and chat history's is `sessionId`. A single partition tops out around ~1000 WCU / 3000 RCU, so avoid funneling very high write throughput through one tenant/session id; spread load across scope roots (e.g. include a tenant id as `namespace[0]`).
387
+ - **Hot partitions.** The store's partition key is `STORE#<namespace[0]>` and chat history's is `HIST#<sessionId>` — the adapter tag is constant, so throughput still concentrates on the identifier you choose. A single partition tops out around ~1000 WCU / 3000 RCU, so avoid funneling very high write throughput through one tenant/session id; spread load across scope roots (e.g. include a tenant id as `namespace[0]`).
345
388
  - **Very large vector corpora** outgrow the in-DB ranker (`maxSearchCandidates`). Configure a `vectorBackend` (OpenSearch, pgvector, …) — the library keeps DynamoDB as the source of truth and only delegates similarity ranking.
346
389
  - **TTL deletion timing** is governed by DynamoDB (typically within 48 h of expiry) and S3 lifecycle expiry is day-granular — the library writes the correct expiry timestamp (and filters expired chat messages on read) but does not guarantee instant deletion. The matching S3 lifecycle rule is not written automatically: it is installed only when you call `ensureS3LifecycleRule()`.
347
390
 
@@ -1,10 +1,9 @@
1
1
  import type { CheckpointerContext } from '../internal/setup';
2
2
  /**
3
3
  * Delete every checkpoint, payload, and write for a thread (all share the
4
- * thread's partition), best-effort deleting any offloaded S3 objects. Streams
5
- * the partition with unbounded pagination and flushes deletes in batches, so a
6
- * thread of any size is deleted to completion with bounded memory — never
7
- * silently truncated at the in-memory page caps.
4
+ * thread's partition), best-effort deleting any offloaded S3 objects. Rows
5
+ * this adapter does not own are left in place and logged, so a shared-table
6
+ * partition holding a foreign row is never collaterally wiped.
8
7
  */
9
8
  export declare function deleteThread(context: CheckpointerContext, threadId: string): Promise<void>;
10
9
  //# sourceMappingURL=delete-thread.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"delete-thread.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/delete-thread.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AA4C7D;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAehG"}
1
+ {"version":3,"file":"delete-thread.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/delete-thread.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAY7D;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhG"}
@@ -1,55 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deleteThread = deleteThread;
4
- const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
5
- const orphans_1 = require("../../shared/codec/s3/orphans");
6
- const constants_1 = require("../../shared/constants");
7
- const batch_write_1 = require("../../shared/dynamodb/batch-write");
8
- const paginate_1 = require("../../shared/dynamodb/paginate");
9
- const primitives_1 = require("../../shared/validation/primitives");
4
+ const partition_delete_1 = require("../../shared/dynamodb/partition-delete");
10
5
  const keys_1 = require("../internal/keys");
11
6
  const query_1 = require("../internal/query");
12
- /** Append an item's delete key and any offloaded payload descriptors to the buffer. */
13
- function bufferItem(buffer, item) {
14
- buffer.keys.push({ PK: item.PK, SK: item.SK });
15
- for (const descriptor of [item.metadata, item.checkpoint, item.value]) {
16
- if (descriptor)
17
- buffer.descriptors.push(descriptor);
18
- }
19
- }
20
- /** Delete the buffered keys and best-effort clean their S3 objects, then clear it. */
21
- async function flushBuffer(context, buffer) {
22
- if (buffer.keys.length === 0)
23
- return;
24
- await (0, batch_write_1.batchWriteAll)(context.client, context.tableName, buffer.keys.map((Key) => ({ DeleteRequest: { Key } })));
25
- if (context.offloader) {
26
- await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)(buffer.descriptors), 'deleteThread', context.logger);
27
- }
28
- buffer.keys = [];
29
- buffer.descriptors = [];
7
+ const validation_1 = require("../internal/validation");
8
+ /** The offloaded payloads a checkpointer row can reference. */
9
+ function descriptorsOf(row) {
10
+ return [
11
+ row.metadata,
12
+ row.checkpoint,
13
+ row.value,
14
+ ];
30
15
  }
31
16
  /**
32
17
  * Delete every checkpoint, payload, and write for a thread (all share the
33
- * thread's partition), best-effort deleting any offloaded S3 objects. Streams
34
- * the partition with unbounded pagination and flushes deletes in batches, so a
35
- * thread of any size is deleted to completion with bounded memory — never
36
- * silently truncated at the in-memory page caps.
18
+ * thread's partition), best-effort deleting any offloaded S3 objects. Rows
19
+ * this adapter does not own are left in place and logged, so a shared-table
20
+ * partition holding a foreign row is never collaterally wiped.
37
21
  */
38
22
  async function deleteThread(context, threadId) {
39
- (0, primitives_1.validateNonEmptyString)(threadId, 'threadId');
40
- const params = (0, query_1.partitionQuery)(context.tableName, (0, keys_1.partitionKey)(threadId), { consistent: true });
41
- const buffer = { keys: [], descriptors: [] };
42
- const pages = (0, paginate_1.paginateQuery)({
23
+ (0, validation_1.validateThreadId)(threadId);
24
+ await (0, partition_delete_1.deletePartitionRows)({
43
25
  client: context.client,
44
- params,
45
- maxItems: Number.POSITIVE_INFINITY,
46
- maxIterations: Number.POSITIVE_INFINITY,
26
+ tableName: context.tableName,
27
+ params: (0, query_1.partitionQuery)(context.tableName, (0, keys_1.partitionKey)(threadId), { consistent: true }),
28
+ logger: context.logger,
29
+ offloader: context.offloader,
30
+ operation: 'deleteThread',
31
+ ownsSortKey: keys_1.isCheckpointerSortKey,
32
+ descriptorsOf,
47
33
  });
48
- for await (const raw of pages) {
49
- bufferItem(buffer, raw);
50
- if (buffer.keys.length >= constants_1.BATCH_WRITE_MAX)
51
- await flushBuffer(context, buffer);
52
- }
53
- await flushBuffer(context, buffer);
54
34
  }
55
35
  //# sourceMappingURL=delete-thread.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"delete-thread.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/delete-thread.ts"],"names":[],"mappings":";;;AACA,wEAAmE;AACnE,2DAAiE;AACjE,sDAAyD;AACzD,mEAAkE;AAClE,6DAA+D;AAC/D,mEAA4E;AAC5E,2CAAgD;AAChD,6CAAmD;AAiBnD,uFAAuF;AACvF,SAAS,UAAU,CAAC,MAAoB,EAAE,IAAmB;IAC3D,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,KAAK,MAAM,UAAU,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtE,IAAI,UAAU;YAAE,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,KAAK,UAAU,WAAW,CAAC,OAA4B,EAAE,MAAoB;IAC3E,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACrC,MAAM,IAAA,2BAAa,EACjB,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,SAAS,EACjB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CACvD,CAAC;IACF,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACtB,MAAM,IAAA,0BAAgB,EACpB,OAAO,CAAC,SAAS,EACjB,IAAA,+BAAa,EAAC,MAAM,CAAC,WAAW,CAAC,EACjC,cAAc,EACd,OAAO,CAAC,MAAM,CACf,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,MAAM,CAAC,WAAW,GAAG,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,uBAAuB,OAA4B,EAAE,QAAgB;IAC/E,IAAA,mCAAsB,EAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAA,sBAAc,EAAC,OAAO,CAAC,SAAS,EAAE,IAAA,mBAAY,EAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/F,MAAM,MAAM,GAAiB,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;IAC3D,MAAM,KAAK,GAAG,IAAA,wBAAa,EAAC;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM;QACN,QAAQ,EAAE,MAAM,CAAC,iBAAiB;QAClC,aAAa,EAAE,MAAM,CAAC,iBAAiB;KACxC,CAAC,CAAC;IACH,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QAC9B,UAAU,CAAC,MAAM,EAAE,GAAoB,CAAC,CAAC;QACzC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,2BAAe;YAAE,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAChF,CAAC;IACD,MAAM,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;AACrC,CAAC"}
1
+ {"version":3,"file":"delete-thread.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/delete-thread.ts"],"names":[],"mappings":";;;AACA,6EAA6E;AAE7E,2CAAuE;AACvE,6CAAmD;AAEnD,uDAA0D;AAE1D,+DAA+D;AAC/D,SAAS,aAAa,CAAC,GAAY;IACjC,OAAO;QACL,GAAG,CAAC,QAAyC;QAC7C,GAAG,CAAC,UAA2C;QAC/C,GAAG,CAAC,KAAsC;KAC3C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACI,KAAK,uBAAuB,OAA4B,EAAE,QAAgB;IAC/E,IAAA,6BAAgB,EAAC,QAAQ,CAAC,CAAC;IAC3B,MAAM,IAAA,sCAAmB,EAAC;QACxB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,IAAA,sBAAc,EAAC,OAAO,CAAC,SAAS,EAAE,IAAA,mBAAY,EAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;QACvF,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,cAAc;QACzB,WAAW,EAAE,4BAAqB;QAClC,aAAa;KACd,CAAC,CAAC;AACL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAS9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AA2B7D;;;;GAIG;AACH,wBAAuB,eAAe,CACpC,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,cAAc,CAAC,eAAe,CAAC,CAqBjC"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAU9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAyC7D;;;;GAIG;AACH,wBAAuB,eAAe,CACpC,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,EACtB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,cAAc,CAAC,eAAe,CAAC,CAgDjC"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.listCheckpoints = listCheckpoints;
4
+ const constants_1 = require("../../shared/constants");
4
5
  const paginate_1 = require("../../shared/dynamodb/paginate");
5
6
  const assemble_1 = require("../internal/assemble");
6
7
  const configurable_1 = require("../internal/configurable");
@@ -16,6 +17,16 @@ function readListOptions(options) {
16
17
  limit: options?.limit,
17
18
  };
18
19
  }
20
+ /**
21
+ * True when `meta` passes the key-level list filters: `before` (strictly older
22
+ * checkpoints) and an explicitly requested `checkpoint_id`. Split out from the
23
+ * loop so each filter stays independently readable.
24
+ */
25
+ function passesKeyFilters(meta, checkpointId, before) {
26
+ if (checkpointId !== undefined && meta.checkpointId !== checkpointId)
27
+ return false;
28
+ return before === undefined || meta.checkpointId < before;
29
+ }
19
30
  /** True when `meta` passes the (optional) metadata-equality filter. */
20
31
  async function passesMetadataFilter(context, meta, filter) {
21
32
  if (!filter)
@@ -33,13 +44,36 @@ async function* listCheckpoints(context, config, options) {
33
44
  const { before, filter, limit } = readListOptions(options);
34
45
  const params = (0, query_1.beginsWithQuery)(context.tableName, (0, keys_1.partitionKey)(threadId), (0, keys_1.metaSortKeyPrefix)(checkpointNs));
35
46
  let yielded = 0;
36
- for await (const raw of (0, paginate_1.paginateQuery)({ client: context.client, params })) {
47
+ let scanned = 0;
48
+ /**
49
+ * Unbounded: this is a generator that streams and never accumulates, and
50
+ * `limit` already returns early, so the caller's own bound is what stops the
51
+ * read. The shared in-memory cap counts *raw rows pulled*, not
52
+ * filter-matched ones, so leaving it in place turned a caller asking for a
53
+ * handful of rare matches over a large thread into a hard
54
+ * ResultTruncatedError instead of the true (possibly empty) answer.
55
+ */
56
+ for await (const raw of (0, paginate_1.paginateQuery)({
57
+ client: context.client,
58
+ params,
59
+ maxItems: Number.POSITIVE_INFINITY,
60
+ maxIterations: Number.POSITIVE_INFINITY,
61
+ })) {
62
+ scanned += 1;
63
+ if (scanned === constants_1.LIST_SCAN_WARN_THRESHOLD) {
64
+ context.logger.warn('list: scanned a large number of rows without the caller stopping; this read is ' +
65
+ 'deliberately unbounded, so narrow the filter or pass options.limit', { threadId, checkpointNs, scanned });
66
+ }
37
67
  if (limit !== undefined && yielded >= limit)
38
68
  return;
39
- const meta = raw;
40
- if (checkpointId !== undefined && meta.checkpointId !== checkpointId)
69
+ const meta = (0, item_reader_1.narrowMetaItem)(raw);
70
+ if (!meta) {
71
+ context.logger.warn('list: skipped a row that is not a checkpoint meta item', {
72
+ sortKey: raw.SK,
73
+ });
41
74
  continue;
42
- if (before !== undefined && meta.checkpointId >= before)
75
+ }
76
+ if (!passesKeyFilters(meta, checkpointId, before))
43
77
  continue;
44
78
  if (!(await passesMetadataFilter(context, meta, filter)))
45
79
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/list.ts"],"names":[],"mappings":";;;AAGA,6DAA+D;AAC/D,mDAAqD;AACrD,2DAA4D;AAC5D,2DAA2E;AAC3E,yDAAuD;AACvD,2CAAmE;AACnE,6CAAoD;AAIpD,kDAAkD;AAClD,SAAS,eAAe,CAAC,OAA+B;IAKtD,OAAO;QACL,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAmC;QAC1E,MAAM,EAAE,OAAO,EAAE,MAAiD;QAClE,KAAK,EAAE,OAAO,EAAE,KAAK;KACtB,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,KAAK,UAAU,oBAAoB,CACjC,OAA4B,EAC5B,IAAwB,EACxB,MAA+C;IAE/C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAA,0BAAY,EAAC,OAAO,EAAE,IAAI,CAAC,CAAgC,CAAC;IACpF,OAAO,IAAA,4BAAa,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACI,KAAK,SAAS,CAAC,iBACpB,OAA4B,EAC5B,MAAsB,EACtB,OAA+B;IAE/B,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,IAAA,+BAAgB,EAAC,MAAM,CAAC,CAAC;IAC1E,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,IAAA,uBAAe,EAC5B,OAAO,CAAC,SAAS,EACjB,IAAA,mBAAY,EAAC,QAAQ,CAAC,EACtB,IAAA,wBAAiB,EAAC,YAAY,CAAC,CAChC,CAAC;IACF,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,KAAK,EAAE,MAAM,GAAG,IAAI,IAAA,wBAAa,EAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QAC1E,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK;YAAE,OAAO;QACpD,MAAM,IAAI,GAAG,GAAyB,CAAC;QACvC,IAAI,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,KAAK,YAAY;YAAE,SAAS;QAC/E,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,YAAY,IAAI,MAAM;YAAE,SAAS;QAClE,IAAI,CAAC,CAAC,MAAM,oBAAoB,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;YAAE,SAAS;QACnE,MAAM,KAAK,GAAG,MAAM,IAAA,wBAAa,EAAC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;QACzE,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,KAAK,CAAC;YACZ,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/list.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"put-writes.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AASpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAoF7D;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
1
+ {"version":3,"file":"put-writes.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAWpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AA2E7D;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -1,44 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.putWrites = putWrites;
4
- const node_crypto_1 = require("node:crypto");
5
4
  const descriptor_keys_1 = require("../../shared/codec/descriptor-keys");
6
5
  const orphans_1 = require("../../shared/codec/s3/orphans");
6
+ const conditional_put_1 = require("../../shared/dynamodb/conditional-put");
7
7
  const retry_1 = require("../../shared/dynamodb/retry");
8
8
  const errors_1 = require("../../shared/errors/errors");
9
+ const ulid_1 = require("../../shared/ulid");
9
10
  const ttl_1 = require("../../shared/validation/ttl");
10
11
  const configurable_1 = require("../internal/configurable");
11
12
  const item_writer_1 = require("../internal/item-writer");
12
13
  const special_write_cleanup_1 = require("../internal/special-write-cleanup");
13
14
  const validation_1 = require("../internal/validation");
15
+ const write_guard_1 = require("../internal/write-guard");
14
16
  /**
15
- * True when the guard rejected a write — NOT evidence a competitor won: a
16
- * `PutCommand` retried after its response was lost can re-hit its own
17
- * committed row and fail identically; the two cases are indistinguishable.
17
+ * Stamps each `putWrites` call, serving two purposes at once. It nonces every
18
+ * S3 upload, so a repeated write never shares an object with an earlier
19
+ * attempt. And because ULIDs are lexicographically time-ordered — and this
20
+ * factory is strictly monotonic even within a single millisecond — it lets the
21
+ * read side identify the *earliest* call that wrote a given channel (see
22
+ * `dropSupersededWrites`). A random UUID nonces just as well but carries no
23
+ * ordering, which would leave that choice arbitrary.
18
24
  */
19
- function isConditionalCheckFailed(error) {
20
- return error.name === 'ConditionalCheckFailedException';
21
- }
25
+ const nextWriteGroup = (0, ulid_1.createUlidFactory)();
22
26
  /** Best-effort delete `items`' offloaded S3 objects, if an offloader is configured. */
23
27
  async function cleanUpItems(context, items) {
24
28
  if (!context.offloader)
25
29
  return;
26
30
  await (0, orphans_1.cleanUpS3Orphans)(context.offloader, (0, descriptor_keys_1.collectS3Keys)(items.map((item) => item.value)), 'putWrites', context.logger);
27
31
  }
28
- /**
29
- * Dedup writes to the same special channel (last-write-wins) before upload,
30
- * so a discarded duplicate is never uploaded. A regular write's index comes
31
- * from its position here (post-dedup), not the caller's raw array — stable
32
- * across retries of an identical array, which is what first-write-wins needs.
33
- */
34
- function dedupeWritesByIndex(writes) {
35
- const byIndex = new Map();
36
- writes.forEach((write, positional) => {
37
- const index = (0, item_writer_1.resolveWriteIndex)(write[0], positional);
38
- byIndex.set(index, write);
39
- });
40
- return [...byIndex.values()];
41
- }
42
32
  /**
43
33
  * Write regular items with a first-write-wins guard. Every `PutCommand` fully
44
34
  * settles (`Promise.allSettled`) before this resolves and never rejects; a
@@ -51,13 +41,14 @@ async function writeRegularItems(context, items) {
51
41
  TableName: context.tableName,
52
42
  Item: item,
53
43
  ConditionExpression: 'attribute_not_exists(PK)',
44
+ ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
54
45
  }))));
55
46
  results.forEach((result, index) => {
56
47
  if (result.status === 'fulfilled')
57
48
  return;
58
49
  const reason = result.reason;
59
- if (isConditionalCheckFailed(reason)) {
60
- context.logger.debug('putWrites: lost the guard', { sortKey: items[index].SK });
50
+ if ((0, conditional_put_1.isConditionalCheckFailed)(reason)) {
51
+ (0, write_guard_1.reportGuardRejection)(context, items[index], reason);
61
52
  return;
62
53
  }
63
54
  failed.push(items[index]);
@@ -84,7 +75,7 @@ async function putWrites(context, config, writes, taskId) {
84
75
  if (writes.length === 0)
85
76
  return;
86
77
  const ttlTimestamp = context.ttl ? (0, ttl_1.calculateTtlTimestamp)(context.ttl) : undefined;
87
- const items = await (0, item_writer_1.buildWriteItems)(context, threadId, checkpointNs, checkpointId, taskId, dedupeWritesByIndex(writes), (0, node_crypto_1.randomUUID)(), ttlTimestamp);
78
+ const items = await (0, item_writer_1.buildWriteItems)(context, threadId, checkpointNs, checkpointId, taskId, writes, nextWriteGroup(), ttlTimestamp);
88
79
  const special = items.filter((item) => item.index < 0);
89
80
  const regular = items.filter((item) => item.index >= 0);
90
81
  const [specialError, regularOutcome] = await Promise.all([
@@ -1 +1 @@
1
- {"version":3,"file":"put-writes.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"put-writes.js","sourceRoot":"","sources":["../../../src/checkpointer/actions/put-writes.ts"],"names":[],"mappings":";;;AAGA,wEAAmE;AACnE,2DAAiE;AACjE,2EAAiF;AACjF,uDAAgE;AAChE,uDAA6D;AAC7D,4CAAsD;AACtD,qDAAoE;AACpE,2DAA4D;AAC5D,yDAA0D;AAE1D,6EAAiF;AACjF,uDAAwD;AACxD,yDAA+D;AAG/D;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,IAAA,wBAAiB,GAAE,CAAC;AAE3C,uFAAuF;AACvF,KAAK,UAAU,YAAY,CACzB,OAA4B,EAC5B,KAA4B;IAE5B,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAO;IAC/B,MAAM,IAAA,0BAAgB,EACpB,OAAO,CAAC,SAAS,EACjB,IAAA,+BAAa,EAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAC9C,WAAW,EACX,OAAO,CAAC,MAAM,CACf,CAAC;AACJ,CAAC;AAWD;;;;GAIG;AACH,KAAK,UAAU,iBAAiB,CAC9B,OAA4B,EAC5B,KAA4B;IAE5B,MAAM,MAAM,GAA0B,EAAE,CAAC;IACzC,IAAI,KAAwB,CAAC;IAC7B,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACjB,IAAA,yBAAiB,EAAC,GAAG,EAAE,CACrB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;QACjB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,IAAI;QACV,mBAAmB,EAAE,0BAA0B;QAC/C,mCAAmC,EAAE,SAAS;KAC/C,CAAC,CACH,CACF,CACF,CAAC;IACF,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QAChC,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW;YAAE,OAAO;QAC1C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAe,CAAC;QACtC,IAAI,IAAA,0CAAwB,EAAC,MAAM,CAAC,EAAE,CAAC;YACrC,IAAA,kCAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;YACpD,OAAO;QACT,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1B,KAAK,GAAG,KAAK,IAAI,MAAM,CAAC;IAC1B,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC;AACD;;;;;;;;;GASG;AACI,KAAK,oBACV,OAA4B,EAC5B,MAAsB,EACtB,MAAsB,EACtB,MAAc;IAEd,IAAA,2BAAc,EAAC,MAAM,CAAC,CAAC;IACvB,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,IAAA,+BAAgB,EAAC,MAAM,CAAC,CAAC;IAC1E,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,IAAI,wBAAe,CAAC,2CAA2C,EAAE,eAAe,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAChC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAA,2BAAqB,EAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClF,MAAM,KAAK,GAAG,MAAM,IAAA,6BAAe,EACjC,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,MAAM,EACN,cAAc,EAAE,EAChB,YAAY,CACb,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;IACxD,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACvD,IAAA,oDAA4B,EAAC,OAAO,EAAE,OAAO,CAAC;QAC9C,iBAAiB,CAAC,OAAO,EAAE,OAAO,CAAC;KACpC,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,YAAY,IAAI,cAAc,CAAC,KAAK,CAAC;IACxD,IAAI,CAAC,UAAU;QAAE,OAAO;IACxB,MAAM,YAAY,CAAC,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACnD,MAAM,UAAU,CAAC;AACnB,CAAC"}
@@ -1,10 +1,55 @@
1
1
  import type { Checkpoint, CheckpointMetadata, CheckpointPendingWrite } from '@langchain/langgraph-checkpoint';
2
+ import type { DocItem } from '../../shared/dynamodb/types';
2
3
  import type { CheckpointMetaItem, CheckpointPayloadItem, CheckpointWriteItem } from '../types';
3
4
  import type { CheckpointerContext } from './setup';
5
+ /**
6
+ * Narrow a raw row to a {@link CheckpointMetaItem}, or `undefined` for a row
7
+ * that merely shares the `META#` sort-key prefix. Replaces an unchecked cast
8
+ * at the one boundary where a row may not have been written by this adapter.
9
+ */
10
+ export declare function narrowMetaItem(raw: DocItem): CheckpointMetaItem | undefined;
4
11
  /** Decode the checkpoint stored in a PAYLOAD item. */
5
12
  export declare function readCheckpoint(context: CheckpointerContext, item: CheckpointPayloadItem): Promise<Checkpoint>;
6
13
  /** Decode the metadata stored in a META item. */
7
14
  export declare function readMetadata(context: CheckpointerContext, item: CheckpointMetaItem): Promise<CheckpointMetadata>;
15
+ /**
16
+ * Drop rows a later `putWrites` call added for a task and channel an earlier
17
+ * call had already committed.
18
+ *
19
+ * A regular write's index is its position in the caller's array, so a retried
20
+ * task whose write mix changed can place an already-committed channel at a
21
+ * second index — a new row, which would replay that channel's value twice. For
22
+ * an accumulating channel (a `messages` add-reducer, a Topic) that
23
+ * double-counts. Every row carries the `writeGroup` of the call that wrote it,
24
+ * which distinguishes that case from a channel a *single* call legitimately
25
+ * wrote more than once (a task emitting two Sends), where both values must
26
+ * survive.
27
+ *
28
+ * Write groups are time-ordered ULIDs, so the smallest one for a `(task,
29
+ * channel)` pair identifies the earliest committed call — first-write-wins,
30
+ * the same rule the write-side guard applies, extended to the case the guard
31
+ * cannot see. Selecting by *order encountered* would not do: rows arrive
32
+ * sorted by index, and a retry emitting fewer writes moves an
33
+ * already-committed channel to a smaller index, where it sorts ahead of the
34
+ * original row and would hand back the later call's value instead. The
35
+ * superseded row stays in DynamoDB and is removed by `deleteThread` or TTL
36
+ * like any other.
37
+ *
38
+ * Identity is `(task, channel, occurrence)`, not `(task, channel)`. Keying on
39
+ * the channel alone treated *any* second row for a channel as a superseding
40
+ * duplicate — including one a retry added at an occurrence no earlier call had
41
+ * ever written, which the write-side guard accepted cleanly. That row was then
42
+ * discarded on read, silently returning fewer values than were written, with
43
+ * `putWrites()` having reported success. For that specific shape — a retry
44
+ * that legitimately emits a channel *more* often than the original call —
45
+ * keeping the occurrence restores the upstream outcome: `MemorySaver` keys
46
+ * first-write-wins on `(taskId, idx)`, so a grown retry keeps both values
47
+ * there too. That is not a claim of parity in general: a retry that reorders
48
+ * or shrinks its writes is still resolved to the earliest call per `(task,
49
+ * channel, occurrence)` as above, which is what keeps an accumulating channel
50
+ * from being double-counted — a case `MemorySaver` does not have to handle.
51
+ */
52
+ export declare function dropSupersededWrites(items: CheckpointWriteItem[]): CheckpointWriteItem[];
8
53
  /** Decode WRITE items into `[taskId, channel, value]` pending-write tuples. */
9
54
  export declare function toPendingWrites(context: CheckpointerContext, items: CheckpointWriteItem[]): Promise<CheckpointPendingWrite[]>;
10
55
  //# sourceMappingURL=item-reader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"item-reader.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AAGzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,sDAAsD;AACtD,wBAAsB,cAAc,CAClC,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,iDAAiD;AACjD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED,+EAA+E;AAC/E,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,mBAAmB,EAAE,GAC3B,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAQnC"}
1
+ {"version":3,"file":"item-reader.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-reader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,iCAAiC,CAAC;AAGzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAM3E;AAED,sDAAsD;AACtD,wBAAsB,cAAc,CAClC,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,iDAAiD;AACjD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,mBAAmB,EAAE,CAUxF;AAED,+EAA+E;AAC/E,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,mBAAmB,EAAE,GAC3B,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAQnC"}
@@ -1,10 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.narrowMetaItem = narrowMetaItem;
3
4
  exports.readCheckpoint = readCheckpoint;
4
5
  exports.readMetadata = readMetadata;
6
+ exports.dropSupersededWrites = dropSupersededWrites;
5
7
  exports.toPendingWrites = toPendingWrites;
6
8
  const codec_1 = require("../../shared/codec/codec");
7
9
  const item_writer_1 = require("./item-writer");
10
+ /**
11
+ * Narrow a raw row to a {@link CheckpointMetaItem}, or `undefined` for a row
12
+ * that merely shares the `META#` sort-key prefix. Replaces an unchecked cast
13
+ * at the one boundary where a row may not have been written by this adapter.
14
+ */
15
+ function narrowMetaItem(raw) {
16
+ return typeof raw.checkpointId === 'string' &&
17
+ typeof raw.checkpointNs === 'string' &&
18
+ raw.metadata !== undefined
19
+ ? raw
20
+ : undefined;
21
+ }
8
22
  /** Decode the checkpoint stored in a PAYLOAD item. */
9
23
  async function readCheckpoint(context, item) {
10
24
  return (0, codec_1.decodePayload)(item.checkpoint, (0, item_writer_1.codecDeps)(context));
@@ -13,11 +27,59 @@ async function readCheckpoint(context, item) {
13
27
  async function readMetadata(context, item) {
14
28
  return (0, codec_1.decodePayload)(item.metadata, (0, item_writer_1.codecDeps)(context));
15
29
  }
30
+ /**
31
+ * Drop rows a later `putWrites` call added for a task and channel an earlier
32
+ * call had already committed.
33
+ *
34
+ * A regular write's index is its position in the caller's array, so a retried
35
+ * task whose write mix changed can place an already-committed channel at a
36
+ * second index — a new row, which would replay that channel's value twice. For
37
+ * an accumulating channel (a `messages` add-reducer, a Topic) that
38
+ * double-counts. Every row carries the `writeGroup` of the call that wrote it,
39
+ * which distinguishes that case from a channel a *single* call legitimately
40
+ * wrote more than once (a task emitting two Sends), where both values must
41
+ * survive.
42
+ *
43
+ * Write groups are time-ordered ULIDs, so the smallest one for a `(task,
44
+ * channel)` pair identifies the earliest committed call — first-write-wins,
45
+ * the same rule the write-side guard applies, extended to the case the guard
46
+ * cannot see. Selecting by *order encountered* would not do: rows arrive
47
+ * sorted by index, and a retry emitting fewer writes moves an
48
+ * already-committed channel to a smaller index, where it sorts ahead of the
49
+ * original row and would hand back the later call's value instead. The
50
+ * superseded row stays in DynamoDB and is removed by `deleteThread` or TTL
51
+ * like any other.
52
+ *
53
+ * Identity is `(task, channel, occurrence)`, not `(task, channel)`. Keying on
54
+ * the channel alone treated *any* second row for a channel as a superseding
55
+ * duplicate — including one a retry added at an occurrence no earlier call had
56
+ * ever written, which the write-side guard accepted cleanly. That row was then
57
+ * discarded on read, silently returning fewer values than were written, with
58
+ * `putWrites()` having reported success. For that specific shape — a retry
59
+ * that legitimately emits a channel *more* often than the original call —
60
+ * keeping the occurrence restores the upstream outcome: `MemorySaver` keys
61
+ * first-write-wins on `(taskId, idx)`, so a grown retry keeps both values
62
+ * there too. That is not a claim of parity in general: a retry that reorders
63
+ * or shrinks its writes is still resolved to the earliest call per `(task,
64
+ * channel, occurrence)` as above, which is what keeps an accumulating channel
65
+ * from being double-counted — a case `MemorySaver` does not have to handle.
66
+ */
67
+ function dropSupersededWrites(items) {
68
+ const identity = (item) => JSON.stringify([item.taskId, item.channel, item.occurrence ?? 0]);
69
+ const earliestGroup = new Map();
70
+ for (const item of items) {
71
+ const id = identity(item);
72
+ const seen = earliestGroup.get(id);
73
+ if (seen === undefined || item.writeGroup < seen)
74
+ earliestGroup.set(id, item.writeGroup);
75
+ }
76
+ return items.filter((item) => earliestGroup.get(identity(item)) === item.writeGroup);
77
+ }
16
78
  /** Decode WRITE items into `[taskId, channel, value]` pending-write tuples. */
17
79
  async function toPendingWrites(context, items) {
18
80
  const deps = (0, item_writer_1.codecDeps)(context);
19
81
  const pending = [];
20
- for (const item of items) {
82
+ for (const item of dropSupersededWrites(items)) {
21
83
  const value = await (0, codec_1.decodePayload)(item.value, deps);
22
84
  pending.push([item.taskId, item.channel, value]);
23
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"item-reader.js","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-reader.ts"],"names":[],"mappings":";;;;;AAMA,oDAAyD;AAEzD,+CAA0C;AAG1C,sDAAsD;AAC/C,KAAK,yBACV,OAA4B,EAC5B,IAA2B;IAE3B,OAAO,IAAA,qBAAa,EAAa,IAAI,CAAC,UAAU,EAAE,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,iDAAiD;AAC1C,KAAK,uBACV,OAA4B,EAC5B,IAAwB;IAExB,OAAO,IAAA,qBAAa,EAAqB,IAAI,CAAC,QAAQ,EAAE,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,+EAA+E;AACxE,KAAK,0BACV,OAA4B,EAC5B,KAA4B;IAE5B,MAAM,IAAI,GAAG,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC;IAChC,MAAM,OAAO,GAA6B,EAAE,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"item-reader.js","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-reader.ts"],"names":[],"mappings":";;;;;;;AAMA,oDAAyD;AAGzD,+CAA0C;AAG1C;;;;GAIG;AACH,wBAA+B,GAAY;IACzC,OAAO,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ;QACzC,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ;QACpC,GAAG,CAAC,QAAQ,KAAK,SAAS;QAC1B,CAAC,CAAE,GAA0B;QAC7B,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED,sDAAsD;AAC/C,KAAK,yBACV,OAA4B,EAC5B,IAA2B;IAE3B,OAAO,IAAA,qBAAa,EAAa,IAAI,CAAC,UAAU,EAAE,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,iDAAiD;AAC1C,KAAK,uBACV,OAA4B,EAC5B,IAAwB;IAExB,OAAO,IAAA,qBAAa,EAAqB,IAAI,CAAC,QAAQ,EAAE,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,8BAAqC,KAA4B;IAC/D,MAAM,QAAQ,GAAG,CAAC,IAAyB,EAAU,EAAE,CACrD,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1B,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,GAAG,IAAI;YAAE,aAAa,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC;AACvF,CAAC;AAED,+EAA+E;AACxE,KAAK,0BACV,OAA4B,EAC5B,KAA4B;IAE5B,MAAM,IAAI,GAAG,IAAA,uBAAS,EAAC,OAAO,CAAC,CAAC;IAChC,MAAM,OAAO,GAA6B,EAAE,CAAC;IAC7C,KAAK,MAAM,IAAI,IAAI,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,IAAA,qBAAa,EAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -4,14 +4,6 @@ import type { CheckpointMetaItem, CheckpointPayloadItem, CheckpointWriteItem } f
4
4
  import type { CheckpointerContext } from './setup';
5
5
  /** Map a context to the codec collaborators. */
6
6
  export declare function codecDeps(context: CheckpointerContext): CodecDeps;
7
- /**
8
- * Resolve a write's DynamoDB sort-key index: WRITES_IDX_MAP's slot for a
9
- * known special channel, else its position. `Object.hasOwn` guards against
10
- * WRITES_IDX_MAP's own `Object.prototype` chain — a channel literally named
11
- * `constructor`/`toString`/etc. must fall through to `positional`, not
12
- * resolve to an inherited function reference.
13
- */
14
- export declare function resolveWriteIndex(channel: string, positional: number): number;
15
7
  /** Encode a checkpoint + metadata into its META and PAYLOAD items. */
16
8
  export declare function buildCheckpointItems(context: CheckpointerContext, threadId: string, checkpointNs: string, checkpoint: Checkpoint, metadata: CheckpointMetadata, parentCheckpointId?: string, ttlTimestamp?: number): Promise<{
17
9
  meta: CheckpointMetaItem;
@@ -1 +1 @@
1
- {"version":3,"file":"item-writer.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpG,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAEjE;AAOD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,kBAAkB,EAC5B,kBAAkB,CAAC,EAAE,MAAM,EAC3B,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAC,CAwBvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EAAE,EACtB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA2BhC"}
1
+ {"version":3,"file":"item-writer.d.ts","sourceRoot":"","sources":["../../../src/checkpointer/internal/item-writer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpG,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAE/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAGnD,gDAAgD;AAChD,wBAAgB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAEjE;AAOD,sEAAsE;AACtE,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,kBAAkB,EAC5B,kBAAkB,CAAC,EAAE,MAAM,EAC3B,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAC,CAwBvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EAAE,EACtB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAiChC"}