feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,104 @@
import type { RunAttemptStatus } from '../run/run';
export const LOCAL_COMPLETION_RECEIPT_JOURNAL_STATES = [
'pending',
'quarantined',
] as const;
export const MAX_LOCAL_COMPLETION_RECEIPT_JOURNAL_PAGE = 64;
export type LocalCompletionReceiptJournalState =
(typeof LOCAL_COMPLETION_RECEIPT_JOURNAL_STATES)[number];
export interface LocalCompletionReceiptJournalRecord {
readonly attemptId: string;
readonly runId: string;
readonly state: LocalCompletionReceiptJournalState;
readonly registeredAtMs: number;
readonly updatedAtMs: number;
readonly quarantineRef?: string;
readonly purgeAfterMs?: number;
}
export interface LocalCompletionReceiptJournalCandidate
extends LocalCompletionReceiptJournalRecord {
readonly attemptStatus: RunAttemptStatus;
readonly executorType: string;
readonly finishedAtMs?: number;
}
export interface LocalCompletionReceiptJournalCursor {
readonly updatedAtMs: number;
readonly attemptId: string;
}
export interface LocalCompletionReceiptJournalPage {
readonly candidates: readonly LocalCompletionReceiptJournalCandidate[];
readonly truncated: boolean;
readonly nextCursor?: LocalCompletionReceiptJournalCursor;
}
export interface RegisterLocalCompletionReceiptCommand {
readonly attemptId: string;
readonly runId: string;
readonly registeredAtMs: number;
}
export interface QuarantineLocalCompletionReceiptCommand {
readonly attemptId: string;
readonly quarantineRef: string;
readonly purgeAfterMs: number;
readonly updatedAtMs: number;
}
export interface LocalCompletionReceiptJournal {
register(command: RegisterLocalCompletionReceiptCommand): Promise<void>;
markQuarantined(
command: QuarantineLocalCompletionReceiptCommand,
): Promise<void>;
resolve(attemptId: string): Promise<boolean>;
listCandidates(options: {
readonly observedAtMs: number;
readonly cursor?: LocalCompletionReceiptJournalCursor;
readonly limit?: number;
}): Promise<LocalCompletionReceiptJournalPage>;
}
const PORTABLE_LOCAL_EXECUTION_ID =
/^[A-Za-z0-9][A-Za-z0-9._:-]{0,35}$/;
export function assertLocalCompletionReceiptId(
value: string,
field: 'runId' | 'attemptId',
): void {
if (!PORTABLE_LOCAL_EXECUTION_ID.test(value)) {
throw new TypeError(`${field} must be a bounded portable execution ID`);
}
}
export function assertLocalCompletionReceiptTimestamp(
value: number,
field: string,
): void {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${field} must be a non-negative safe integer`);
}
}
export function assertLocalCompletionReceiptJournalCursor(
cursor: LocalCompletionReceiptJournalCursor,
): void {
assertLocalCompletionReceiptTimestamp(cursor.updatedAtMs, 'updatedAtMs');
assertLocalCompletionReceiptId(cursor.attemptId, 'attemptId');
}
export function assertLocalCompletionReceiptJournalLimit(limit: number): void {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_LOCAL_COMPLETION_RECEIPT_JOURNAL_PAGE
) {
throw new RangeError(
`limit must be between 1 and ${MAX_LOCAL_COMPLETION_RECEIPT_JOURNAL_PAGE}`,
);
}
}
@@ -0,0 +1,453 @@
import { createHash } from 'node:crypto';
export type { LocalSecretEnvironmentProvider } from '../secret/localSecret';
export const LOCAL_PROCESS_EXECUTOR_TYPE = 'local_process';
export const MAX_LOCAL_DISPATCH_PAGE_SIZE = 64;
export const MAX_LOCAL_DISPATCH_ENVIRONMENT_ENTRIES = 256;
export const MAX_LOCAL_DISPATCH_ENVIRONMENT_BYTES = 64 * 1024;
export const MAX_LOCAL_DISPATCH_SECRET_REFS = 64;
export const MAX_LOCAL_DISPATCH_TIMEOUT_MS = 365 * 24 * 60 * 60_000;
const CONTEXT_REF_PATTERN = /^localctx:sha256:[a-f0-9]{64}$/;
const ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
export type LocalDispatchCommand =
| Readonly<{
kind: 'argv';
file: string;
args: readonly string[];
}>
| Readonly<{
kind: 'shell';
command: string;
shell?: '/bin/sh' | '/bin/bash';
}>;
export interface LocalDispatchCandidateCursor {
readonly priority: number;
readonly queuedAtMs: number;
readonly attemptCreatedAtMs: number;
readonly attemptId: string;
}
export interface LocalDispatchCandidate extends LocalDispatchCandidateCursor {
readonly runId: string;
readonly stepRunId?: string;
readonly projectId: string;
readonly taskId: string;
readonly taskRevision: string;
readonly attemptNumber: number;
readonly executorType: typeof LOCAL_PROCESS_EXECUTOR_TYPE;
}
export interface LocalDispatchCandidatePage {
readonly candidates: readonly LocalDispatchCandidate[];
readonly truncated: boolean;
}
export interface LocalTaskExecutionRevisionContent {
readonly projectId: string;
readonly taskId: string;
readonly taskRevision: string;
readonly executorType: typeof LOCAL_PROCESS_EXECUTOR_TYPE;
readonly command: LocalDispatchCommand;
readonly workingDirectory?: string;
readonly timeoutMs?: number;
readonly contextRef: string;
readonly createdAtMs: number;
}
export interface LocalTaskExecutionRevision
extends LocalTaskExecutionRevisionContent {
readonly contentDigest: string;
}
export type LocalExecutionEnvironmentBinding =
| Readonly<{ name: string; kind: 'public'; value: string }>
| Readonly<{ name: string; kind: 'secret'; secretRef: string }>;
export interface LocalExecutionContextRecipe {
readonly contextRef: string;
readonly environment: readonly LocalExecutionEnvironmentBinding[];
readonly contentDigest: string;
readonly createdAtMs: number;
}
export interface LocalDispatchCandidateSource {
listLocalDispatchCandidates(options: {
readonly limit: number;
readonly after?: LocalDispatchCandidateCursor;
}): Promise<LocalDispatchCandidatePage>;
}
export interface LocalTaskExecutionRevisionSource {
resolveLocalTaskExecutionRevision(identity: {
readonly projectId: string;
readonly taskId: string;
readonly taskRevision: string;
}): Promise<LocalTaskExecutionRevision | null>;
}
export interface LocalExecutionContextRecipeSource {
resolveLocalExecutionContextRecipe(
contextRef: string,
): Promise<LocalExecutionContextRecipe | null>;
}
export interface LocalDispatchDefinitionWriter {
appendLocalExecutionContextRecipe(
recipe: LocalExecutionContextRecipe,
): Promise<'inserted' | 'existing'>;
appendLocalTaskExecutionRevision(
revision: LocalTaskExecutionRevision,
): Promise<'inserted' | 'existing'>;
}
export interface LocalDispatchStore
extends LocalDispatchCandidateSource,
LocalTaskExecutionRevisionSource,
LocalExecutionContextRecipeSource,
LocalDispatchDefinitionWriter {}
function assertBoundedText(
value: unknown,
field: string,
maximumBytes: number,
): asserts value is string {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.includes('\0') ||
Buffer.byteLength(value, 'utf8') > maximumBytes
) {
throw new TypeError(`${field} is invalid`);
}
}
function assertIdentifier(
value: unknown,
field: string,
): asserts value is string {
assertBoundedText(value, field, 255);
if (/\r|\n|[\u0001-\u001f\u007f]/.test(value)) {
throw new TypeError(`${field} is invalid`);
}
}
function assertTimestamp(
value: unknown,
field: string,
): asserts value is number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError(`${field} is invalid`);
}
}
export function assertLocalDispatchContextRef(
value: unknown,
): asserts value is string {
if (typeof value !== 'string' || !CONTEXT_REF_PATTERN.test(value)) {
throw new TypeError('Local dispatch contextRef is invalid');
}
}
export function normalizeLocalDispatchCommand(
command: LocalDispatchCommand,
): LocalDispatchCommand {
if (!command || typeof command !== 'object' || Array.isArray(command)) {
throw new TypeError('Local dispatch command is invalid');
}
if (command.kind === 'argv') {
assertBoundedText(command.file, 'Local dispatch executable', 4096);
if (!command.file.startsWith('/')) {
throw new TypeError('Local dispatch executable must be absolute');
}
if (!Array.isArray(command.args) || command.args.length > 256) {
throw new TypeError('Local dispatch arguments are invalid');
}
let bytes = Buffer.byteLength(command.file, 'utf8');
const args = command.args.map((argument) => {
if (
typeof argument !== 'string' ||
argument.includes('\0') ||
Buffer.byteLength(argument, 'utf8') > 16 * 1024
) {
throw new TypeError('Local dispatch argument is invalid');
}
bytes += Buffer.byteLength(argument, 'utf8');
return argument;
});
if (bytes > 64 * 1024) {
throw new RangeError('Local dispatch command exceeds its byte budget');
}
return Object.freeze({
kind: 'argv' as const,
file: command.file,
args: Object.freeze(args),
});
}
if (command.kind === 'shell') {
assertBoundedText(
command.command,
'Local dispatch shell command',
64 * 1024,
);
if (
command.shell !== undefined &&
command.shell !== '/bin/sh' &&
command.shell !== '/bin/bash'
) {
throw new TypeError('Local dispatch shell is not allowlisted');
}
return Object.freeze({
kind: 'shell' as const,
command: command.command,
...(command.shell === undefined ? {} : { shell: command.shell }),
});
}
throw new TypeError('Local dispatch command kind is invalid');
}
export function normalizeLocalDispatchCandidate(
candidate: LocalDispatchCandidate,
): LocalDispatchCandidate {
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
throw new TypeError('Local dispatch candidate is invalid');
}
assertIdentifier(candidate.runId, 'Local dispatch runId');
assertIdentifier(candidate.attemptId, 'Local dispatch attemptId');
if (candidate.stepRunId !== undefined) {
assertIdentifier(candidate.stepRunId, 'Local dispatch stepRunId');
}
assertIdentifier(candidate.projectId, 'Local dispatch projectId');
assertIdentifier(candidate.taskId, 'Local dispatch taskId');
assertIdentifier(candidate.taskRevision, 'Local dispatch taskRevision');
if (!Number.isSafeInteger(candidate.priority)) {
throw new TypeError('Local dispatch priority is invalid');
}
assertTimestamp(candidate.queuedAtMs, 'Local dispatch queuedAtMs');
assertTimestamp(
candidate.attemptCreatedAtMs,
'Local dispatch attemptCreatedAtMs',
);
if (
!Number.isSafeInteger(candidate.attemptNumber) ||
candidate.attemptNumber < 1
) {
throw new TypeError('Local dispatch attempt number is invalid');
}
if (candidate.executorType !== LOCAL_PROCESS_EXECUTOR_TYPE) {
throw new TypeError('Local dispatch executor type is invalid');
}
return Object.freeze({ ...candidate });
}
export function assertLocalDispatchPageSize(limit: number): void {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_LOCAL_DISPATCH_PAGE_SIZE
) {
throw new RangeError(
`Local dispatch page size must be between 1 and ${MAX_LOCAL_DISPATCH_PAGE_SIZE}`,
);
}
}
function normalizeLocalTaskExecutionRevisionContent(
revision: LocalTaskExecutionRevisionContent,
): LocalTaskExecutionRevisionContent {
if (!revision || typeof revision !== 'object' || Array.isArray(revision)) {
throw new TypeError('Local Task execution revision is invalid');
}
assertIdentifier(revision.projectId, 'Local Task revision projectId');
assertIdentifier(revision.taskId, 'Local Task revision taskId');
assertIdentifier(revision.taskRevision, 'Local Task revision identity');
if (revision.executorType !== LOCAL_PROCESS_EXECUTOR_TYPE) {
throw new TypeError('Local Task revision executor type is invalid');
}
const command = normalizeLocalDispatchCommand(revision.command);
if (revision.workingDirectory !== undefined) {
assertBoundedText(
revision.workingDirectory,
'Local Task working directory',
4096,
);
if (!revision.workingDirectory.startsWith('/')) {
throw new TypeError('Local Task working directory must be absolute');
}
}
if (
revision.timeoutMs !== undefined &&
(!Number.isSafeInteger(revision.timeoutMs) ||
revision.timeoutMs < 1 ||
revision.timeoutMs > MAX_LOCAL_DISPATCH_TIMEOUT_MS)
) {
throw new RangeError('Local Task timeout is invalid');
}
assertLocalDispatchContextRef(revision.contextRef);
assertTimestamp(revision.createdAtMs, 'Local Task revision createdAtMs');
return Object.freeze({
projectId: revision.projectId,
taskId: revision.taskId,
taskRevision: revision.taskRevision,
executorType: LOCAL_PROCESS_EXECUTOR_TYPE,
command,
...(revision.workingDirectory === undefined
? {}
: { workingDirectory: revision.workingDirectory }),
...(revision.timeoutMs === undefined
? {}
: { timeoutMs: revision.timeoutMs }),
contextRef: revision.contextRef,
createdAtMs: revision.createdAtMs,
});
}
function localTaskExecutionRevisionDigestFromCanonical(
revision: LocalTaskExecutionRevisionContent,
): string {
const { createdAtMs: _createdAtMs, ...immutableContent } = revision;
return createHash('sha256')
.update('qinglong.local-task-execution-revision.v1\0', 'utf8')
.update(JSON.stringify(immutableContent), 'utf8')
.digest('hex');
}
export function localTaskExecutionRevisionDigest(
revision: LocalTaskExecutionRevisionContent,
): string {
return localTaskExecutionRevisionDigestFromCanonical(
normalizeLocalTaskExecutionRevisionContent(revision),
);
}
export function createLocalTaskExecutionRevision(
revision: LocalTaskExecutionRevisionContent,
): LocalTaskExecutionRevision {
const content = normalizeLocalTaskExecutionRevisionContent(revision);
return Object.freeze({
...content,
contentDigest: localTaskExecutionRevisionDigestFromCanonical(content),
});
}
export function normalizeLocalTaskExecutionRevision(
revision: LocalTaskExecutionRevision,
): LocalTaskExecutionRevision {
const content = normalizeLocalTaskExecutionRevisionContent(revision);
const contentDigest = localTaskExecutionRevisionDigestFromCanonical(content);
if (
typeof revision.contentDigest !== 'string' ||
!/^[a-f0-9]{64}$/.test(revision.contentDigest) ||
revision.contentDigest !== contentDigest
) {
throw new TypeError(
'Local Task execution revision digest does not match its content',
);
}
return Object.freeze({ ...content, contentDigest });
}
function canonicalBindings(
bindings: readonly LocalExecutionEnvironmentBinding[],
): readonly LocalExecutionEnvironmentBinding[] {
if (
!Array.isArray(bindings) ||
bindings.length > MAX_LOCAL_DISPATCH_ENVIRONMENT_ENTRIES
) {
throw new TypeError('Local context environment bindings are invalid');
}
const names = new Set<string>();
let bytes = 0;
const normalized = bindings.map((binding) => {
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) {
throw new TypeError('Local context environment binding is invalid');
}
if (
!ENVIRONMENT_NAME_PATTERN.test(binding.name) ||
binding.name.startsWith('QL3_') ||
names.has(binding.name)
) {
throw new TypeError(
'Local context environment name is invalid or duplicated',
);
}
names.add(binding.name);
bytes += Buffer.byteLength(binding.name, 'utf8');
if (binding.kind === 'public') {
if (typeof binding.value !== 'string' || binding.value.includes('\0')) {
throw new TypeError('Local context public value is invalid');
}
bytes += Buffer.byteLength(binding.value, 'utf8');
return Object.freeze({
name: binding.name,
kind: 'public' as const,
value: binding.value,
});
}
if (binding.kind === 'secret') {
assertBoundedText(binding.secretRef, 'Local context secretRef', 512);
bytes += Buffer.byteLength(binding.secretRef, 'utf8');
return Object.freeze({
name: binding.name,
kind: 'secret' as const,
secretRef: binding.secretRef,
});
}
throw new TypeError('Local context environment binding kind is invalid');
});
if (bytes > MAX_LOCAL_DISPATCH_ENVIRONMENT_BYTES) {
throw new RangeError('Local context environment exceeds its byte budget');
}
normalized.sort((left, right) => left.name.localeCompare(right.name));
return Object.freeze(normalized);
}
function bindingsDigest(
bindings: readonly LocalExecutionEnvironmentBinding[],
): string {
return createHash('sha256')
.update(JSON.stringify(bindings), 'utf8')
.digest('hex');
}
export function createLocalExecutionContextRecipe(input: {
readonly environment: readonly LocalExecutionEnvironmentBinding[];
readonly createdAtMs: number;
}): LocalExecutionContextRecipe {
assertTimestamp(input.createdAtMs, 'Local context recipe createdAtMs');
const environment = canonicalBindings(input.environment);
const contentDigest = bindingsDigest(environment);
return Object.freeze({
contextRef: `localctx:sha256:${contentDigest}`,
environment,
contentDigest,
createdAtMs: input.createdAtMs,
});
}
export function normalizeLocalExecutionContextRecipe(
recipe: LocalExecutionContextRecipe,
): LocalExecutionContextRecipe {
if (!recipe || typeof recipe !== 'object' || Array.isArray(recipe)) {
throw new TypeError('Local execution context recipe is invalid');
}
assertLocalDispatchContextRef(recipe.contextRef);
assertTimestamp(recipe.createdAtMs, 'Local context recipe createdAtMs');
const environment = canonicalBindings(recipe.environment);
const contentDigest = bindingsDigest(environment);
if (
recipe.contentDigest !== contentDigest ||
recipe.contextRef !== `localctx:sha256:${contentDigest}` ||
JSON.stringify(recipe.environment) !== JSON.stringify(environment)
) {
throw new TypeError('Local execution context recipe is not canonical');
}
return Object.freeze({
contextRef: recipe.contextRef,
environment,
contentDigest,
createdAtMs: recipe.createdAtMs,
});
}
@@ -0,0 +1,225 @@
import type { RunCancellationReason } from '../run/run';
export const MAX_LOCAL_EXECUTION_CONTROL_PAGE = 64;
export const LOCAL_EXECUTION_CONTROL_KINDS = [
'cancellation',
'deadline',
] as const;
export type LocalExecutionControlKind =
(typeof LOCAL_EXECUTION_CONTROL_KINDS)[number];
export interface LocalExecutionControlCandidate {
readonly kind: LocalExecutionControlKind;
readonly runId: string;
readonly attemptId: string;
readonly dueAtMs: number;
readonly cancelReason?: RunCancellationReason;
}
export interface LocalExecutionControlCursor {
readonly dueAtMs: number;
readonly kind: LocalExecutionControlKind;
readonly attemptId: string;
}
export interface LocalExecutionControlPage {
readonly candidates: readonly LocalExecutionControlCandidate[];
readonly truncated: boolean;
readonly nextCursor?: LocalExecutionControlCursor;
}
export interface LocalActiveExecutionCandidate {
readonly runId: string;
readonly attemptId: string;
readonly attemptCreatedAtMs: number;
}
export interface LocalActiveExecutionCursor {
readonly attemptCreatedAtMs: number;
readonly attemptId: string;
}
export interface LocalActiveExecutionPage {
readonly candidates: readonly LocalActiveExecutionCandidate[];
readonly truncated: boolean;
readonly nextCursor?: LocalActiveExecutionCursor;
}
export interface LocalExecutionControlSource {
listLocalExecutionControlCandidates(options: {
readonly observedAtMs: number;
readonly limit: number;
readonly after?: LocalExecutionControlCursor;
}): Promise<LocalExecutionControlPage>;
listLocalActiveExecutions(options: {
readonly limit: number;
readonly after?: LocalActiveExecutionCursor;
}): Promise<LocalActiveExecutionPage>;
}
const CONTROL_CANDIDATE_KEYS = Object.freeze([
'attemptId',
'cancelReason',
'dueAtMs',
'kind',
'runId',
]);
const ACTIVE_CANDIDATE_KEYS = Object.freeze([
'attemptCreatedAtMs',
'attemptId',
'runId',
]);
const CANCELLATION_REASONS = new Set<RunCancellationReason>([
'user',
'policy',
'shutdown',
'reconcile',
'timeout',
]);
function exactKeys(value: Record<string, unknown>, expected: readonly string[]) {
return (
JSON.stringify(Object.keys(value).sort()) ===
JSON.stringify([...expected].sort())
);
}
function boundedIdentity(value: unknown, field: string): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
Buffer.byteLength(value, 'utf8') > 128 ||
/[\0\r\n]/.test(value)
) {
throw new TypeError(`${field} is invalid`);
}
return value;
}
function safeTimestamp(value: unknown, field: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new RangeError(`${field} must be a non-negative safe integer`);
}
return value as number;
}
function controlKind(value: unknown): LocalExecutionControlKind {
if (value !== 'cancellation' && value !== 'deadline') {
throw new TypeError('Local execution control kind is invalid');
}
return value;
}
export function assertLocalExecutionControlLimit(limit: number): void {
if (
!Number.isSafeInteger(limit) ||
limit < 1 ||
limit > MAX_LOCAL_EXECUTION_CONTROL_PAGE
) {
throw new RangeError(
`limit must be between 1 and ${MAX_LOCAL_EXECUTION_CONTROL_PAGE}`,
);
}
}
export function normalizeLocalExecutionControlCandidate(
value: LocalExecutionControlCandidate,
): Readonly<LocalExecutionControlCandidate> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Local execution control candidate is invalid');
}
const record = value as unknown as Record<string, unknown>;
const kind = controlKind(record.kind);
const expectedKeys =
kind === 'cancellation'
? CONTROL_CANDIDATE_KEYS
: CONTROL_CANDIDATE_KEYS.filter((key) => key !== 'cancelReason');
if (!exactKeys(record, expectedKeys)) {
throw new TypeError('Local execution control candidate shape is invalid');
}
const cancelReason = record.cancelReason;
if (
kind === 'cancellation' &&
!CANCELLATION_REASONS.has(cancelReason as RunCancellationReason)
) {
throw new TypeError('Local execution cancellation reason is invalid');
}
return Object.freeze({
kind,
runId: boundedIdentity(record.runId, 'runId'),
attemptId: boundedIdentity(record.attemptId, 'attemptId'),
dueAtMs: safeTimestamp(record.dueAtMs, 'dueAtMs'),
...(kind === 'cancellation'
? { cancelReason: cancelReason as RunCancellationReason }
: {}),
});
}
export function normalizeLocalExecutionControlCursor(
value: LocalExecutionControlCursor,
): Readonly<LocalExecutionControlCursor> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exactKeys(value as unknown as Record<string, unknown>, [
'attemptId',
'dueAtMs',
'kind',
])
) {
throw new TypeError('Local execution control cursor is invalid');
}
return Object.freeze({
dueAtMs: safeTimestamp(value.dueAtMs, 'dueAtMs'),
kind: controlKind(value.kind),
attemptId: boundedIdentity(value.attemptId, 'attemptId'),
});
}
export function normalizeLocalActiveExecutionCandidate(
value: LocalActiveExecutionCandidate,
): Readonly<LocalActiveExecutionCandidate> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exactKeys(value as unknown as Record<string, unknown>, ACTIVE_CANDIDATE_KEYS)
) {
throw new TypeError('Local active execution candidate is invalid');
}
return Object.freeze({
runId: boundedIdentity(value.runId, 'runId'),
attemptId: boundedIdentity(value.attemptId, 'attemptId'),
attemptCreatedAtMs: safeTimestamp(
value.attemptCreatedAtMs,
'attemptCreatedAtMs',
),
});
}
export function normalizeLocalActiveExecutionCursor(
value: LocalActiveExecutionCursor,
): Readonly<LocalActiveExecutionCursor> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
!exactKeys(value as unknown as Record<string, unknown>, [
'attemptCreatedAtMs',
'attemptId',
])
) {
throw new TypeError('Local active execution cursor is invalid');
}
return Object.freeze({
attemptCreatedAtMs: safeTimestamp(
value.attemptCreatedAtMs,
'attemptCreatedAtMs',
),
attemptId: boundedIdentity(value.attemptId, 'attemptId'),
});
}
@@ -0,0 +1,21 @@
/** Bounds one startup recovery observation independently of the local Profile. */
export const MAX_LOCAL_RUN_STARTUP_RECOVERY_CANDIDATES = 256;
export type LocalRunStartupRecoveryStatus = 'dispatching' | 'running';
export interface LocalRunStartupRecoveryCandidate {
readonly runId: string;
readonly runStatus: LocalRunStartupRecoveryStatus;
readonly activeAttemptCount: number;
}
export interface LocalRunStartupRecoveryPage {
readonly candidates: readonly LocalRunStartupRecoveryCandidate[];
readonly truncated: boolean;
}
export interface LocalRunStartupRecoverySource {
inspectCandidates(options?: {
readonly limit?: number;
}): Promise<LocalRunStartupRecoveryPage>;
}