deepline 0.1.295 → 0.1.296

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.
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
155
155
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
156
156
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
157
157
  // Operators use the checkout-local deepline-admin binary instead.
158
- version: '0.1.295',
158
+ version: '0.1.296',
159
159
  contracts: {
160
160
  api: {
161
161
  name: 'sdk-http-api',
@@ -35,6 +35,7 @@ import type { PlayQueueHint } from './governor/rate-state-backend';
35
35
  import type { MapRowOutcome } from './durability-store';
36
36
  import { stringifyPostgresJson } from './postgres-json';
37
37
  import { RUNTIME_ROW_OUTPUT_NOT_JSON_SERIALIZABLE } from './runtime-sheet-row-transition';
38
+ import { RuntimeSheetRowWriter } from './runtime-sheet-row-writer';
38
39
  import type { WorkReceiptFailureKind } from './work-receipts';
39
40
  import {
40
41
  completedMapRowOutcome,
@@ -300,9 +301,8 @@ const MAP_FRAME_FLUSH_ROW_INTERVAL = 100;
300
301
  /** Executed-row sheet persistence chunking: rows AND bytes, whichever first. */
301
302
  const MAP_PERSIST_CHUNK_ROWS = 2_000;
302
303
  const MAP_PERSIST_CHUNK_BYTES = 8 * 1024 * 1024;
303
- const MAP_INCREMENTAL_PERSIST_CHUNK_ROWS = 100;
304
- const MAP_INCREMENTAL_PERSIST_CHUNK_BYTES = 1 * 1024 * 1024;
305
- const MAP_INCREMENTAL_PERSIST_INTERVAL_MS = 100;
304
+ const MAP_ROW_WRITER_BUFFER_BYTES = 16 * 1024 * 1024;
305
+ const MAP_ROW_WRITER_MAX_FLUSH_MS = 100;
306
306
  const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
307
307
  // Tool scheduling lanes wait at most this long for same-lane row continuations.
308
308
  // The window is fixed from the first item and never resets on later arrivals.
@@ -778,123 +778,117 @@ type FieldMapRunResult = {
778
778
  failedRows: PersistableMapRow[];
779
779
  };
780
780
 
781
- type IncrementalMapRowPersistence = {
782
- persistRows: (rows: PersistableMapRow[]) => Promise<void>;
781
+ type RuntimeMapRowPersistence = {
782
+ persistRows: (rows: PersistableMapRow[]) => {
783
+ admitted: Promise<void>;
784
+ committed: Promise<void>;
785
+ };
783
786
  isPersisted: (row: PersistableMapRow) => boolean;
787
+ checkpoint: (updates: PlayRowUpdate[]) => Promise<void>;
784
788
  flush: () => Promise<void>;
785
789
  };
786
790
 
787
- function createIncrementalMapRowPersistence(
791
+ function createRuntimeMapRowPersistence(
788
792
  persistRows: (rows: PersistableMapRow[]) => Promise<void>,
789
- ): IncrementalMapRowPersistence {
793
+ options?: {
794
+ persistCheckpoint?: (updates: PlayRowUpdate[]) => Promise<void>;
795
+ onFailure?: (error: unknown) => void;
796
+ },
797
+ ): RuntimeMapRowPersistence {
790
798
  const persisted = new Set<string>();
791
799
  const queued = new Set<string>();
792
- let pendingRows: PersistableMapRow[] = [];
793
- let pendingBytes = 0;
794
- let pendingResolvers: Array<{
795
- resolve: () => void;
796
- reject: (error: unknown) => void;
797
- }> = [];
798
- let scheduledTimer: ReturnType<typeof setTimeout> | null = null;
799
- let flushChain: Promise<void> = Promise.resolve();
800
-
801
- const clearScheduledTimer = (): void => {
802
- if (scheduledTimer) {
803
- clearTimeout(scheduledTimer);
804
- scheduledTimer = null;
805
- }
806
- };
800
+ const writer = new RuntimeSheetRowWriter<PersistableMapRow, PlayRowUpdate>({
801
+ terminalKey: (row) => {
802
+ const identity = persistableMapRowIdentity(row);
803
+ if (!identity) {
804
+ throw new Error('Runtime Sheet terminal row is missing an identity.');
805
+ }
806
+ return identity;
807
+ },
808
+ checkpointKey: (update) => `key:${update.key}`,
809
+ mergeCheckpointUpdates: (current, incoming) => ({
810
+ ...current,
811
+ ...incoming,
812
+ dataPatch: {
813
+ ...(current.dataPatch ?? {}),
814
+ ...(incoming.dataPatch ?? {}),
815
+ },
816
+ cellMetaPatch: {
817
+ ...(current.cellMetaPatch ?? {}),
818
+ ...(incoming.cellMetaPatch ?? {}),
819
+ },
820
+ }),
821
+ estimateTerminalBytes: persistableMapRowBytes,
822
+ estimateCheckpointBytes: (update) => JSON.stringify(update).length,
823
+ maxBatchRows: MAP_PERSIST_CHUNK_ROWS,
824
+ maxBatchBytes: MAP_PERSIST_CHUNK_BYTES,
825
+ maxBufferedBytes: MAP_ROW_WRITER_BUFFER_BYTES,
826
+ maxFlushMs: MAP_ROW_WRITER_MAX_FLUSH_MS,
827
+ onFailure: options?.onFailure,
828
+ writeBatch: async (batch) => {
829
+ if (batch.kind === 'terminal') {
830
+ await persistRows([...batch.rows]);
831
+ return { committed: batch.rows.length };
832
+ }
833
+ if (batch.updates.length > 0) {
834
+ await options?.persistCheckpoint?.([...batch.updates]);
835
+ }
836
+ return { committed: batch.updates.length };
837
+ },
838
+ });
807
839
 
808
- const flushPendingRows = (): Promise<void> => {
809
- clearScheduledTimer();
810
- const rows = pendingRows;
811
- const resolvers = pendingResolvers;
812
- pendingRows = [];
813
- pendingBytes = 0;
814
- pendingResolvers = [];
815
- if (rows.length === 0) {
816
- resolvers.forEach(({ resolve }) => resolve());
817
- return flushChain;
818
- }
819
- flushChain = flushChain
820
- .then(async () => {
821
- await persistRows(rows);
822
- for (const row of rows) {
823
- const identity = persistableMapRowIdentity(row);
824
- if (identity) {
825
- persisted.add(identity);
826
- queued.delete(identity);
827
- }
840
+ return {
841
+ persistRows: (rows) => {
842
+ const admissions: Promise<void>[] = [];
843
+ const commits: Promise<void>[] = [];
844
+ const clearQueuedState = (identity: string | null) => {
845
+ if (identity) {
846
+ queued.delete(identity);
828
847
  }
829
- })
830
- .then(
831
- () => {
832
- resolvers.forEach(({ resolve }) => resolve());
833
- },
834
- (error) => {
835
- for (const row of rows) {
836
- const identity = persistableMapRowIdentity(row);
837
- if (identity) queued.delete(identity);
838
- }
839
- resolvers.forEach(({ reject }) => reject(error));
840
- throw error;
841
- },
842
- );
843
- return flushChain;
844
- };
845
-
846
- const scheduleFlush = (): void => {
847
- if (scheduledTimer) return;
848
- scheduledTimer = setTimeout(() => {
849
- scheduledTimer = null;
850
- void flushPendingRows().catch(() => {
851
- // Each queued caller is rejected by flushPendingRows; this scheduled
852
- // fire-and-forget path must not create a second unhandled rejection.
853
- });
854
- }, MAP_INCREMENTAL_PERSIST_INTERVAL_MS);
855
- };
848
+ };
856
849
 
857
- return {
858
- persistRows: async (rows) => {
859
- const freshRows: PersistableMapRow[] = [];
860
850
  for (const row of rows) {
861
851
  const identity = persistableMapRowIdentity(row);
862
852
  if (identity && (persisted.has(identity) || queued.has(identity))) {
863
853
  continue;
864
854
  }
865
855
  if (identity) queued.add(identity);
866
- freshRows.push(row);
867
- }
868
- if (freshRows.length === 0) return;
869
- await new Promise<void>((resolve, reject) => {
870
- pendingRows.push(...freshRows);
871
- pendingBytes += freshRows.reduce(
872
- (total, row) => total + persistableMapRowBytes(row),
873
- 0,
856
+ const settlement = writer.settle(row);
857
+ admissions.push(
858
+ settlement.admitted.catch((error) => {
859
+ clearQueuedState(identity);
860
+ throw error;
861
+ }),
874
862
  );
875
- pendingResolvers.push({ resolve, reject });
876
- if (
877
- pendingRows.length >= MAP_INCREMENTAL_PERSIST_CHUNK_ROWS ||
878
- pendingBytes >= MAP_INCREMENTAL_PERSIST_CHUNK_BYTES
879
- ) {
880
- void flushPendingRows().catch(() => {
881
- // Each queued caller is rejected by flushPendingRows.
882
- });
883
- return;
884
- }
885
- scheduleFlush();
886
- });
863
+ commits.push(
864
+ settlement.committed.then(
865
+ () => {
866
+ if (identity) {
867
+ persisted.add(identity);
868
+ clearQueuedState(identity);
869
+ }
870
+ },
871
+ (error) => {
872
+ clearQueuedState(identity);
873
+ throw error;
874
+ },
875
+ ),
876
+ );
877
+ }
878
+ return {
879
+ admitted: Promise.all(admissions).then(() => {}),
880
+ committed: Promise.all(commits).then(() => {}),
881
+ };
887
882
  },
888
883
  isPersisted: (row) => {
889
884
  const identity = persistableMapRowIdentity(row);
890
885
  return identity ? persisted.has(identity) : false;
891
886
  },
887
+ checkpoint: async (updates) => {
888
+ await writer.checkpoint(updates);
889
+ },
892
890
  flush: async () => {
893
- if (pendingRows.length > 0) {
894
- clearScheduledTimer();
895
- await flushPendingRows();
896
- }
897
- await flushChain;
891
+ await writer.finish();
898
892
  },
899
893
  };
900
894
  }
@@ -1310,6 +1304,13 @@ export class PlayContextImpl {
1310
1304
  * completed/cached cell meta — cross-run reuse decisions read that meta.
1311
1305
  */
1312
1306
  private activeMapCellMeta: Map<string, Record<string, unknown>> | null = null;
1307
+ /**
1308
+ * Latest partial row patch per key for the active map. Terminal rows are
1309
+ * removed after their Runtime Sheet settlement commits; any remainder is the
1310
+ * exact partial-cell checkpoint that must cross the durability barrier before
1311
+ * an integration-event suspension is published.
1312
+ */
1313
+ private activeMapCheckpointUpdates: Map<string, PlayRowUpdate> | null = null;
1313
1314
  private lastProgressHeartbeatAt = 0;
1314
1315
  private pendingRowEventBoundaries: Array<{
1315
1316
  boundaryId: string;
@@ -1642,6 +1643,33 @@ export class PlayContextImpl {
1642
1643
  : { ...update.cellMetaPatch },
1643
1644
  );
1644
1645
  }
1646
+ if (key && this.activeMapCheckpointUpdates) {
1647
+ const checkpointKey = `${tableNamespace ?? ''}\u0000${key}`;
1648
+ const existing = this.activeMapCheckpointUpdates.get(checkpointKey);
1649
+ const next: PlayRowUpdate = {
1650
+ ...(existing ?? {
1651
+ key,
1652
+ rowId: update.rowId,
1653
+ tableNamespace,
1654
+ }),
1655
+ key,
1656
+ rowId: update.rowId,
1657
+ tableNamespace,
1658
+ ...(update.status !== undefined ? { status: update.status } : {}),
1659
+ ...(update.stage !== undefined ? { stage: update.stage } : {}),
1660
+ ...(update.provider !== undefined ? { provider: update.provider } : {}),
1661
+ ...(update.error !== undefined ? { error: update.error } : {}),
1662
+ dataPatch: {
1663
+ ...(existing?.dataPatch ?? {}),
1664
+ ...(update.dataPatch ?? {}),
1665
+ },
1666
+ cellMetaPatch: {
1667
+ ...(existing?.cellMetaPatch ?? {}),
1668
+ ...(update.cellMetaPatch ?? {}),
1669
+ },
1670
+ };
1671
+ this.activeMapCheckpointUpdates.set(checkpointKey, next);
1672
+ }
1645
1673
  if (!key || !this.#options.onRowUpdate) {
1646
1674
  return;
1647
1675
  }
@@ -1652,6 +1680,13 @@ export class PlayContextImpl {
1652
1680
  });
1653
1681
  }
1654
1682
 
1683
+ private clearActiveMapCheckpointUpdate(
1684
+ key: string,
1685
+ tableNamespace: string,
1686
+ ): void {
1687
+ this.activeMapCheckpointUpdates?.delete(`${tableNamespace}\u0000${key}`);
1688
+ }
1689
+
1655
1690
  private emitExecutionEvent(event: PlayExecutionEvent): void {
1656
1691
  if (!this.#options.onExecutionEvent) {
1657
1692
  return;
@@ -3969,32 +4004,23 @@ export class PlayContextImpl {
3969
4004
  const flushChunk = async () => {
3970
4005
  if (chunk.length === 0) return;
3971
4006
  try {
3972
- const sheetFlushLease =
3973
- await this.resourceGovernor.acquireSheetFlush({
3974
- estimatedBytes: chunkBytes,
3975
- rowCount: chunk.length,
3976
- });
3977
4007
  const flushStartedAt = Date.now();
3978
- try {
3979
- await this.#options.onMapRowsCompleted!({
3980
- playName: this.currentPlayName,
3981
- playId: this.currentExecutionScope.logical.playId,
3982
- runId: this.currentRunId,
3983
- executorToken: this.#options.executorToken,
3984
- tableNamespace: resolvedTableNamespace,
3985
- rows: chunk,
3986
- outputFields: datasetColumnNames.filter((field) =>
3987
- shouldPersistMapCellField(field),
3988
- ),
3989
- staticPipeline: this.currentStaticPipeline ?? null,
3990
- });
3991
- this.resourceGovernor.observe({
3992
- sheetFlushBytes: chunkBytes,
3993
- sheetFlushLatencyMs: Date.now() - flushStartedAt,
3994
- });
3995
- } finally {
3996
- sheetFlushLease.release();
3997
- }
4008
+ await this.#options.onMapRowsCompleted!({
4009
+ playName: this.currentPlayName,
4010
+ playId: this.currentExecutionScope.logical.playId,
4011
+ runId: this.currentRunId,
4012
+ executorToken: this.#options.executorToken,
4013
+ tableNamespace: resolvedTableNamespace,
4014
+ rows: chunk,
4015
+ outputFields: datasetColumnNames.filter((field) =>
4016
+ shouldPersistMapCellField(field),
4017
+ ),
4018
+ staticPipeline: this.currentStaticPipeline ?? null,
4019
+ });
4020
+ this.resourceGovernor.observe({
4021
+ sheetFlushBytes: chunkBytes,
4022
+ sheetFlushLatencyMs: Date.now() - flushStartedAt,
4023
+ });
3998
4024
  } catch (error) {
3999
4025
  tripRuntimePersistenceLatch(this.persistenceLatch, error);
4000
4026
  throw error;
@@ -4142,10 +4168,28 @@ export class PlayContextImpl {
4142
4168
  const rowsToExecute = rowsToExecuteEntries.map((entry) => entry.row);
4143
4169
  const rowsToExecuteMemoryEstimate =
4144
4170
  estimateRuntimeMapRowsMemory(rowsToExecute);
4145
- const incrementalPersistence =
4146
- createIncrementalMapRowPersistence(persistMapRows);
4171
+ const incrementalPersistence = createRuntimeMapRowPersistence(
4172
+ persistMapRows,
4173
+ {
4174
+ persistCheckpoint: this.#options.onMapRowsCheckpoint
4175
+ ? (updates) =>
4176
+ this.#options.onMapRowsCheckpoint!({
4177
+ playName: this.currentPlayName,
4178
+ playId: this.currentExecutionScope.logical.playId,
4179
+ runId: this.currentRunId,
4180
+ executorToken: this.#options.executorToken,
4181
+ tableNamespace: resolvedTableNamespace,
4182
+ updates,
4183
+ staticPipeline: this.currentStaticPipeline ?? null,
4184
+ })
4185
+ : undefined,
4186
+ onFailure: (error) =>
4187
+ tripRuntimePersistenceLatch(this.persistenceLatch, error),
4188
+ },
4189
+ );
4147
4190
 
4148
4191
  this.activeMapCellMeta = new Map();
4192
+ this.activeMapCheckpointUpdates = new Map();
4149
4193
  this.setMapFrame({
4150
4194
  mapInvocationId: mapScope.mapInvocationId,
4151
4195
  mapNodeId: mapScope.mapNodeId ?? null,
@@ -4207,9 +4251,11 @@ export class PlayContextImpl {
4207
4251
  );
4208
4252
  await persistMapRows(unpersistedRows);
4209
4253
  this.activeMapCellMeta = null;
4254
+ this.activeMapCheckpointUpdates = null;
4210
4255
  throw error.cause;
4211
4256
  }
4212
4257
  this.activeMapCellMeta = null;
4258
+ this.activeMapCheckpointUpdates = null;
4213
4259
  throw error;
4214
4260
  }
4215
4261
 
@@ -4238,6 +4284,7 @@ export class PlayContextImpl {
4238
4284
  );
4239
4285
  await persistMapRows(persistRows);
4240
4286
  this.activeMapCellMeta = null;
4287
+ this.activeMapCheckpointUpdates = null;
4241
4288
 
4242
4289
  for (const row of mapResult.completedRows) {
4243
4290
  const materializedRow = this.toMaterializedOutputRow(row.data);
@@ -4669,6 +4716,7 @@ export class PlayContextImpl {
4669
4716
  );
4670
4717
 
4671
4718
  this.activeMapCellMeta = new Map();
4719
+ this.activeMapCheckpointUpdates = new Map();
4672
4720
  const staleCompletionKeys = new Set<string>();
4673
4721
  const persistMapRows = async (rows: PersistableMapRow[]) => {
4674
4722
  if (!this.#options.onMapRowsCompleted || rows.length === 0) {
@@ -4685,38 +4733,28 @@ export class PlayContextImpl {
4685
4733
  const flushChunk = async () => {
4686
4734
  if (chunk.length === 0) return;
4687
4735
  try {
4688
- const sheetFlushLease = await this.resourceGovernor.acquireSheetFlush(
4689
- {
4690
- estimatedBytes: chunkBytes,
4691
- rowCount: chunk.length,
4692
- },
4693
- );
4694
4736
  const flushStartedAt = Date.now();
4695
- try {
4696
- const writeResult = await this.#options.onMapRowsCompleted!({
4697
- playName: this.currentPlayName,
4698
- playId: this.currentExecutionScope.logical.playId,
4699
- runId: this.currentRunId,
4700
- executorToken: this.#options.executorToken,
4701
- tableNamespace: resolvedTableNamespace,
4702
- rows: chunk,
4703
- outputFields: datasetColumnNames.filter((field) =>
4704
- shouldPersistMapCellField(field),
4705
- ),
4706
- staticPipeline: this.currentStaticPipeline ?? null,
4707
- });
4708
- if (writeResult) {
4709
- for (const key of writeResult.staleDroppedKeys ?? []) {
4710
- staleCompletionKeys.add(key);
4711
- }
4737
+ const writeResult = await this.#options.onMapRowsCompleted!({
4738
+ playName: this.currentPlayName,
4739
+ playId: this.currentExecutionScope.logical.playId,
4740
+ runId: this.currentRunId,
4741
+ executorToken: this.#options.executorToken,
4742
+ tableNamespace: resolvedTableNamespace,
4743
+ rows: chunk,
4744
+ outputFields: datasetColumnNames.filter((field) =>
4745
+ shouldPersistMapCellField(field),
4746
+ ),
4747
+ staticPipeline: this.currentStaticPipeline ?? null,
4748
+ });
4749
+ if (writeResult) {
4750
+ for (const key of writeResult.staleDroppedKeys ?? []) {
4751
+ staleCompletionKeys.add(key);
4712
4752
  }
4713
- this.resourceGovernor.observe({
4714
- sheetFlushBytes: chunkBytes,
4715
- sheetFlushLatencyMs: Date.now() - flushStartedAt,
4716
- });
4717
- } finally {
4718
- sheetFlushLease.release();
4719
4753
  }
4754
+ this.resourceGovernor.observe({
4755
+ sheetFlushBytes: chunkBytes,
4756
+ sheetFlushLatencyMs: Date.now() - flushStartedAt,
4757
+ });
4720
4758
  } catch (error) {
4721
4759
  // Output-sheet flush failed: trip the breaker so the dispatch loops
4722
4760
  // stop dispatching new provider calls for the rest of this map.
@@ -4740,7 +4778,22 @@ export class PlayContextImpl {
4740
4778
  await flushChunk();
4741
4779
  };
4742
4780
  const incrementalPersistence = this.#options.onMapRowsCompleted
4743
- ? createIncrementalMapRowPersistence(persistMapRows)
4781
+ ? createRuntimeMapRowPersistence(persistMapRows, {
4782
+ persistCheckpoint: this.#options.onMapRowsCheckpoint
4783
+ ? (updates) =>
4784
+ this.#options.onMapRowsCheckpoint!({
4785
+ playName: this.currentPlayName,
4786
+ playId: this.currentExecutionScope.logical.playId,
4787
+ runId: this.currentRunId,
4788
+ executorToken: this.#options.executorToken,
4789
+ tableNamespace: resolvedTableNamespace,
4790
+ updates,
4791
+ staticPipeline: this.currentStaticPipeline ?? null,
4792
+ })
4793
+ : undefined,
4794
+ onFailure: (error) =>
4795
+ tripRuntimePersistenceLatch(this.persistenceLatch, error),
4796
+ })
4744
4797
  : null;
4745
4798
 
4746
4799
  let mapResult: FieldMapRunResult;
@@ -4785,9 +4838,11 @@ export class PlayContextImpl {
4785
4838
  );
4786
4839
  }
4787
4840
  this.activeMapCellMeta = null;
4841
+ this.activeMapCheckpointUpdates = null;
4788
4842
  throw error.cause;
4789
4843
  }
4790
4844
  this.activeMapCellMeta = null;
4845
+ this.activeMapCheckpointUpdates = null;
4791
4846
  throw error;
4792
4847
  }
4793
4848
  const resultsByKey = new Map<string, Record<string, unknown>>();
@@ -4852,6 +4907,7 @@ export class PlayContextImpl {
4852
4907
  }
4853
4908
  }
4854
4909
  this.activeMapCellMeta = null;
4910
+ this.activeMapCheckpointUpdates = null;
4855
4911
 
4856
4912
  if (staleCompletionKeys.size > 0) {
4857
4913
  results = results.filter((row, index) => {
@@ -5083,7 +5139,7 @@ export class PlayContextImpl {
5083
5139
  progressCompletedOffset?: number;
5084
5140
  progressFailedOffset?: number;
5085
5141
  progressTotalRows?: number;
5086
- incrementalPersistence?: IncrementalMapRowPersistence | null;
5142
+ incrementalPersistence?: RuntimeMapRowPersistence | null;
5087
5143
  },
5088
5144
  ): Promise<FieldMapRunResult> {
5089
5145
  const fieldEntries = Object.entries(definition);
@@ -5135,18 +5191,17 @@ export class PlayContextImpl {
5135
5191
  const enqueueIncrementalPersist = (
5136
5192
  row: PersistableMapRow,
5137
5193
  onCommitted: () => void,
5138
- ): void => {
5194
+ ): Promise<void> => {
5139
5195
  if (!incrementalPersistence) {
5140
5196
  onCommitted();
5141
- return;
5197
+ return Promise.resolve();
5142
5198
  }
5143
- void incrementalPersistence
5144
- .persistRows([row])
5145
- .then(onCommitted)
5146
- .catch(() => {
5147
- // The final map-level flush awaits the same chain and surfaces the
5148
- // persistence failure loudly without holding a completed row slot open.
5149
- });
5199
+ const settlement = incrementalPersistence.persistRows([row]);
5200
+ void settlement.committed.then(onCommitted).catch(() => {
5201
+ // The terminal map barrier awaits the writer and surfaces the same
5202
+ // latched failure. This continuation only publishes durable progress.
5203
+ });
5204
+ return settlement.admitted;
5150
5205
  };
5151
5206
 
5152
5207
  if (completedRows > 0 || pendingRows !== totalRows) {
@@ -5512,7 +5567,11 @@ export class PlayContextImpl {
5512
5567
  error: formattedError,
5513
5568
  dataPatch: {},
5514
5569
  });
5515
- enqueueIncrementalPersist(failedRow, () => {
5570
+ await enqueueIncrementalPersist(failedRow, () => {
5571
+ this.clearActiveMapCheckpointUpdate(
5572
+ rowKey,
5573
+ normalizedTableNamespace,
5574
+ );
5516
5575
  updateMapFrameProgress({ failedRowKey: rowKey });
5517
5576
  });
5518
5577
  return FAILED_ROW;
@@ -5591,7 +5650,8 @@ export class PlayContextImpl {
5591
5650
  });
5592
5651
  retainedRowsMemoryTracker.track(completedRow);
5593
5652
  completedRowsToPersist.push(completedRow);
5594
- enqueueIncrementalPersist(completedRow, () => {
5653
+ await enqueueIncrementalPersist(completedRow, () => {
5654
+ this.clearActiveMapCheckpointUpdate(rowKey, normalizedTableNamespace);
5595
5655
  updateMapFrameProgress({ completedRowKey: rowKey });
5596
5656
  });
5597
5657
  return publicRow;
@@ -5739,8 +5799,18 @@ export class PlayContextImpl {
5739
5799
  ]),
5740
5800
  ).values(),
5741
5801
  ];
5742
- this.pendingRowEventBoundaries = [];
5802
+ const checkpointUpdates = [
5803
+ ...(this.activeMapCheckpointUpdates?.values() ?? []),
5804
+ ].filter(
5805
+ (update) =>
5806
+ (update.tableNamespace ?? normalizedTableNamespace) ===
5807
+ normalizedTableNamespace,
5808
+ );
5809
+ // A suspension is not observable until both terminal rows and partial
5810
+ // completed-cell patches cross the same writer's durability barrier.
5811
+ await incrementalPersistence?.checkpoint(checkpointUpdates);
5743
5812
  await incrementalPersistence?.flush();
5813
+ this.pendingRowEventBoundaries = [];
5744
5814
  this.#options.onBatchComplete?.(this.checkpoint);
5745
5815
  throw new PlayExecutionSuspendedError({
5746
5816
  kind: 'integration_event_batch',
@@ -604,6 +604,20 @@ export interface ContextOptions {
604
604
  updated: number;
605
605
  staleDroppedKeys?: string[];
606
606
  }>;
607
+ /**
608
+ * Persists coalesced, non-terminal row patches before a map suspends.
609
+ * Terminal rows use onMapRowsCompleted; this narrow seam only preserves
610
+ * cells that completed before an integration-event boundary.
611
+ */
612
+ onMapRowsCheckpoint?: (input: {
613
+ playName?: string;
614
+ playId?: string;
615
+ runId: string;
616
+ executorToken?: string;
617
+ tableNamespace: string;
618
+ updates: PlayRowUpdate[];
619
+ staticPipeline?: PlayStaticPipeline | null;
620
+ }) => Promise<void>;
607
621
  playId?: string;
608
622
  runId?: string;
609
623
  /** Physical executor run that owns leases for an inline child context. */
@@ -1030,6 +1044,7 @@ export interface PlayRowUpdate {
1030
1044
  attemptOwnerRunId?: string | null;
1031
1045
  attemptSeq?: number | null;
1032
1046
  attemptExpiresAt?: string | null;
1047
+ writeVersion?: number | null;
1033
1048
  status?: 'running' | 'completed' | 'failed';
1034
1049
  stage?: string | null;
1035
1050
  provider?: string | null;