deepline 0.3.90 → 0.3.91
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.
- package/dist/bundling-sources/sdk/src/client.ts +101 -6
- package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
- package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +7 -12
- package/dist/bundling-sources/sdk/src/types.ts +6 -1
- package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
- package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
- package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
- package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +36 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
- package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
- package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
- package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
- package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
- package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
- package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
- package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
- package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
- package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
- package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
- package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
- package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
- package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
- package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
- package/dist/cli/index.js +369 -162
- package/dist/cli/index.mjs +369 -162
- package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
- package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
- package/dist/index.d.mts +45 -8
- package/dist/index.d.ts +45 -8
- package/dist/index.js +61 -14
- package/dist/index.mjs +61 -14
- package/dist/install-integrity.json +54 -2
- package/dist/monitor-fleet-contract.d.mts +3 -10
- package/dist/monitor-fleet-contract.d.ts +3 -10
- package/dist/monitor-fleet-contract.js +2 -0
- package/dist/monitor-fleet-contract.mjs +1 -1
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/release.d.mts +3 -3
- package/dist/release.d.ts +3 -3
- package/dist/release.js +7 -12
- package/dist/release.mjs +7 -12
- package/package.json +2 -2
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION,
|
|
3
|
+
type PlayRunnerEvent,
|
|
4
|
+
type PlayRunnerResult,
|
|
5
|
+
} from '../protocol';
|
|
6
|
+
import { parseCtxFetchHttpFailureDiagnostic } from '../log-provenance';
|
|
7
|
+
import {
|
|
8
|
+
RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES,
|
|
9
|
+
TERMINAL_RUN_RESULT_MAX_BYTES,
|
|
10
|
+
jsonByteLength,
|
|
11
|
+
jsonByteLengthUpTo,
|
|
12
|
+
} from '../output-size-limits';
|
|
13
|
+
import {
|
|
14
|
+
PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER,
|
|
15
|
+
} from '../runtime-contract';
|
|
16
|
+
import {
|
|
17
|
+
createRunnerGatewayTransportAttemptId,
|
|
18
|
+
runnerGatewayTransportErrorCode,
|
|
19
|
+
type RunnerGatewayTransportEvent,
|
|
20
|
+
} from './runtime-gateway-transport';
|
|
21
|
+
import { RUNTIME_RELIABILITY_POLICY } from '../runtime-reliability-policy';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Push-execution terminal push (runs INSIDE the Daytona sandbox, B2-final).
|
|
25
|
+
*
|
|
26
|
+
* The worker parks after the detached start; the runner's pushed terminal is
|
|
27
|
+
* what wakes it. This POSTs the final status-aware terminal projection to the
|
|
28
|
+
* receipt gateway's `runner_terminal` action, which persists it on the attempt
|
|
29
|
+
* row (first-write-wins) and emits the attempt-scoped absurd wake event.
|
|
30
|
+
* Completed and failed projections omit replay payloads; suspended projections
|
|
31
|
+
* retain the checkpoint required for resume.
|
|
32
|
+
*
|
|
33
|
+
* Retries are bounded and loud. A push that ultimately fails does NOT fail the
|
|
34
|
+
* play by itself: the structured `result` event still lands in the captured
|
|
35
|
+
* output file, and the parked attempt's ceiling-timeout salvage reads it from
|
|
36
|
+
* there — slower, but no outcome is lost.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const TERMINAL_PUSH_MAX_ATTEMPTS =
|
|
40
|
+
RUNTIME_RELIABILITY_POLICY.gateway.terminalPushMaxAttempts;
|
|
41
|
+
const TERMINAL_PUSH_RETRY_DELAYS_MS = [500, 1_000, 2_000, 4_000] as const;
|
|
42
|
+
const TERMINAL_PUSH_REQUEST_TIMEOUT_MS =
|
|
43
|
+
RUNTIME_RELIABILITY_POLICY.gateway.terminalPushRequestTimeoutMs;
|
|
44
|
+
const APP_RUNTIME_COMPAT_PATH = '/api/v2/plays/runtime';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A terminal result reserves its 5 MiB customer budget for the authored
|
|
48
|
+
* `output`. Runner diagnostics are useful, but they must never make a valid
|
|
49
|
+
* customer return too large to settle or to recover from Daytona stdout.
|
|
50
|
+
*
|
|
51
|
+
* These caps total well below the 1 MiB terminal-envelope headroom. They apply
|
|
52
|
+
* only to runner-owned diagnostics; returned rows and output values are never
|
|
53
|
+
* sliced here.
|
|
54
|
+
*/
|
|
55
|
+
const TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES = 128 * 1024;
|
|
56
|
+
const TERMINAL_TRANSPORT_STATS_MAX_BYTES = 96 * 1024;
|
|
57
|
+
const TERMINAL_TRANSPORT_STEPS_MAX_BYTES = 160 * 1024;
|
|
58
|
+
const TERMINAL_TRANSPORT_FAILURE_DETAILS_MAX_BYTES = 96 * 1024;
|
|
59
|
+
const TERMINAL_TRANSPORT_OUTPUT_WARNINGS_MAX_BYTES = 32 * 1024;
|
|
60
|
+
const TERMINAL_TRANSPORT_RUNTIME_DIAGNOSTICS_MAX_BYTES = 16 * 1024;
|
|
61
|
+
const TERMINAL_TRANSPORT_ERROR_MAX_BYTES = 64 * 1024;
|
|
62
|
+
const TERMINAL_TRANSPORT_TABLE_NAMESPACE_MAX_BYTES = 8 * 1024;
|
|
63
|
+
|
|
64
|
+
type CompletedRunnerResult = Extract<PlayRunnerResult, { status: 'completed' }>;
|
|
65
|
+
type SuspendedRunnerResult = Extract<PlayRunnerResult, { status: 'suspended' }>;
|
|
66
|
+
type FailedRunnerResult = Extract<PlayRunnerResult, { status: 'failed' }>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Settlement payload carried by a detached runner's gateway POST and stdout
|
|
70
|
+
* salvage line. Completed and failed executions cannot resume, so their replay
|
|
71
|
+
* checkpoint is redundant with durable receipts and must not cross the
|
|
72
|
+
* gateway's bounded request-body transport. Suspended executions still need
|
|
73
|
+
* the exact checkpoint to resume and remain unchanged.
|
|
74
|
+
*/
|
|
75
|
+
type OmittedTerminalCheckpoint = {
|
|
76
|
+
checkpointDisposition: typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.OMITTED_TERMINAL_REPLAY;
|
|
77
|
+
checkpointBytes: number;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type IncludedResumeCheckpoint = {
|
|
81
|
+
checkpointDisposition: typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.INCLUDED_RESUME_REQUIRED;
|
|
82
|
+
checkpointBytes: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type AbsentCheckpoint = {
|
|
86
|
+
checkpointDisposition: typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT;
|
|
87
|
+
checkpointBytes: 0;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type PlayRunnerTerminalTransportResult =
|
|
91
|
+
| (Omit<CompletedRunnerResult, 'checkpoint'> &
|
|
92
|
+
(OmittedTerminalCheckpoint | AbsentCheckpoint))
|
|
93
|
+
| (SuspendedRunnerResult & IncludedResumeCheckpoint)
|
|
94
|
+
| (Omit<FailedRunnerResult, 'checkpoint'> &
|
|
95
|
+
(OmittedTerminalCheckpoint | AbsentCheckpoint));
|
|
96
|
+
|
|
97
|
+
type SettledRunnerTerminalTransportResult = Exclude<
|
|
98
|
+
PlayRunnerTerminalTransportResult,
|
|
99
|
+
SuspendedRunnerResult & IncludedResumeCheckpoint
|
|
100
|
+
>;
|
|
101
|
+
|
|
102
|
+
function isTerminalTransportResult(
|
|
103
|
+
result: PlayRunnerResult | PlayRunnerTerminalTransportResult,
|
|
104
|
+
): result is PlayRunnerTerminalTransportResult {
|
|
105
|
+
const disposition = (result as Partial<PlayRunnerTerminalTransportResult>)
|
|
106
|
+
.checkpointDisposition;
|
|
107
|
+
return (
|
|
108
|
+
disposition ===
|
|
109
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.OMITTED_TERMINAL_REPLAY ||
|
|
110
|
+
disposition ===
|
|
111
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.INCLUDED_RESUME_REQUIRED ||
|
|
112
|
+
disposition === RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fitsJsonByteLimit(value: unknown, limitBytes: number): boolean {
|
|
117
|
+
return !jsonByteLengthUpTo(value, limitBytes).exceeded;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function truncateStringForJsonValue(
|
|
121
|
+
value: string,
|
|
122
|
+
limitBytes: number,
|
|
123
|
+
): { value: string; truncated: boolean } {
|
|
124
|
+
if (fitsJsonByteLimit(value, limitBytes)) {
|
|
125
|
+
return { value, truncated: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const suffix = '\u2026 [truncated]';
|
|
129
|
+
let low = 0;
|
|
130
|
+
let high = value.length;
|
|
131
|
+
let best = suffix;
|
|
132
|
+
while (low <= high) {
|
|
133
|
+
const middle = Math.floor((low + high) / 2);
|
|
134
|
+
const candidate = `${value.slice(0, middle)}${suffix}`;
|
|
135
|
+
if (fitsJsonByteLimit(candidate, limitBytes)) {
|
|
136
|
+
best = candidate;
|
|
137
|
+
low = middle + 1;
|
|
138
|
+
} else {
|
|
139
|
+
high = middle - 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { value: best, truncated: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function retainArrayWithinJsonByteLimit<T>(
|
|
146
|
+
values: readonly T[],
|
|
147
|
+
limitBytes: number,
|
|
148
|
+
direction: 'head' | 'tail',
|
|
149
|
+
): { values: T[]; truncated: boolean } {
|
|
150
|
+
const retained: T[] = [];
|
|
151
|
+
const ordered = direction === 'tail' ? [...values].reverse() : values;
|
|
152
|
+
for (const value of ordered) {
|
|
153
|
+
const candidate =
|
|
154
|
+
direction === 'tail' ? [value, ...retained] : [...retained, value];
|
|
155
|
+
if (!fitsJsonByteLimit(candidate, limitBytes)) {
|
|
156
|
+
return { values: retained, truncated: true };
|
|
157
|
+
}
|
|
158
|
+
if (direction === 'tail') {
|
|
159
|
+
retained.unshift(value);
|
|
160
|
+
} else {
|
|
161
|
+
retained.push(value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { values: retained, truncated: false };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Retain the normal newest log tail, but reserve space for the canonical
|
|
169
|
+
* ctx.fetch failure diagnostics first. `runtimeDiagnostics` powers the final
|
|
170
|
+
* warning, while the matching raw log line is the durable evidence customers
|
|
171
|
+
* see through `runs logs --debug` after progress events could not be pushed.
|
|
172
|
+
*/
|
|
173
|
+
function retainTerminalLogTailWithRuntimeDiagnostics(logs: readonly string[]): {
|
|
174
|
+
values: string[];
|
|
175
|
+
truncated: boolean;
|
|
176
|
+
} {
|
|
177
|
+
const retainedIndexes = new Set<number>();
|
|
178
|
+
let retainedBytes = 2; // JSON array brackets.
|
|
179
|
+
const retainIfWithinLimit = (index: number): void => {
|
|
180
|
+
// String encodings and separators contribute independently to a JSON
|
|
181
|
+
// array. Re-serializing the retained 128 KiB tail for every historical
|
|
182
|
+
// line made large completed runs exhaust their terminal-flush deadline.
|
|
183
|
+
const separatorBytes = retainedIndexes.size ? 1 : 0;
|
|
184
|
+
const measurement = jsonByteLengthUpTo(
|
|
185
|
+
logs[index]!,
|
|
186
|
+
TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES - retainedBytes - separatorBytes,
|
|
187
|
+
);
|
|
188
|
+
if (measurement.exceeded) return;
|
|
189
|
+
retainedBytes += measurement.bytes + separatorBytes;
|
|
190
|
+
retainedIndexes.add(index);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// The newest diagnostics are the most relevant if malformed external output
|
|
194
|
+
// somehow exceeds the terminal transport's bounded diagnostic allowance.
|
|
195
|
+
for (let index = logs.length - 1; index >= 0; index -= 1) {
|
|
196
|
+
const line = logs[index]!;
|
|
197
|
+
if (!parseCtxFetchHttpFailureDiagnostic(line)) continue;
|
|
198
|
+
retainIfWithinLimit(index);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let index = logs.length - 1; index >= 0; index -= 1) {
|
|
202
|
+
if (retainedIndexes.has(index)) continue;
|
|
203
|
+
retainIfWithinLimit(index);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const indexes = [...retainedIndexes].sort((left, right) => left - right);
|
|
207
|
+
return {
|
|
208
|
+
values: indexes.map((index) => logs[index]!),
|
|
209
|
+
truncated: indexes.length !== logs.length,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function retainRecordWithinJsonByteLimit(
|
|
214
|
+
value: Record<string, unknown>,
|
|
215
|
+
limitBytes: number,
|
|
216
|
+
): { value: Record<string, unknown>; truncated: boolean } {
|
|
217
|
+
if (fitsJsonByteLimit(value, limitBytes)) {
|
|
218
|
+
return { value, truncated: false };
|
|
219
|
+
}
|
|
220
|
+
const retained: Record<string, unknown> = {};
|
|
221
|
+
let truncated = false;
|
|
222
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
223
|
+
const candidate = { ...retained, [key]: entry };
|
|
224
|
+
if (fitsJsonByteLimit(candidate, limitBytes)) {
|
|
225
|
+
retained[key] = entry;
|
|
226
|
+
} else {
|
|
227
|
+
truncated = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { value: retained, truncated };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function appendTerminalDiagnosticsMarker(
|
|
234
|
+
logs: string[],
|
|
235
|
+
truncatedFields: string[],
|
|
236
|
+
): string[] {
|
|
237
|
+
const marker =
|
|
238
|
+
`[deepline] terminal diagnostic metadata was bounded (${truncatedFields.join(', ')}); ` +
|
|
239
|
+
'the authored play output is complete.';
|
|
240
|
+
const marked = [marker, ...logs];
|
|
241
|
+
while (
|
|
242
|
+
marked.length > 1 &&
|
|
243
|
+
!fitsJsonByteLimit(marked, TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES)
|
|
244
|
+
) {
|
|
245
|
+
// Preserve canonical ctx.fetch evidence when a marker itself needs room.
|
|
246
|
+
// Other tail lines are diagnostic context, but this line is the durable
|
|
247
|
+
// proof behind the final run warning and `runs logs --debug` explanation.
|
|
248
|
+
const removableIndex = marked.findIndex(
|
|
249
|
+
(line, index) =>
|
|
250
|
+
index > 0 && parseCtxFetchHttpFailureDiagnostic(line) === null,
|
|
251
|
+
);
|
|
252
|
+
marked.splice(removableIndex === -1 ? 1 : removableIndex, 1);
|
|
253
|
+
}
|
|
254
|
+
return fitsJsonByteLimit(marked, TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES)
|
|
255
|
+
? marked
|
|
256
|
+
: [marker];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `logs`, `stats`, and rendered `steps` are runner diagnostics. Unlike
|
|
261
|
+
* `output`, they have no customer-return contract and can grow with every map
|
|
262
|
+
* row. Project them before stdout and gateway settlement so a complete 5 MiB
|
|
263
|
+
* result is always recoverable after a primary Daytona push failure.
|
|
264
|
+
*/
|
|
265
|
+
function boundSettledTerminalTransportDiagnostics(
|
|
266
|
+
result: SettledRunnerTerminalTransportResult,
|
|
267
|
+
): SettledRunnerTerminalTransportResult {
|
|
268
|
+
const logProjection = retainTerminalLogTailWithRuntimeDiagnostics(
|
|
269
|
+
result.logs,
|
|
270
|
+
);
|
|
271
|
+
const statsProjection = retainRecordWithinJsonByteLimit(
|
|
272
|
+
result.stats,
|
|
273
|
+
TERMINAL_TRANSPORT_STATS_MAX_BYTES,
|
|
274
|
+
);
|
|
275
|
+
const stepsProjection = retainArrayWithinJsonByteLimit(
|
|
276
|
+
result.steps,
|
|
277
|
+
TERMINAL_TRANSPORT_STEPS_MAX_BYTES,
|
|
278
|
+
'head',
|
|
279
|
+
);
|
|
280
|
+
const tableNamespace =
|
|
281
|
+
typeof result.tableNamespace === 'string'
|
|
282
|
+
? truncateStringForJsonValue(
|
|
283
|
+
result.tableNamespace,
|
|
284
|
+
TERMINAL_TRANSPORT_TABLE_NAMESPACE_MAX_BYTES,
|
|
285
|
+
)
|
|
286
|
+
: { value: result.tableNamespace, truncated: false };
|
|
287
|
+
const runtimeDiagnosticsProjection = retainArrayWithinJsonByteLimit(
|
|
288
|
+
result.status === 'completed' ? (result.runtimeDiagnostics ?? []) : [],
|
|
289
|
+
TERMINAL_TRANSPORT_RUNTIME_DIAGNOSTICS_MAX_BYTES,
|
|
290
|
+
'head',
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const truncatedFields = [
|
|
294
|
+
...(logProjection.truncated ? ['logs'] : []),
|
|
295
|
+
...(statsProjection.truncated ? ['stats'] : []),
|
|
296
|
+
...(stepsProjection.truncated ? ['steps'] : []),
|
|
297
|
+
...(tableNamespace.truncated ? ['tableNamespace'] : []),
|
|
298
|
+
...(runtimeDiagnosticsProjection.truncated ? ['runtimeDiagnostics'] : []),
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
if (result.status === 'completed') {
|
|
302
|
+
const warningsProjection = retainArrayWithinJsonByteLimit(
|
|
303
|
+
result.outputWarnings ?? [],
|
|
304
|
+
TERMINAL_TRANSPORT_OUTPUT_WARNINGS_MAX_BYTES,
|
|
305
|
+
'head',
|
|
306
|
+
);
|
|
307
|
+
if (warningsProjection.truncated) truncatedFields.push('outputWarnings');
|
|
308
|
+
return {
|
|
309
|
+
...result,
|
|
310
|
+
logs:
|
|
311
|
+
truncatedFields.length > 0
|
|
312
|
+
? appendTerminalDiagnosticsMarker(
|
|
313
|
+
logProjection.values,
|
|
314
|
+
truncatedFields,
|
|
315
|
+
)
|
|
316
|
+
: logProjection.values,
|
|
317
|
+
stats: statsProjection.value,
|
|
318
|
+
steps: stepsProjection.values,
|
|
319
|
+
...(result.outputWarnings
|
|
320
|
+
? { outputWarnings: warningsProjection.values }
|
|
321
|
+
: {}),
|
|
322
|
+
...(result.runtimeDiagnostics
|
|
323
|
+
? { runtimeDiagnostics: runtimeDiagnosticsProjection.values }
|
|
324
|
+
: {}),
|
|
325
|
+
...(typeof tableNamespace.value === 'string'
|
|
326
|
+
? { tableNamespace: tableNamespace.value }
|
|
327
|
+
: {}),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const error = truncateStringForJsonValue(
|
|
332
|
+
result.error,
|
|
333
|
+
TERMINAL_TRANSPORT_ERROR_MAX_BYTES,
|
|
334
|
+
);
|
|
335
|
+
const errorsProjection = retainArrayWithinJsonByteLimit(
|
|
336
|
+
result.errors ?? [],
|
|
337
|
+
TERMINAL_TRANSPORT_FAILURE_DETAILS_MAX_BYTES,
|
|
338
|
+
'head',
|
|
339
|
+
);
|
|
340
|
+
if (error.truncated) truncatedFields.push('error');
|
|
341
|
+
if (errorsProjection.truncated) truncatedFields.push('errors');
|
|
342
|
+
return {
|
|
343
|
+
...result,
|
|
344
|
+
error: error.value,
|
|
345
|
+
logs:
|
|
346
|
+
truncatedFields.length > 0
|
|
347
|
+
? appendTerminalDiagnosticsMarker(logProjection.values, truncatedFields)
|
|
348
|
+
: logProjection.values,
|
|
349
|
+
stats: statsProjection.value,
|
|
350
|
+
steps: stepsProjection.values,
|
|
351
|
+
...(result.errors ? { errors: errorsProjection.values } : {}),
|
|
352
|
+
...(typeof tableNamespace.value === 'string'
|
|
353
|
+
? { tableNamespace: tableNamespace.value }
|
|
354
|
+
: {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function runnerTerminalResultForTransport(
|
|
359
|
+
result: PlayRunnerResult | PlayRunnerTerminalTransportResult,
|
|
360
|
+
): PlayRunnerTerminalTransportResult {
|
|
361
|
+
let transportResult: PlayRunnerTerminalTransportResult;
|
|
362
|
+
if (isTerminalTransportResult(result)) {
|
|
363
|
+
transportResult = result;
|
|
364
|
+
} else {
|
|
365
|
+
// `isTerminalTransportResult` separates the optional transport shape from
|
|
366
|
+
// the runner's canonical result. Keep the raw result type here so a
|
|
367
|
+
// completed checkpoint remains required by that canonical contract.
|
|
368
|
+
const rawResult = result as PlayRunnerResult;
|
|
369
|
+
if (rawResult.status === 'suspended') {
|
|
370
|
+
transportResult = {
|
|
371
|
+
...rawResult,
|
|
372
|
+
checkpointDisposition:
|
|
373
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.INCLUDED_RESUME_REQUIRED,
|
|
374
|
+
checkpointBytes: jsonByteLength(rawResult.checkpoint),
|
|
375
|
+
};
|
|
376
|
+
} else {
|
|
377
|
+
const { checkpoint, ...terminal } = rawResult;
|
|
378
|
+
transportResult =
|
|
379
|
+
checkpoint === null || checkpoint === undefined
|
|
380
|
+
? {
|
|
381
|
+
...terminal,
|
|
382
|
+
checkpointDisposition:
|
|
383
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT,
|
|
384
|
+
checkpointBytes: 0,
|
|
385
|
+
}
|
|
386
|
+
: {
|
|
387
|
+
...terminal,
|
|
388
|
+
checkpointDisposition:
|
|
389
|
+
RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.OMITTED_TERMINAL_REPLAY,
|
|
390
|
+
checkpointBytes: jsonByteLength(checkpoint),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (transportResult.status === 'suspended') {
|
|
395
|
+
// A suspended result retains its exact checkpoint because it is needed to
|
|
396
|
+
// resume. Its checkpoint follows the dedicated receipt transport contract.
|
|
397
|
+
return transportResult;
|
|
398
|
+
}
|
|
399
|
+
const bounded = boundSettledTerminalTransportDiagnostics(transportResult);
|
|
400
|
+
if (!fitsJsonByteLimit(bounded, TERMINAL_RUN_RESULT_MAX_BYTES)) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
'Terminal result exceeded its durable envelope after runner diagnostics were bounded.',
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return bounded;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export interface PushRunnerTerminalOptions {
|
|
409
|
+
gatewayBaseUrl: string;
|
|
410
|
+
executorToken: string;
|
|
411
|
+
runId: string;
|
|
412
|
+
result: PlayRunnerResult | PlayRunnerTerminalTransportResult;
|
|
413
|
+
finalProgressEvents?: PlayRunnerEvent[];
|
|
414
|
+
finalProgressLogOffset?: number;
|
|
415
|
+
/** Injectable for tests. Defaults to a bearer POST to the gateway. */
|
|
416
|
+
fetchImpl?: typeof fetch;
|
|
417
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
418
|
+
nowImpl?: () => number;
|
|
419
|
+
createTransportAttemptId?: () => string;
|
|
420
|
+
/** Per-attempt HTTP deadline. A hung gateway request must not suppress the
|
|
421
|
+
* remaining bounded retries. */
|
|
422
|
+
requestTimeoutMs?: number;
|
|
423
|
+
warn?: (message: string, detail: Record<string, unknown>) => void;
|
|
424
|
+
onTransport?: (event: RunnerGatewayTransportEvent) => void;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
type RunnerTerminalRequestBody = {
|
|
428
|
+
action: 'runner_terminal';
|
|
429
|
+
runId: string;
|
|
430
|
+
result: PlayRunnerTerminalTransportResult;
|
|
431
|
+
finalProgressLogOffset?: number;
|
|
432
|
+
finalProgressEvents?: PlayRunnerEvent[];
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
function durableProgressLogCount(events: readonly PlayRunnerEvent[]): number {
|
|
436
|
+
return events.reduce(
|
|
437
|
+
(count, event) =>
|
|
438
|
+
event.type === 'log' && event.source !== 'worker' && event.line.length > 0
|
|
439
|
+
? count + 1
|
|
440
|
+
: count,
|
|
441
|
+
0,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Keep the newest recovery observations without ever exceeding the gateway. */
|
|
446
|
+
function terminalRequestBody(input: {
|
|
447
|
+
runId: string;
|
|
448
|
+
result: PlayRunnerTerminalTransportResult;
|
|
449
|
+
finalProgressEvents?: PlayRunnerEvent[];
|
|
450
|
+
finalProgressLogOffset?: number;
|
|
451
|
+
}): { body: RunnerTerminalRequestBody; droppedProgressEvents: number } {
|
|
452
|
+
const events = input.finalProgressEvents ?? [];
|
|
453
|
+
const bodyFor = (
|
|
454
|
+
droppedProgressEvents: number,
|
|
455
|
+
): RunnerTerminalRequestBody => {
|
|
456
|
+
const retainedEvents = events.slice(droppedProgressEvents);
|
|
457
|
+
return {
|
|
458
|
+
action: 'runner_terminal',
|
|
459
|
+
runId: input.runId,
|
|
460
|
+
result: input.result,
|
|
461
|
+
...(input.finalProgressLogOffset !== undefined
|
|
462
|
+
? {
|
|
463
|
+
finalProgressLogOffset:
|
|
464
|
+
input.finalProgressLogOffset +
|
|
465
|
+
durableProgressLogCount(events.slice(0, droppedProgressEvents)),
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
468
|
+
...(retainedEvents.length ? { finalProgressEvents: retainedEvents } : {}),
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (
|
|
473
|
+
!jsonByteLengthUpTo(bodyFor(0), RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES)
|
|
474
|
+
.exceeded
|
|
475
|
+
) {
|
|
476
|
+
return { body: bodyFor(0), droppedProgressEvents: 0 };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Dropping a prefix is monotonic for ordinary PlayRunnerEvent JSON. Binary
|
|
480
|
+
// search avoids repeatedly serializing a potentially multi-megabyte tail.
|
|
481
|
+
let low = 1;
|
|
482
|
+
let high = events.length;
|
|
483
|
+
let retained = bodyFor(events.length);
|
|
484
|
+
let droppedProgressEvents = events.length;
|
|
485
|
+
while (low <= high) {
|
|
486
|
+
const middle = Math.floor((low + high) / 2);
|
|
487
|
+
const candidate = bodyFor(middle);
|
|
488
|
+
if (
|
|
489
|
+
jsonByteLengthUpTo(candidate, RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES)
|
|
490
|
+
.exceeded
|
|
491
|
+
) {
|
|
492
|
+
low = middle + 1;
|
|
493
|
+
} else {
|
|
494
|
+
retained = candidate;
|
|
495
|
+
droppedProgressEvents = middle;
|
|
496
|
+
high = middle - 1;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return { body: retained, droppedProgressEvents };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function pushRunnerTerminal(
|
|
503
|
+
options: PushRunnerTerminalOptions,
|
|
504
|
+
): Promise<boolean> {
|
|
505
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
506
|
+
const now = options.nowImpl ?? Date.now;
|
|
507
|
+
const sleepImpl =
|
|
508
|
+
options.sleepImpl ??
|
|
509
|
+
((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
510
|
+
const warn =
|
|
511
|
+
options.warn ??
|
|
512
|
+
((message, detail) =>
|
|
513
|
+
console.warn(`[play-runner.terminal-push] ${message}`, detail));
|
|
514
|
+
const url = `${options.gatewayBaseUrl.replace(/\/$/, '')}${APP_RUNTIME_COMPAT_PATH}`;
|
|
515
|
+
const result = runnerTerminalResultForTransport(options.result);
|
|
516
|
+
const request = terminalRequestBody({
|
|
517
|
+
runId: options.runId,
|
|
518
|
+
result,
|
|
519
|
+
finalProgressEvents: options.finalProgressEvents,
|
|
520
|
+
finalProgressLogOffset: options.finalProgressLogOffset,
|
|
521
|
+
});
|
|
522
|
+
if (request.droppedProgressEvents > 0) {
|
|
523
|
+
warn('terminal_progress_truncated', {
|
|
524
|
+
runId: options.runId,
|
|
525
|
+
retainedEvents: request.body.finalProgressEvents?.length ?? 0,
|
|
526
|
+
droppedEvents: request.droppedProgressEvents,
|
|
527
|
+
requestBytes: jsonByteLength(request.body),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const requestBody = JSON.stringify(request.body);
|
|
531
|
+
let lastError = '';
|
|
532
|
+
for (let attempt = 1; attempt <= TERMINAL_PUSH_MAX_ATTEMPTS; attempt += 1) {
|
|
533
|
+
const startedAt = now();
|
|
534
|
+
const transportAttemptId =
|
|
535
|
+
options.createTransportAttemptId?.() ??
|
|
536
|
+
createRunnerGatewayTransportAttemptId();
|
|
537
|
+
const reportTransport = (
|
|
538
|
+
outcome: RunnerGatewayTransportEvent['outcome'],
|
|
539
|
+
detail: Pick<RunnerGatewayTransportEvent, 'status' | 'errorCode'> = {},
|
|
540
|
+
) => {
|
|
541
|
+
const event: RunnerGatewayTransportEvent = {
|
|
542
|
+
operation: 'runner_terminal',
|
|
543
|
+
attempt,
|
|
544
|
+
transportAttemptId,
|
|
545
|
+
durationMs: Math.max(0, now() - startedAt),
|
|
546
|
+
outcome,
|
|
547
|
+
...detail,
|
|
548
|
+
};
|
|
549
|
+
if (outcome !== 'ok') {
|
|
550
|
+
warn('gateway_transport', { runId: options.runId, ...event });
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
options.onTransport?.(event);
|
|
554
|
+
} catch {
|
|
555
|
+
// Diagnostics must not interfere with terminal persistence or salvage.
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const requestController = new AbortController();
|
|
559
|
+
const requestTimeout = setTimeout(
|
|
560
|
+
() =>
|
|
561
|
+
requestController.abort(
|
|
562
|
+
new Error(
|
|
563
|
+
`runner_terminal request exceeded ${
|
|
564
|
+
options.requestTimeoutMs ?? TERMINAL_PUSH_REQUEST_TIMEOUT_MS
|
|
565
|
+
}ms`,
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
options.requestTimeoutMs ?? TERMINAL_PUSH_REQUEST_TIMEOUT_MS,
|
|
569
|
+
);
|
|
570
|
+
requestTimeout.unref?.();
|
|
571
|
+
try {
|
|
572
|
+
const response = await fetchImpl(url, {
|
|
573
|
+
method: 'POST',
|
|
574
|
+
headers: {
|
|
575
|
+
'content-type': 'application/json',
|
|
576
|
+
authorization: `Bearer ${options.executorToken}`,
|
|
577
|
+
[PLAY_RUNTIME_TRANSPORT_ATTEMPT_HEADER]: transportAttemptId,
|
|
578
|
+
},
|
|
579
|
+
body: requestBody,
|
|
580
|
+
redirect: 'manual',
|
|
581
|
+
signal: requestController.signal,
|
|
582
|
+
});
|
|
583
|
+
if (response.ok) {
|
|
584
|
+
await response.text().catch(() => '');
|
|
585
|
+
reportTransport('ok', { status: response.status });
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
const body = await response.text().catch(() => '');
|
|
589
|
+
lastError = `runner_terminal failed (${response.status}): ${body}`;
|
|
590
|
+
reportTransport(
|
|
591
|
+
response.status >= 500
|
|
592
|
+
? 'gateway_5xx'
|
|
593
|
+
: response.status >= 400
|
|
594
|
+
? 'gateway_4xx'
|
|
595
|
+
: 'redirect',
|
|
596
|
+
{ status: response.status },
|
|
597
|
+
);
|
|
598
|
+
// 4xx (scope/fence rejections) are deterministic — retrying cannot help.
|
|
599
|
+
if (response.status >= 400 && response.status < 500) {
|
|
600
|
+
warn('terminal_push_rejected', {
|
|
601
|
+
runId: options.runId,
|
|
602
|
+
status: response.status,
|
|
603
|
+
body,
|
|
604
|
+
});
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
609
|
+
reportTransport('network_error', {
|
|
610
|
+
errorCode: runnerGatewayTransportErrorCode(error),
|
|
611
|
+
});
|
|
612
|
+
} finally {
|
|
613
|
+
clearTimeout(requestTimeout);
|
|
614
|
+
}
|
|
615
|
+
if (attempt < TERMINAL_PUSH_MAX_ATTEMPTS) {
|
|
616
|
+
await sleepImpl(TERMINAL_PUSH_RETRY_DELAYS_MS[attempt - 1] ?? 4_000);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
warn('terminal_push_exhausted', {
|
|
620
|
+
runId: options.runId,
|
|
621
|
+
attempts: TERMINAL_PUSH_MAX_ATTEMPTS,
|
|
622
|
+
lastError,
|
|
623
|
+
});
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
@@ -319,6 +319,7 @@ const RUNTIME_SHEET_ATTEMPT_COLUMNS = [
|
|
|
319
319
|
'_attempt_seq',
|
|
320
320
|
'_attempt_expires_at',
|
|
321
321
|
] as const;
|
|
322
|
+
const RUNTIME_SHEET_TIMESTAMP_COLUMNS = ['_created_at', '_updated_at'] as const;
|
|
322
323
|
const RUNTIME_SHEET_ENSURE_CACHE_TTL_MS = 10 * 60_000;
|
|
323
324
|
const RUNTIME_SHEET_READY_AFTER_ENSURE_RETRY_DELAYS_MS = [
|
|
324
325
|
100, 250, 500, 1_000, 2_000,
|
|
@@ -1007,9 +1008,28 @@ async function isRuntimeSheetSchemaReady(
|
|
|
1007
1008
|
[...RUNTIME_SHEET_ATTEMPT_COLUMNS],
|
|
1008
1009
|
],
|
|
1009
1010
|
);
|
|
1011
|
+
const timestampColumns = await client.query<Record<string, unknown>>(
|
|
1012
|
+
`
|
|
1013
|
+
SELECT count(*)::int AS ready_count
|
|
1014
|
+
FROM information_schema.columns
|
|
1015
|
+
WHERE table_schema = $1
|
|
1016
|
+
AND table_name = $2
|
|
1017
|
+
AND column_name = ANY($3::text[])
|
|
1018
|
+
AND data_type = 'timestamp with time zone'
|
|
1019
|
+
AND is_nullable = 'NO'
|
|
1020
|
+
AND column_default IS NOT NULL
|
|
1021
|
+
`,
|
|
1022
|
+
[
|
|
1023
|
+
session.postgres.schema,
|
|
1024
|
+
session.postgres.sheetTable,
|
|
1025
|
+
[...RUNTIME_SHEET_TIMESTAMP_COLUMNS],
|
|
1026
|
+
],
|
|
1027
|
+
);
|
|
1010
1028
|
return (
|
|
1011
1029
|
Number(attemptColumns.rows[0]?.present_count ?? 0) ===
|
|
1012
|
-
|
|
1030
|
+
RUNTIME_SHEET_ATTEMPT_COLUMNS.length &&
|
|
1031
|
+
Number(timestampColumns.rows[0]?.ready_count ?? 0) ===
|
|
1032
|
+
RUNTIME_SHEET_TIMESTAMP_COLUMNS.length
|
|
1013
1033
|
);
|
|
1014
1034
|
};
|
|
1015
1035
|
try {
|
|
@@ -1040,9 +1060,14 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1040
1060
|
const query = async (
|
|
1041
1061
|
client: RuntimeQueryClient,
|
|
1042
1062
|
): Promise<Record<string, unknown>> => {
|
|
1043
|
-
const visibleColumns = await client.query<{
|
|
1063
|
+
const visibleColumns = await client.query<{
|
|
1064
|
+
column_name: string;
|
|
1065
|
+
data_type: string;
|
|
1066
|
+
is_nullable: string;
|
|
1067
|
+
column_default: string | null;
|
|
1068
|
+
}>(
|
|
1044
1069
|
`
|
|
1045
|
-
SELECT column_name
|
|
1070
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
1046
1071
|
FROM information_schema.columns
|
|
1047
1072
|
WHERE table_schema = $1
|
|
1048
1073
|
AND table_name = $2
|
|
@@ -1066,6 +1091,15 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1066
1091
|
],
|
|
1067
1092
|
);
|
|
1068
1093
|
const visible = new Set(visibleColumns.rows.map((row) => row.column_name));
|
|
1094
|
+
const timestampColumns = new Map(
|
|
1095
|
+
visibleColumns.rows
|
|
1096
|
+
.filter((row) =>
|
|
1097
|
+
RUNTIME_SHEET_TIMESTAMP_COLUMNS.includes(
|
|
1098
|
+
row.column_name as (typeof RUNTIME_SHEET_TIMESTAMP_COLUMNS)[number],
|
|
1099
|
+
),
|
|
1100
|
+
)
|
|
1101
|
+
.map((row) => [row.column_name, row]),
|
|
1102
|
+
);
|
|
1069
1103
|
return {
|
|
1070
1104
|
schema: session.postgres.schema,
|
|
1071
1105
|
table: session.postgres.sheetTable,
|
|
@@ -1078,6 +1112,15 @@ async function readRuntimeSheetSchemaDiagnostic(
|
|
|
1078
1112
|
missingAttemptColumns: RUNTIME_SHEET_ATTEMPT_COLUMNS.filter(
|
|
1079
1113
|
(column) => !visible.has(column),
|
|
1080
1114
|
),
|
|
1115
|
+
timestampColumnStates: RUNTIME_SHEET_TIMESTAMP_COLUMNS.map((column) => {
|
|
1116
|
+
const state = timestampColumns.get(column);
|
|
1117
|
+
return {
|
|
1118
|
+
column,
|
|
1119
|
+
dataType: state?.data_type ?? null,
|
|
1120
|
+
isNullable: state?.is_nullable ?? null,
|
|
1121
|
+
hasDefault: state?.column_default != null,
|
|
1122
|
+
};
|
|
1123
|
+
}),
|
|
1081
1124
|
};
|
|
1082
1125
|
};
|
|
1083
1126
|
try {
|