deepline 0.3.315 → 0.3.317

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 (134) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/runs/overview.ts +17 -10
  3. package/dist/bundling-sources/sdk/src/types.ts +6 -5
  4. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +64 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +217 -488
  6. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +15 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +4 -1
  8. package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +13 -4
  9. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/live-events.ts +31 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +444 -51
  12. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +22 -43
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +4 -3
  14. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +136 -87
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-authority.ts +24 -6
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +56 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +94 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +5 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +20 -11
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +18 -12
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +396 -35
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +2 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +123 -19
  25. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +13 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/workflow-executor-auth.ts +2 -0
  27. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +3 -2
  28. package/dist/cli/auth-XPLGQ2KO.js +31 -0
  29. package/dist/cli/{auth-OVWOO5NP.mjs → auth-YAHBE5WU.mjs} +4 -4
  30. package/dist/cli/{billing-W3OXKWNE.mjs → billing-AY4OS7MR.mjs} +4 -4
  31. package/dist/cli/{billing-SRZL7UOH.js → billing-ORZDEMH2.js} +70 -70
  32. package/dist/cli/{chunk-OJP4FCCX.js → chunk-2FKXFTOF.js} +48 -48
  33. package/dist/cli/{chunk-OJ243ZA5.js → chunk-2HKPAZVJ.js} +1 -1
  34. package/dist/cli/{chunk-LOWURQMF.js → chunk-342BTES3.js} +50 -50
  35. package/dist/cli/{chunk-FGAK6FET.js → chunk-3NRNDPAE.js} +26 -26
  36. package/dist/cli/{chunk-GK67FYMS.mjs → chunk-5BLF3HDM.mjs} +5 -5
  37. package/dist/cli/{chunk-TZ5KUDPP.mjs → chunk-5OFRM64Y.mjs} +1 -1
  38. package/dist/cli/{chunk-US4S3E4Y.mjs → chunk-7PZFGB7L.mjs} +1 -1
  39. package/dist/cli/{chunk-YL42TF53.js → chunk-AMJYFPFK.js} +6 -6
  40. package/dist/cli/{chunk-PPONZ6IE.js → chunk-AW355CLN.js} +15 -15
  41. package/dist/cli/{chunk-TQK662WG.js → chunk-BGBYK2T3.js} +60 -60
  42. package/dist/cli/{chunk-TQ3F77AC.mjs → chunk-CGGM4CE6.mjs} +3 -3
  43. package/dist/cli/{chunk-FTJQO757.mjs → chunk-D5MHSO5T.mjs} +1 -1
  44. package/dist/cli/{chunk-Y42UERUY.js → chunk-DSTUP75H.js} +6 -6
  45. package/dist/cli/{chunk-HNKHMHJ7.mjs → chunk-FRK2AGSG.mjs} +1 -1
  46. package/dist/cli/{chunk-OUIIYFG3.mjs → chunk-GX3WO4M4.mjs} +2 -2
  47. package/dist/cli/{chunk-OYJCNCUW.js → chunk-GYOLWON6.js} +2 -2
  48. package/dist/cli/{chunk-N5V2SBB2.mjs → chunk-KP7W5NO6.mjs} +1 -1
  49. package/dist/cli/{chunk-L5TSFCJ6.mjs → chunk-KXOXXVYJ.mjs} +2 -2
  50. package/dist/cli/{chunk-B473QUQU.mjs → chunk-L6ERGROE.mjs} +4 -4
  51. package/dist/cli/{chunk-R3IFZDJL.mjs → chunk-LI6YPPBR.mjs} +2 -2
  52. package/dist/cli/{chunk-IGE7EJJY.mjs → chunk-NDFBAMVI.mjs} +19 -15
  53. package/dist/cli/{chunk-JF4O5OMH.mjs → chunk-OTI562P3.mjs} +1 -1
  54. package/dist/cli/{chunk-AO4IWPP7.mjs → chunk-PSHIZP3K.mjs} +17 -15
  55. package/dist/cli/{chunk-VYK23G2C.js → chunk-QFKVAUIQ.js} +239 -235
  56. package/dist/cli/{chunk-AZWQJAQF.mjs → chunk-QPKHOZI3.mjs} +1 -1
  57. package/dist/cli/{chunk-URVZGYGG.js → chunk-SUPQQYGY.js} +4 -4
  58. package/dist/cli/{chunk-C5WSTXUB.js → chunk-TJ2V2JA3.js} +537 -535
  59. package/dist/cli/{chunk-6BJACIVK.js → chunk-UGQHYOT4.js} +1 -1
  60. package/dist/cli/{chunk-IALPYID6.js → chunk-XSEMQ4II.js} +3 -3
  61. package/dist/cli/{chunk-SNFSO2AN.js → chunk-Z5NRL7A7.js} +6 -6
  62. package/dist/cli/{client-WQ3J5BSH.mjs → client-QNLVLW54.mjs} +3 -3
  63. package/dist/cli/client-ZW5VAHWD.js +20 -0
  64. package/dist/cli/{csv-36ARCBNQ.mjs → csv-AKZASZP6.mjs} +3 -3
  65. package/dist/cli/{csv-CENMEWDH.js → csv-PW6S3SMJ.js} +12 -12
  66. package/dist/cli/{db-JU7XLKQM.mjs → db-DNF53LDS.mjs} +4 -4
  67. package/dist/cli/{db-6DQZG6NB.js → db-FLDYET2D.js} +20 -20
  68. package/dist/cli/{describe-VUD4AFBY.js → describe-JDAEOCYQ.js} +17 -17
  69. package/dist/cli/{describe-34ALEEFO.mjs → describe-U6RGIFMA.mjs} +10 -10
  70. package/dist/cli/{executions-H2UXS6FP.js → executions-DASCB6LS.js} +6 -6
  71. package/dist/cli/{executions-H66BMN6N.mjs → executions-W6BBU2HN.mjs} +4 -4
  72. package/dist/cli/{feedback-Q7LXXLSH.mjs → feedback-BVWPDWZG.mjs} +3 -3
  73. package/dist/cli/{feedback-YSYFQSAB.js → feedback-N3MBCT4A.js} +6 -6
  74. package/dist/cli/{git-XV43NSWN.mjs → git-4GJOKOLU.mjs} +4 -4
  75. package/dist/cli/git-WVHH3QXQ.js +23 -0
  76. package/dist/cli/index.js +171 -98
  77. package/dist/cli/index.mjs +114 -41
  78. package/dist/cli/{legacy-noops-Z2DTZQN3.mjs → legacy-noops-5XMSV7WL.mjs} +4 -4
  79. package/dist/cli/{legacy-noops-RGDW6SCA.js → legacy-noops-D7C7ZVKK.js} +7 -7
  80. package/dist/cli/{monitors-I75KZ2BP.js → monitors-NYP4LG3K.js} +85 -85
  81. package/dist/cli/{monitors-OFQ56O4S.mjs → monitors-Y4RKWAS5.mjs} +4 -4
  82. package/dist/cli/{org-75AH6RIB.js → org-PYKQMP6F.js} +60 -60
  83. package/dist/cli/{org-ZYMCU6XO.mjs → org-XZ26CUCV.mjs} +4 -4
  84. package/dist/cli/play-BYZCR4PP.js +152 -0
  85. package/dist/cli/{play-BKXM76DV.mjs → play-FYHIYZ3W.mjs} +9 -9
  86. package/dist/cli/{providers-GWG2WFIY.mjs → providers-3UG7NK55.mjs} +4 -4
  87. package/dist/cli/{providers-Z77MM7ID.js → providers-QS5X236G.js} +7 -7
  88. package/dist/cli/{quickstart-I4RECIQC.js → quickstart-GMASUT7T.js} +5 -5
  89. package/dist/cli/{quickstart-BGVIV374.mjs → quickstart-PHHHHHWO.mjs} +3 -3
  90. package/dist/cli/search-52CC3OVI.js +26 -0
  91. package/dist/cli/{search-TIKBP3RE.mjs → search-AVGITJP4.mjs} +5 -5
  92. package/dist/cli/{secrets-SNGRI4KS.js → secrets-HY5E6F6G.js} +13 -13
  93. package/dist/cli/{secrets-YCHKLWKC.mjs → secrets-RBEZRQ6P.mjs} +4 -4
  94. package/dist/cli/{sessions-WXLADGDX.js → sessions-7CDLWLVV.js} +5 -5
  95. package/dist/cli/{sessions-IRDG6ZRX.mjs → sessions-ME5R5FTG.mjs} +4 -4
  96. package/dist/cli/{settings-KZALJDXW.mjs → settings-X4JQLBB6.mjs} +7 -7
  97. package/dist/cli/{settings-72J4BVF6.js → settings-ZCVJPOFP.js} +74 -74
  98. package/dist/cli/{setup-FFB3KLFD.js → setup-DROGU6MC.js} +41 -41
  99. package/dist/cli/{setup-4KCA7ESV.mjs → setup-NEILS2UF.mjs} +6 -6
  100. package/dist/cli/skills-KUV3WWW5.js +28 -0
  101. package/dist/cli/{skills-MQZPYUQ3.mjs → skills-PC57GDXZ.mjs} +4 -4
  102. package/dist/cli/{tools-M3WKRZ5T.mjs → tools-IFJUWQC5.mjs} +6 -6
  103. package/dist/cli/tools-NHDAOQPB.js +38 -0
  104. package/dist/cli/update-DRWQCVXJ.js +28 -0
  105. package/dist/cli/{update-QJMNYGNF.mjs → update-N4HSH2TJ.mjs} +6 -6
  106. package/dist/cli/{workflow-6T2EUFCN.mjs → workflow-2OZBTUU2.mjs} +5 -5
  107. package/dist/cli/{workflow-35UUOZ5E.js → workflow-LP4Z6RF5.js} +15 -15
  108. package/dist/{client-Dh_4Ph1R.d.ts → client-BV68GMPo.d.ts} +7 -6
  109. package/dist/{client-CvpZsBqt.d.mts → client-D9ACjiRC.d.mts} +7 -6
  110. package/dist/{compiler-manifest-wOKNiXpa.d.mts → compiler-manifest-BXcZ4P4W.d.mts} +2 -2
  111. package/dist/{compiler-manifest-wOKNiXpa.d.ts → compiler-manifest-BXcZ4P4W.d.ts} +2 -2
  112. package/dist/index.d.mts +4 -4
  113. package/dist/index.d.ts +4 -4
  114. package/dist/index.js +7 -7
  115. package/dist/index.mjs +4 -4
  116. package/dist/install-integrity.json +82 -82
  117. package/dist/plays/bundle-play-file.d.mts +2 -2
  118. package/dist/plays/bundle-play-file.d.ts +2 -2
  119. package/dist/plays/bundle-play-file.mjs +2 -2
  120. package/dist/plays/run-request.d.mts +2 -2
  121. package/dist/plays/run-request.d.ts +2 -2
  122. package/dist/release.d.mts +1 -1
  123. package/dist/release.d.ts +1 -1
  124. package/dist/release.js +2 -2
  125. package/dist/release.mjs +1 -1
  126. package/package.json +1 -1
  127. package/dist/cli/auth-P2TTZU36.js +0 -31
  128. package/dist/cli/client-CXFPFCIG.js +0 -20
  129. package/dist/cli/git-SYFCYIFP.js +0 -23
  130. package/dist/cli/play-OKXUWPMO.js +0 -152
  131. package/dist/cli/search-3KISUDDT.js +0 -26
  132. package/dist/cli/skills-XGOK4GRW.js +0 -28
  133. package/dist/cli/tools-LEAJ7EPW.js +0 -38
  134. package/dist/cli/update-ZN6LDUHS.js +0 -28
