217 lines
6.4 KiB
TypeScript
217 lines
6.4 KiB
TypeScript
import type { PersistedTeamLaunchPhase } from '@shared/types';
|
|
|
|
export type ProcessBootstrapTransportEvent = Record<string, unknown>;
|
|
|
|
export type ProcessBootstrapTransportTerminalKind =
|
|
| 'non_retryable_submit_rejection'
|
|
| 'accepted_without_message_id'
|
|
| 'process_exited_before_confirmation'
|
|
| 'runtime_failed_before_confirmation';
|
|
|
|
export interface ProcessBootstrapTransportSummary {
|
|
lastStage?: string;
|
|
lastObservedAt?: string;
|
|
submitted: boolean;
|
|
hasProgress: boolean;
|
|
terminalFailure?: {
|
|
kind: ProcessBootstrapTransportTerminalKind;
|
|
reason: string;
|
|
observedAt?: string;
|
|
};
|
|
}
|
|
|
|
export type ProcessBootstrapTransportProjectionPhase = 'active' | 'final';
|
|
|
|
// These helpers intentionally summarize process transport only. They explain
|
|
// where bootstrap got stuck, but never prove teammate readiness by themselves.
|
|
const MAX_TRANSPORT_DETAIL_CHARS = 500;
|
|
const WINDOWS_RESERVED_BASENAMES = new Set([
|
|
'con',
|
|
'prn',
|
|
'aux',
|
|
'nul',
|
|
'com1',
|
|
'com2',
|
|
'com3',
|
|
'com4',
|
|
'com5',
|
|
'com6',
|
|
'com7',
|
|
'com8',
|
|
'com9',
|
|
'lpt1',
|
|
'lpt2',
|
|
'lpt3',
|
|
'lpt4',
|
|
'lpt5',
|
|
'lpt6',
|
|
'lpt7',
|
|
'lpt8',
|
|
'lpt9',
|
|
]);
|
|
|
|
const TRANSPORT_STAGE_LABELS: Record<string, string> = {
|
|
process_spawned: 'process spawned',
|
|
stdout_attached: 'stdout attached',
|
|
cli_started: 'CLI started',
|
|
runtime_ready: 'runtime ready',
|
|
inbox_poller_ready: 'inbox poller ready',
|
|
mailbox_bootstrap_written: 'bootstrap mailbox row written',
|
|
bootstrap_prompt_observed: 'bootstrap prompt observed',
|
|
bootstrap_submit_attempted: 'bootstrap submit attempted',
|
|
bootstrap_submit_deferred: 'bootstrap submit deferred',
|
|
bootstrap_submit_rejected: 'bootstrap submit rejected',
|
|
bootstrap_submit_accepted_without_uuid: 'bootstrap submit accepted without message id',
|
|
bootstrap_submitted: 'bootstrap submitted',
|
|
failed: 'runtime failed',
|
|
exited: 'runtime exited',
|
|
};
|
|
|
|
export function sanitizeProcessRuntimeEventFilePrefix(value: string): string {
|
|
const normalized = String(value)
|
|
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
.toLowerCase();
|
|
const normalizedStem =
|
|
normalized
|
|
.trim()
|
|
.replace(/[. ]+$/g, '')
|
|
.split('.')[0] ?? normalized;
|
|
return normalizedStem && WINDOWS_RESERVED_BASENAMES.has(normalizedStem)
|
|
? `_${normalized}`
|
|
: normalized;
|
|
}
|
|
|
|
export function deriveProcessTransportProjectionPhase(input: {
|
|
launchPhase: PersistedTeamLaunchPhase;
|
|
finalTimeoutReached?: boolean;
|
|
}): ProcessBootstrapTransportProjectionPhase {
|
|
if (input.launchPhase !== 'active') {
|
|
return 'final';
|
|
}
|
|
return input.finalTimeoutReached === true ? 'final' : 'active';
|
|
}
|
|
|
|
export function sanitizeProcessBootstrapTransportDetail(value: unknown): string | undefined {
|
|
if (typeof value !== 'string') {
|
|
return undefined;
|
|
}
|
|
const sanitized = value
|
|
.replace(/\b(sk-[A-Za-z0-9_-]{12,}|[A-Za-z0-9_-]{32,})\b/g, '[redacted]')
|
|
.replace(/\/[^\s"'`]+/g, '[path]')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.slice(0, MAX_TRANSPORT_DETAIL_CHARS);
|
|
return sanitized.length > 0 ? sanitized : undefined;
|
|
}
|
|
|
|
function eventType(event: ProcessBootstrapTransportEvent): string {
|
|
return typeof event.type === 'string' ? event.type : '';
|
|
}
|
|
|
|
function eventTimestamp(event: ProcessBootstrapTransportEvent): string | undefined {
|
|
return typeof event.timestamp === 'string' && Number.isFinite(Date.parse(event.timestamp))
|
|
? event.timestamp
|
|
: undefined;
|
|
}
|
|
|
|
function stageLabel(event: ProcessBootstrapTransportEvent): string | undefined {
|
|
const type = eventType(event);
|
|
const label = TRANSPORT_STAGE_LABELS[type];
|
|
if (!label) {
|
|
return undefined;
|
|
}
|
|
const detail = sanitizeProcessBootstrapTransportDetail(event.detail);
|
|
if (type === 'process_spawned' || type === 'stdout_attached' || type === 'cli_started') {
|
|
return label;
|
|
}
|
|
return detail ? `${label}: ${detail}` : label;
|
|
}
|
|
|
|
function terminalFailureForEvent(
|
|
event: ProcessBootstrapTransportEvent
|
|
): ProcessBootstrapTransportSummary['terminalFailure'] | undefined {
|
|
const type = eventType(event);
|
|
const label = stageLabel(event);
|
|
const observedAt = eventTimestamp(event);
|
|
if (type === 'failed') {
|
|
return {
|
|
kind: 'runtime_failed_before_confirmation',
|
|
reason: label ?? 'runtime failed before bootstrap confirmation',
|
|
observedAt,
|
|
};
|
|
}
|
|
if (type === 'exited') {
|
|
return {
|
|
kind: 'process_exited_before_confirmation',
|
|
reason: label ?? 'runtime exited before bootstrap confirmation',
|
|
observedAt,
|
|
};
|
|
}
|
|
if (type === 'bootstrap_submit_accepted_without_uuid') {
|
|
return {
|
|
kind: 'accepted_without_message_id',
|
|
reason: label ?? 'bootstrap submit accepted without message id',
|
|
observedAt,
|
|
};
|
|
}
|
|
if (type === 'bootstrap_submit_rejected' && event.retryable === false) {
|
|
return {
|
|
kind: 'non_retryable_submit_rejection',
|
|
reason: label ?? 'bootstrap submit rejected',
|
|
observedAt,
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function summarizeProcessBootstrapTransportEvents(
|
|
events: readonly ProcessBootstrapTransportEvent[]
|
|
): ProcessBootstrapTransportSummary | null {
|
|
if (events.length === 0) {
|
|
return null;
|
|
}
|
|
let lastStage: string | undefined;
|
|
let lastObservedAt: string | undefined;
|
|
let submitted = false;
|
|
let terminalFailure: ProcessBootstrapTransportSummary['terminalFailure'];
|
|
|
|
for (const event of events) {
|
|
const label = stageLabel(event);
|
|
if (!label) {
|
|
continue;
|
|
}
|
|
lastStage = label;
|
|
lastObservedAt = eventTimestamp(event) ?? lastObservedAt;
|
|
if (eventType(event) === 'bootstrap_submitted') {
|
|
submitted = true;
|
|
}
|
|
terminalFailure = terminalFailureForEvent(event) ?? terminalFailure;
|
|
}
|
|
|
|
if (!lastStage && !terminalFailure) {
|
|
return null;
|
|
}
|
|
return {
|
|
...(lastStage ? { lastStage } : {}),
|
|
...(lastObservedAt ? { lastObservedAt } : {}),
|
|
submitted,
|
|
hasProgress: Boolean(lastStage),
|
|
...(terminalFailure ? { terminalFailure } : {}),
|
|
};
|
|
}
|
|
|
|
export function buildProcessBootstrapPendingDiagnostic(
|
|
summary: ProcessBootstrapTransportSummary
|
|
): string {
|
|
return summary.lastStage
|
|
? `Bootstrap transport reached ${summary.lastStage}; waiting for bootstrap confirmation.`
|
|
: 'Bootstrap transport is waiting for bootstrap confirmation.';
|
|
}
|
|
|
|
export function buildProcessBootstrapTimeoutDiagnostic(
|
|
summary: ProcessBootstrapTransportSummary
|
|
): string {
|
|
return summary.lastStage
|
|
? `Teammate was registered but did not bootstrap-confirm before timeout. Last transport stage: ${summary.lastStage}`
|
|
: 'Teammate was registered but did not bootstrap-confirm before timeout.';
|
|
}
|