@@ -4,6 +4,9 @@ import type {
4
4
  PlayCheckpoint,
5
5
  PlayExecutionEvent,
6
6
  } from './ctx-types';
7
+ import type { MapRowOutcome, MapRowOutcomeStatus } from './durability-store';
8
+ import { cloneCsvAliasedRow } from './csv-rename';
9
+ import { RuntimeSheetAttemptSupersededError } from './runtime-sheet-errors';
7
10
 
8
11
  export function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
9
12
  return {
@@ -14,7 +17,7 @@ export function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
14
17
  ? {
15
18
  suspendedInputRows: frame.suspendedInputRows.map((row) => ({
16
19
  inputIndex: row.inputIndex,
17
- data: { ...row.data },
20
+ data: cloneCsvAliasedRow(row.data),
18
21
  ...(row.authoredInputDigest
19
22
  ? { authoredInputDigest: row.authoredInputDigest }
20
23
  : {}),
@@ -71,63 +74,453 @@ export class MapExecutionFrameStore {
71
74
  });
72
75
  }
73
76
 
74
- updateProgress(input: {
75
- scope: MapExecutionScope;
76
- totalRows: number;
77
- status?: MapExecutionFrame['status'];
78
- completedRowKey?: string | null;
79
- pendingRowKey?: string | null;
80
- failedDelta?: number;
81
- activeBoundaryId?: string | null;
82
- emitEventType?: PlayExecutionEvent['type'];
83
- }): void {
84
- const existing =
85
- this.checkpoint.mapFrames?.[input.scope.mapInvocationId] ?? null;
86
- if (!existing) {
87
- return;
77
+ /**
78
+ * Open one resolver pool's progress. Restored keys are seeded once; offsets
79
+ * never add that same restored baseline again. Sources stay live because a
80
+ * streaming feed admits pages and changes latest-view visibility during execution.
81
+ */
82
+ progress(
83
+ scope: MapExecutionScope,
84
+ source: MapProgressSource,
85
+ ): MapExecutionProgress {
86
+ return new MapExecutionProgress(
87
+ this.checkpoint,
88
+ (frame) => this.set(frame),
89
+ this.emitExecutionEvent,
90
+ scope,
91
+ source,
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Restore visible success identities and their logical input count together.
97
+ * Superseded frames can contain historical keys even when their scalar count
98
+ * appears consistent, so the run-filtered sheet reader checks every such key.
99
+ * An explicit success count stays authoritative; its excess over visible keys
100
+ * represents reused inputs. Older materialized suspension plans prove a
101
+ * missing count by conservation of successful, failed, superseded and pending
102
+ * inputs. Frames without that plan keep the historical key-count default.
103
+ */
104
+ async restoreCompletedRows(
105
+ frame: MapExecutionFrame,
106
+ readVisibleKeys:
107
+ | ((keys: string[]) => Promise<readonly string[]>)
108
+ | undefined,
109
+ maxReadKeys: number,
110
+ ): Promise<{
111
+ completedRowKeys: string[];
112
+ completedRowsCount: number;
113
+ reusedRowsCount: number;
114
+ }> {
115
+ const completedRowKeys =
116
+ (frame.supersededRows ?? 0) > 0 &&
117
+ frame.completedRowKeys.length > 0 &&
118
+ readVisibleKeys
119
+ ? await this.visibleRestoredKeys(
120
+ frame.completedRowKeys,
121
+ readVisibleKeys,
122
+ maxReadKeys,
123
+ )
124
+ : [...frame.completedRowKeys];
125
+ const completedRowsCount =
126
+ frame.completedRowsCount ??
127
+ (Array.isArray(frame.suspendedInputRows)
128
+ ? frame.totalRows -
129
+ (frame.failedRowsCount ?? 0) -
130
+ (frame.supersededRows ?? 0) -
131
+ new Set(frame.pendingRowKeys).size
132
+ : completedRowKeys.length);
133
+ if (
134
+ !Number.isSafeInteger(completedRowsCount) ||
135
+ completedRowsCount < completedRowKeys.length
136
+ ) {
137
+ throw new Error(
138
+ `Cannot restore map ${frame.mapInvocationId}: successful input count ${completedRowsCount} contradicts ${completedRowKeys.length} visible success keys.`,
139
+ );
88
140
  }
89
- const completedRowKeys = new Set(existing.completedRowKeys);
90
- const pendingRowKeys = new Set(existing.pendingRowKeys);
91
- const completedBefore = completedRowKeys.size;
92
- if (input.completedRowKey?.trim()) {
93
- const completedRowKey = input.completedRowKey.trim();
94
- completedRowKeys.add(completedRowKey);
95
- pendingRowKeys.delete(completedRowKey);
141
+ return {
142
+ completedRowKeys,
143
+ completedRowsCount,
144
+ reusedRowsCount: completedRowsCount - completedRowKeys.length,
145
+ };
146
+ }
147
+
148
+ private async visibleRestoredKeys(
149
+ keys: readonly string[],
150
+ readVisibleKeys: (keys: string[]) => Promise<readonly string[]>,
151
+ maxReadKeys: number,
152
+ ): Promise<string[]> {
153
+ if (!Number.isSafeInteger(maxReadKeys) || maxReadKeys <= 0)
154
+ throw new Error('Restored map key reads require a positive batch size.');
155
+ const visible: string[] = [];
156
+ for (let offset = 0; offset < keys.length; offset += maxReadKeys) {
157
+ const page = keys.slice(offset, offset + maxReadKeys);
158
+ const matched = new Set(await readVisibleKeys(page));
159
+ visible.push(...page.filter((key) => matched.has(key)));
96
160
  }
97
- if (input.pendingRowKey?.trim()) {
98
- pendingRowKeys.add(input.pendingRowKey.trim());
161
+ return visible;
162
+ }
163
+ }
164
+
165
+ type LiveCount = number | (() => number);
166
+ const readCount = (value: LiveCount | undefined, fallback = 0): number =>
167
+ typeof value === 'function' ? value() : (value ?? fallback);
168
+ const FRAME_FLUSH_ROWS = 100;
169
+ const FRAME_FLUSH_MS = 250;
170
+
171
+ /**
172
+ * Accepted Runtime Sheet writes decide which terminal rows remain visible.
173
+ * The account is shared across a map's pools; restored superseded rows need
174
+ * only their existing scalar because they are absent from restored success keys.
175
+ * Classification changes and recovery update counts in O(1) per reported key.
176
+ */
177
+ export class MapExecutionVisibility {
178
+ private readonly superseded = new Map<string, MapRowOutcomeStatus>();
179
+ private supersededCompleted = 0;
180
+ private supersededFailed = 0;
181
+
182
+ constructor(private readonly restoredSupersededRows = 0) {}
183
+
184
+ /** Consume visibility facts only after the writer accepts the terminal batch. */
185
+ recordAcceptedWrite(
186
+ result: unknown,
187
+ rows: readonly MapRowOutcome[],
188
+ scope: { tableNamespace: string; runId: string },
189
+ ): void {
190
+ if (!result || typeof result !== 'object') return;
191
+ const facts = result as {
192
+ supersededKeys?: string[];
193
+ staleDroppedKeys?: unknown;
194
+ recoveredStaleKeys?: unknown;
195
+ };
196
+ if (facts.supersededKeys?.length) {
197
+ // An attempt-wide rejection never admits visibility facts or outcomes.
198
+ throw new RuntimeSheetAttemptSupersededError({
199
+ tableNamespace: scope.tableNamespace,
200
+ runId: scope.runId,
201
+ writeVersionValue: null,
202
+ submittedRows: rows.length,
203
+ supersededRows: facts.supersededKeys.map((key) => ({
204
+ key,
205
+ reason: 'unreported' as const,
206
+ rowVersionStamp: null,
207
+ })),
208
+ });
99
209
  }
100
- const failedRowsCount = Math.max(
101
- 0,
102
- (existing.failedRowsCount ?? 0) + Math.max(0, input.failedDelta ?? 0),
210
+ const statuses = new Map(
211
+ rows.map((row) => [row.key, row.status ?? 'completed']),
103
212
  );
104
- const nextFrame: MapExecutionFrame = {
105
- ...existing,
106
- status: input.status ?? existing.status,
107
- completedRowKeys: [...completedRowKeys],
108
- pendingRowKeys: [...pendingRowKeys],
109
- failedRowsCount,
110
- ...(input.activeBoundaryId !== undefined
111
- ? { activeBoundaryId: input.activeBoundaryId }
213
+ if (Array.isArray(facts.staleDroppedKeys)) {
214
+ for (const key of facts.staleDroppedKeys) {
215
+ if (typeof key !== 'string' || !key) continue;
216
+ this.removeSuperseded(key);
217
+ const status = statuses.get(key) ?? 'completed';
218
+ this.superseded.set(key, status);
219
+ if (status === 'failed') this.supersededFailed += 1;
220
+ else this.supersededCompleted += 1;
221
+ }
222
+ }
223
+ if (Array.isArray(facts.recoveredStaleKeys)) {
224
+ for (const key of facts.recoveredStaleKeys) {
225
+ if (typeof key === 'string' && key) this.removeSuperseded(key);
226
+ }
227
+ }
228
+ }
229
+
230
+ private removeSuperseded(key: string): void {
231
+ const previous = this.superseded.get(key);
232
+ if (previous === undefined) return;
233
+ this.superseded.delete(key);
234
+ if (previous === 'failed') this.supersededFailed -= 1;
235
+ else this.supersededCompleted -= 1;
236
+ }
237
+
238
+ isVisible(key: string): boolean {
239
+ return !this.superseded.has(key);
240
+ }
241
+
242
+ get supersededRows(): number {
243
+ return this.restoredSupersededRows + this.superseded.size;
244
+ }
245
+
246
+ /** One successes/failures/superseded split for live and terminal projections. */
247
+ settlement(completedRows: number, failedRows: number) {
248
+ return {
249
+ completedRows: completedRows - this.supersededCompleted,
250
+ failedRows: failedRows - this.supersededFailed,
251
+ supersededRows: this.supersededRows,
252
+ };
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Live offsets are outcomes from earlier pools, before visibility corrections.
258
+ * The shared visibility account owns corrections for earlier and current-pool
259
+ * writes. A committed batch is accounted atomically before projecting either.
260
+ */
261
+ export interface MapProgressSource {
262
+ totalRows: LiveCount;
263
+ completedOffset?: LiveCount;
264
+ failedOffset?: LiveCount;
265
+ visibility: MapExecutionVisibility;
266
+ skippedInputs?: LiveCount;
267
+ /** Fed maps keep only scalars in checkpoints; the Runtime Sheet owns row records. */
268
+ checkpointRows: 'keys' | 'counts';
269
+ eventFields?: () => Partial<PlayExecutionEvent>;
270
+ }
271
+
272
+ type ProgressChange = {
273
+ status?: MapExecutionFrame['status'];
274
+ completedRowKey?: string | null;
275
+ completedRowKeys?: readonly string[];
276
+ pendingRowKey?: string | null;
277
+ pendingRowKeys?: readonly string[];
278
+ failedRowKey?: string | null;
279
+ failedRowKeys?: readonly string[];
280
+ failedDelta?: number;
281
+ activeBoundaryId?: string | null;
282
+ emitEventType?: PlayExecutionEvent['type'];
283
+ };
284
+
285
+ /**
286
+ * Owns the pool's committed outcomes and its scalar progress projection.
287
+ * Resolver admission is activity, not settlement. Sheet-backed row outcomes
288
+ * enter through recordCommittedRows after their terminal write commits; pending
289
+ * keys and lifecycle transitions may update earlier. Failure counts are
290
+ * explicit, never inferred from sparse pending keys. Terminal sheet
291
+ * reconciliation remains authoritative.
292
+ * Keys mutate in place; checkpoint arrays are materialized on a row/time
293
+ * throttle and on every transition, preserving the existing replay format.
294
+ * Accepted terminal input indexes also drain the retained suspension plan in
295
+ * O(1); an empty retained plan remains distinct from an absent plan on replay.
296
+ */
297
+ export class MapExecutionProgress {
298
+ private readonly completed: Set<string>;
299
+ private readonly pending: Set<string>;
300
+ private failed = 0;
301
+ private rowsSinceFlush = 0;
302
+ private lastFlushAt = 0;
303
+ private started = 0;
304
+ private active = 0;
305
+ private tracksActivity = false;
306
+ private lastActivityKey: string | null = null;
307
+ private readonly initiallyStarted: number;
308
+ private readonly initiallySkipped: number;
309
+ private status: MapExecutionFrame['status'];
310
+ private activeBoundaryId: string | null | undefined;
311
+ private suspendedInputs:
312
+ | Map<number, NonNullable<MapExecutionFrame['suspendedInputRows']>[number]>
313
+ | undefined;
314
+
315
+ constructor(
316
+ private readonly checkpoint: PlayCheckpoint,
317
+ private readonly setFrame: (frame: MapExecutionFrame) => void,
318
+ private readonly emitExecutionEvent: (event: PlayExecutionEvent) => void,
319
+ private readonly scope: MapExecutionScope,
320
+ private readonly source: MapProgressSource,
321
+ ) {
322
+ const frame = this.frame();
323
+ this.completed = new Set(frame?.completedRowKeys ?? []);
324
+ this.pending = new Set(frame?.pendingRowKeys ?? []);
325
+ this.status = frame?.status ?? 'running';
326
+ this.activeBoundaryId = frame?.activeBoundaryId;
327
+ this.suspendedInputs = frame?.suspendedInputRows
328
+ ? new Map(frame.suspendedInputRows.map((row) => [row.inputIndex, row]))
329
+ : undefined;
330
+ // Freeze settled admission once: live superseded facts can include a
331
+ // resolver already counted in started, whereas skipped inputs are new.
332
+ const baseline = this.settlement();
333
+ this.initiallyStarted =
334
+ baseline.completedRows + baseline.failedRows + baseline.supersededRows;
335
+ this.initiallySkipped = readCount(source.skippedInputs);
336
+ }
337
+
338
+ private frame(): MapExecutionFrame | undefined {
339
+ return this.checkpoint.mapFrames?.[this.scope.mapInvocationId];
340
+ }
341
+
342
+ private settlement() {
343
+ return this.source.visibility.settlement(
344
+ readCount(this.source.completedOffset) + this.completed.size,
345
+ readCount(this.source.failedOffset) + this.failed,
346
+ );
347
+ }
348
+
349
+ /** One fixed-shape view for checkpoints, settlement events and activity samples. */
350
+ projection() {
351
+ const startedRows =
352
+ this.initiallyStarted +
353
+ readCount(this.source.skippedInputs, this.initiallySkipped) -
354
+ this.initiallySkipped +
355
+ this.started;
356
+ return {
357
+ ...this.settlement(),
358
+ totalRows: readCount(this.source.totalRows),
359
+ ...(this.tracksActivity
360
+ ? {
361
+ startedRows,
362
+ activeRows: this.active,
363
+ waitingRows: readCount(this.source.totalRows) - startedRows,
364
+ }
112
365
  : {}),
113
- updatedAt: Date.now(),
114
366
  };
115
- this.set(nextFrame);
116
- const progressEventType =
367
+ }
368
+
369
+ private emit(type: PlayExecutionEvent['type']): void {
370
+ const { startedRows, activeRows, waitingRows, ...settled } =
371
+ this.projection();
372
+ this.emitExecutionEvent({
373
+ ...this.source.eventFields?.(),
374
+ type,
375
+ mapInvocationId: this.scope.mapInvocationId,
376
+ mapNodeId: this.scope.mapNodeId ?? null,
377
+ logicalNamespace: this.scope.logicalNamespace,
378
+ artifactTableNamespace: this.scope.artifactTableNamespace,
379
+ ...settled,
380
+ ...(type === 'map.progress' && this.tracksActivity
381
+ ? { startedRows, activeRows, waitingRows }
382
+ : {}),
383
+ at: Date.now(),
384
+ } as PlayExecutionEvent);
385
+ }
386
+
387
+ update(input: ProgressChange): void {
388
+ if (!this.frame()) return;
389
+ const before = this.completed.size;
390
+ for (const raw of [
391
+ input.completedRowKey,
392
+ ...(input.completedRowKeys ?? []),
393
+ ]) {
394
+ const key = raw?.trim();
395
+ if (!key) continue;
396
+ this.completed.add(key);
397
+ this.pending.delete(key);
398
+ }
399
+ for (const raw of [input.pendingRowKey, ...(input.pendingRowKeys ?? [])]) {
400
+ const key = raw?.trim();
401
+ if (key && !this.completed.has(key)) this.pending.add(key);
402
+ }
403
+ for (const raw of [input.failedRowKey, ...(input.failedRowKeys ?? [])]) {
404
+ const key = raw?.trim();
405
+ if (key) this.pending.delete(key);
406
+ }
407
+ this.failed += Math.max(0, input.failedDelta ?? 0);
408
+ this.rowsSinceFlush += this.completed.size - before;
409
+ this.status = input.status ?? this.status;
410
+ if (input.activeBoundaryId !== undefined)
411
+ this.activeBoundaryId = input.activeBoundaryId;
412
+ const transition =
413
+ input.status !== undefined ||
414
+ input.activeBoundaryId !== undefined ||
415
+ input.emitEventType !== undefined;
416
+ const now = Date.now();
417
+ const flush =
418
+ transition ||
419
+ readCount(this.source.totalRows) <= FRAME_FLUSH_ROWS ||
420
+ this.rowsSinceFlush >= FRAME_FLUSH_ROWS ||
421
+ now - this.lastFlushAt >= FRAME_FLUSH_MS;
422
+ if (flush) this.flush(now);
423
+ const event =
117
424
  input.emitEventType ??
118
- (completedRowKeys.size > completedBefore ? 'map.progress' : null);
119
- if (progressEventType) {
120
- this.emitExecutionEvent({
121
- type: progressEventType,
122
- mapInvocationId: input.scope.mapInvocationId,
123
- mapNodeId: input.scope.mapNodeId ?? null,
124
- logicalNamespace: input.scope.logicalNamespace,
125
- artifactTableNamespace: input.scope.artifactTableNamespace,
126
- completedRows: nextFrame.completedRowKeys.length,
127
- failedRows: failedRowsCount,
128
- totalRows: input.totalRows,
129
- at: Date.now(),
130
- } as PlayExecutionEvent);
425
+ (flush && this.completed.size > before ? 'map.progress' : null);
426
+ if (event) this.emit(event);
427
+ }
428
+
429
+ /** Consume one durably committed batch, then project all its outcomes together. */
430
+ recordCommittedRows(rows: readonly MapRowOutcome[]): void {
431
+ const completed: string[] = [];
432
+ const failed: string[] = [];
433
+ for (const row of rows) {
434
+ if (typeof row.inputIndex === 'number')
435
+ this.suspendedInputs?.delete(row.inputIndex);
436
+ (row.status === 'failed' ? failed : completed).push(row.key);
437
+ }
438
+ this.update({
439
+ completedRowKeys: completed,
440
+ failedRowKeys: failed,
441
+ failedDelta: failed.length,
442
+ });
443
+ }
444
+
445
+ private flush(now: number): void {
446
+ const frame = this.frame();
447
+ if (!frame) return;
448
+ const progress = this.projection();
449
+ this.setFrame({
450
+ ...frame,
451
+ status: this.status,
452
+ totalRows: progress.totalRows,
453
+ completedRowKeys:
454
+ this.source.checkpointRows === 'counts'
455
+ ? []
456
+ : [...this.completed].filter((key) =>
457
+ this.source.visibility.isVisible(key),
458
+ ),
459
+ pendingRowKeys:
460
+ this.source.checkpointRows === 'counts' ? [] : [...this.pending],
461
+ ...(this.source.checkpointRows === 'counts'
462
+ ? {
463
+ pendingRowsCount: this.pending.size,
464
+ }
465
+ : {}),
466
+ completedRowsCount: progress.completedRows,
467
+ failedRowsCount: progress.failedRows,
468
+ supersededRows: progress.supersededRows,
469
+ ...(this.suspendedInputs !== undefined
470
+ ? { suspendedInputRows: [...this.suspendedInputs.values()] }
471
+ : {}),
472
+ ...(this.activeBoundaryId !== undefined
473
+ ? { activeBoundaryId: this.activeBoundaryId }
474
+ : {}),
475
+ updatedAt: now,
476
+ });
477
+ this.rowsSinceFlush = 0;
478
+ this.lastFlushAt = now;
479
+ }
480
+
481
+ /** Record the resume plan before publishing the suspension or handing it off. */
482
+ suspend(input: {
483
+ pendingRowKeys: readonly string[];
484
+ suspendedInputRows: MapExecutionFrame['suspendedInputRows'];
485
+ activeBoundaryId: string | null;
486
+ }): void {
487
+ const frame = this.frame();
488
+ if (!frame)
489
+ throw new Error('Suspended map lost its durable execution frame.');
490
+ this.pending.clear();
491
+ for (const key of input.pendingRowKeys) {
492
+ this.completed.delete(key);
493
+ this.pending.add(key);
131
494
  }
495
+ this.suspendedInputs = input.suspendedInputRows
496
+ ? new Map(input.suspendedInputRows.map((row) => [row.inputIndex, row]))
497
+ : undefined;
498
+ this.setFrame({ ...frame, suspendedInputRows: input.suspendedInputRows });
499
+ this.update({
500
+ status: 'suspended',
501
+ activeBoundaryId: input.activeBoundaryId,
502
+ emitEventType: 'map.suspended',
503
+ });
504
+ }
505
+
506
+ resolverStarted(): void {
507
+ this.tracksActivity = true;
508
+ this.started += 1;
509
+ this.active += 1;
510
+ }
511
+
512
+ resolverFinished(): void {
513
+ this.active -= 1;
514
+ }
515
+
516
+ /** Activity never copies key arrays or publishes after a terminal transition. */
517
+ publishActivity(): void {
518
+ if (this.status !== 'running') return;
519
+ this.tracksActivity = true;
520
+ const counts = this.projection();
521
+ const key = `${counts.startedRows}:${counts.activeRows}:${counts.waitingRows}`;
522
+ if (key === this.lastActivityKey) return;
523
+ this.lastActivityKey = key;
524
+ this.emit('map.progress');
132
525
  }
133
526
  }
@@ -1,4 +1,8 @@
1
- import { resolveTimingWindow } from './live-events';
1
+ import {
2
+ projectPlayStepProgressCounters,
3
+ resolveTimingWindow,
4
+ type PlayStepProgressCounters,
5
+ } from './live-events';
2
6
  import {
3
7
  LOG_TAIL_LIMIT,
4
8
  hasConfirmedPlayRunLedgerWorkCounters,
@@ -36,18 +40,13 @@ import { stripProvenanceTag } from './log-provenance';
36
40
  * Vercel app (dashboard and legacy SSE compatibility) and the SDK REST polling
37
41
  * watcher (`packages/sdk/src/runs/watch-polling.ts`), so both use the same
38
42
  * event projection from the persisted Run Snapshot. See ADR-0008.
43
+ * Snapshots, progress signatures and events share one scalar counter projection;
44
+ * counter changes are significant even when their timestamps are identical.
39
45
  */
40
46
 
41
47
  export type PlayRunLiveStatus = PlayRunLifecycleStatus;
42
48
 
43
- export type PlayRunStreamNodeProgress = {
44
- completed?: number;
45
- total?: number;
46
- failed?: number;
47
- startedRows?: number;
48
- activeRows?: number;
49
- waitingRows?: number;
50
- completedRows?: number;
49
+ export type PlayRunStreamNodeProgress = PlayStepProgressCounters & {
51
50
  message?: string;
52
51
  updatedAt?: number | null;
53
52
  startedAt?: number | null;
@@ -241,14 +240,7 @@ function buildSnapshotFromLedger(
241
240
  artifactTableNamespace: step.artifactTableNamespace ?? null,
242
241
  progress: step.progress
243
242
  ? {
244
- completed: step.progress.completed,
245
- total: step.progress.total,
246
- failed: step.progress.failed,
247
- startedRows: step.progress.startedRows,
248
- activeRows: step.progress.activeRows,
249
- waitingRows: step.progress.waitingRows,
250
- completedRows: step.progress.completedRows,
251
- supersededRows: step.progress.supersededRows,
243
+ ...projectPlayStepProgressCounters(step.progress),
252
244
  message: step.progress.message,
253
245
  artifactTableNamespace:
254
246
  step.progress.artifactTableNamespace ??
@@ -418,12 +410,9 @@ export type PlayRunStreamEvent =
418
410
  'play.step.status'
419
411
  >
420
412
  | RunStreamEventEnvelope<
421
- {
413
+ PlayStepProgressCounters & {
422
414
  runId: string;
423
415
  stepId: string;
424
- completed?: number;
425
- total?: number;
426
- failed?: number;
427
416
  message?: string;
428
417
  artifactTableNamespace: string | null;
429
418
  startedAt?: number | null;
@@ -512,21 +501,17 @@ function getStepStatusSignature(snapshot: PlayRunLiveSnapshot): string {
512
501
  }
513
502
 
514
503
  function getStepProgressSignature(snapshot: PlayRunLiveSnapshot): string {
515
- return snapshot.nodeStates
516
- .map((state) =>
517
- [
518
- state.nodeId,
519
- state.progress?.completed ?? '',
520
- state.progress?.total ?? '',
521
- state.progress?.failed ?? '',
522
- state.progress?.artifactTableNamespace ?? '',
523
- state.progress?.startedAt ?? '',
524
- state.progress?.completedAt ?? '',
525
- state.progress?.updatedAt ?? '',
526
- state.progress?.message ?? '',
527
- ].join(':'),
528
- )
529
- .join('|');
504
+ return JSON.stringify(
505
+ snapshot.nodeStates.map((state) => [
506
+ state.nodeId,
507
+ projectPlayStepProgressCounters(state.progress ?? {}),
508
+ state.progress?.artifactTableNamespace ?? '',
509
+ state.progress?.startedAt ?? '',
510
+ state.progress?.completedAt ?? '',
511
+ state.progress?.updatedAt ?? '',
512
+ state.progress?.message ?? '',
513
+ ]),
514
+ );
530
515
  }
531
516
 
532
517
  function getSnapshotSignature(snapshot: PlayRunLiveSnapshot): string {
@@ -682,13 +667,7 @@ export function diffPlayRunStreamEvents(input: {
682
667
  payload: {
683
668
  runId: snapshot.runId,
684
669
  stepId: state.nodeId,
685
- completed: state.progress.completed,
686
- total: state.progress.total,
687
- failed: state.progress.failed,
688
- startedRows: state.progress.startedRows,
689
- activeRows: state.progress.activeRows,
690
- waitingRows: state.progress.waitingRows,
691
- completedRows: state.progress.completedRows,
670
+ ...projectPlayStepProgressCounters(state.progress),
692
671
  message: state.progress.message,
693
672
  artifactTableNamespace:
694
673
  state.progress.artifactTableNamespace ??
@@ -2574,9 +2574,9 @@ async function run(
2574
2574
  runId,
2575
2575
  tableNamespace,
2576
2576
  });
2577
- // A later source page of a table this runner already admitted keeps the
2578
- // first page's write version: rows of earlier pages are still executing
2579
- // and their terminal writes are fenced by it.
2577
+ // The admission owner durably pins one write intent per run/table, also
2578
+ // after a scheduler wake recreates this Map. Retain the local version as
2579
+ // an additional consistency check across pages of this runner.
2580
2580
  const existingAttempt = runtimeSheetAttempts.get(attemptKey);
2581
2581
  const mapStartResult = await retryRuntimeSheetPersistence({
2582
2582
  description: `runtime map start table=${tableNamespace}`,
@@ -2612,6 +2612,7 @@ async function run(
2612
2612
  columnTypes: mapContext.columnTypes,
2613
2613
  }),
2614
2614
  rows: items,
2615
+ stableWriteIntent: true,
2615
2616
  pendingRerunAdmission: true,
2616
2617
  inputOffset: mapContext.inputOffset,
2617
2618
  mode: mapContext.mode,