src/proto-trace.js
Rama master · réplica de lectura del repositorio original.
/**
* Optional protobuf field-tree tracing for the local Windsurf language server.
*
* Disabled by default. Enable with WINDSURFAPI_PROTO_TRACE=1. String payloads
* are redacted by default: traces keep byte length + hash, not raw API keys,
* account emails, session tokens, prompts, or tool preambles.
*/
import { appendFileSync, mkdirSync } from 'fs';
import { join } from 'path';
import { createHash } from 'crypto';
import { gunzipSync } from 'zlib';
import { getAllFields, getField, parseFields } from './proto.js';
let _seq = 0;
function enabled() {
return process.env.WINDSURFAPI_PROTO_TRACE === '1';
}
function traceDir() {
return process.env.WINDSURFAPI_PROTO_TRACE_DIR || '/data/proto-trace';
}
function positiveIntEnv(name, fallback) {
const n = parseInt(process.env[name] || '', 10);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
function shortHash(buf) {
return createHash('sha256').update(buf).digest('hex').slice(0, 16);
}
function safeMethodName(path) {
const name = String(path || 'unknown').split('/').filter(Boolean).pop() || 'unknown';
return name.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 80);
}
function mostlyText(buf) {
if (!buf || buf.length === 0) return false;
const s = buf.toString('utf8');
let printable = 0;
for (let i = 0; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c === 9 || c === 10 || c === 13 || (c >= 32 && c !== 127)) printable++;
}
return printable / Math.max(1, s.length) > 0.9;
}
function redactPreview(s) {
return String(s)
.replace(/\b(?:devin-session-token|sessionToken|api[_-]?key|firebase_id_token|idToken|refreshToken)\b\s*[:=]\s*["']?[^"',\s)]+/gi, '<redacted-secret>')
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '<redacted-email>')
.replace(/\b[A-Za-z0-9_-]{32,}\b/g, '<redacted-token>')
.slice(0, 240);
}
function looksPathLike(s) {
const value = String(s || '').trim();
if (!value || value.length > 1024 || /[\r\n<>]/.test(value)) return false;
if (/^file:\/\/\/?(?:[A-Za-z]:[\\/]|\/|~[\\/])/.test(value)) return true;
if (/^(?:[A-Za-z]:[\\/]|\/|~[\\/]|\.{1,2}[\\/])\S+/.test(value)) return true;
return /^[A-Za-z0-9._-]+(?:[\\/][A-Za-z0-9._-]+)*\.[A-Za-z0-9]{1,12}$/.test(value);
}
function looksPromptLike(s) {
const value = String(s || '');
if (!value) return false;
if (value.includes('Working directory:') || value.includes('Use the Read tool')) return true;
if (value.includes('<env>') || value.includes('</env>') || value.includes('<system-reminder>')) return true;
return value.length > 512 && /(?:tool|prompt|environment|platform|workspace)/i.test(value);
}
function basenameOfPath(s) {
const value = String(s || '').trim().replace(/^file:\/+/, '');
const parts = value.split(/[\\/]+/).filter(Boolean);
return parts.length ? parts[parts.length - 1].slice(0, 120) : '';
}
function summarizeReadWrapperStringField(field, value, bytes, sha256) {
const text = String(value || '');
const looksPath = looksPathLike(text);
const looksPrompt = looksPromptLike(text);
const acceptedByParser = (field === 1 || field === 2) && looksPath;
return {
field,
bytes,
sha256,
looksPathLike: looksPath,
looksPromptLike: looksPrompt,
acceptedByParser,
basename: looksPath ? basenameOfPath(text) : '',
};
}
function looksLikeMessage(buf) {
if (!buf?.length) return false;
const key = buf[0];
const wireType = key & 7;
const field = key >> 3;
if (!field || ![0, 1, 2, 5].includes(wireType)) return false;
try {
const parsed = parseFields(buf);
return parsed.length > 0 && parsed.every(f => f.field > 0);
} catch {
return false;
}
}
function stringField(fields, num) {
const f = getField(fields, num, 2);
return f ? f.value.toString('utf8') : '';
}
function numberField(fields, num) {
const f = getField(fields, num, 0);
return f ? Number(f.value || 0) : null;
}
function boolField(fields, num) {
const n = numberField(fields, num);
return n == null ? null : n !== 0;
}
function countRepeatedMessageFields(fields, num) {
return getAllFields(fields, num).filter(f => f.wireType === 2).length;
}
function summarizeMessageChildren(buf, maxFields = 12) {
let children = [];
try {
children = parseFields(buf).slice(0, maxFields).map(child => ({
field: child.field,
wireType: child.wireType,
type: Buffer.isBuffer(child.value)
? (mostlyText(child.value) ? 'string' : 'message_or_bytes')
: 'varint',
bytes: Buffer.isBuffer(child.value) ? child.value.length : undefined,
value: Buffer.isBuffer(child.value) ? undefined : Number(child.value),
}));
} catch {
children = [];
}
return {
bytes: buf.length,
fieldNumbers: children.map(child => child.field),
fields: children,
};
}
function summarizeKnowledgeBaseChunk(buf) {
const fields = parseFields(buf);
const markdown = getField(fields, 3, 2);
let markdownTextBytes = 0;
if (markdown) {
try {
markdownTextBytes = stringField(parseFields(markdown.value), 2).length;
} catch {}
}
return {
bytes: buf.length,
textBytes: stringField(fields, 1).length,
markdownTextBytes,
fieldNumbers: fields.map(f => f.field),
};
}
function summarizeKnowledgeBaseItem(buf) {
const fields = parseFields(buf);
const chunks = getAllFields(fields, 6)
.filter(f => f.wireType === 2)
.map(f => summarizeKnowledgeBaseChunk(f.value));
return {
bytes: buf.length,
textBytes: stringField(fields, 2).length,
urlBytes: stringField(fields, 3).length,
titleBytes: stringField(fields, 4).length,
summaryBytes: stringField(fields, 7).length,
chunkCount: chunks.length,
chunkTextBytes: chunks.reduce((sum, chunk) => sum + chunk.textBytes + chunk.markdownTextBytes, 0),
fieldNumbers: fields.map(f => f.field),
chunks: chunks.slice(0, 4),
};
}
const NATIVE_TOOL_CONFIG_FIELDS = new Map([
[5, 'find'],
[8, 'run_command'],
[13, 'search_web'],
[10, 'view_file'],
[19, 'list_dir'],
[33, 'grep_v2'],
[37, 'read_url_content'],
]);
const THIRD_PARTY_WEB_SEARCH_PROVIDER = new Map([
[0, 'UNSPECIFIED'],
[1, 'OPENAI'],
]);
const THIRD_PARTY_WEB_SEARCH_MODEL = new Map([
[0, 'UNSPECIFIED'],
[1, 'O3'],
[2, 'GPT_4_1'],
[3, 'O4_MINI'],
]);
const CASCADE_WEB_REQUESTS_AUTO_EXECUTION = new Map([
[0, 'UNSPECIFIED'],
[1, 'DISABLED'],
[2, 'ALLOWLIST'],
[3, 'TURBO'],
]);
const READ_URL_CONTENT_ACTION = new Map([
[1, 'ALLOW_ONCE'],
[2, 'REJECT'],
[3, 'ALWAYS_ALLOW_ORIGIN'],
]);
function enumSummary(value, names) {
return value == null ? null : {
value,
name: names.get(value) || `UNKNOWN_${value}`,
};
}
function summarizeThirdPartyWebSearchConfig(buf) {
const fields = parseFields(buf);
return {
provider: enumSummary(numberField(fields, 1), THIRD_PARTY_WEB_SEARCH_PROVIDER),
model: enumSummary(numberField(fields, 2), THIRD_PARTY_WEB_SEARCH_MODEL),
fieldNumbers: fields.map(f => f.field),
};
}
function summarizeAutoWebRequestConfig(buf) {
const fields = parseFields(buf);
const allowlist = getAllFields(fields, 1)
.filter(f => f.wireType === 2)
.map(f => ({ bytes: f.value.length, sha256: shortHash(f.value) }));
return {
allowlistCount: allowlist.length,
allowlist,
autoExecutionPolicy: enumSummary(numberField(fields, 2), CASCADE_WEB_REQUESTS_AUTO_EXECUTION),
fieldNumbers: fields.map(f => f.field),
};
}
function summarizeNativeToolSubconfig(fields, num) {
const f = getField(fields, num, 2);
if (!f) return null;
const summary = summarizeMessageChildren(f.value);
const kind = NATIVE_TOOL_CONFIG_FIELDS.get(num) || `field_${num}`;
let decoded = null;
try {
const subFields = parseFields(f.value);
if (kind === 'search_web') {
const thirdParty = getField(subFields, 2, 2);
decoded = {
forceDisable: boolField(subFields, 1),
thirdPartyConfig: thirdParty ? summarizeThirdPartyWebSearchConfig(thirdParty.value) : null,
};
} else if (kind === 'read_url_content') {
const autoWeb = getField(subFields, 2, 2);
decoded = {
forceDisable: boolField(subFields, 1),
autoWebRequestConfig: autoWeb ? summarizeAutoWebRequestConfig(autoWeb.value) : null,
};
}
} catch {}
return {
field: num,
kind,
bytes: summary.bytes,
fieldNumbers: summary.fieldNumbers,
fields: summary.fields.map(child => ({
field: child.field,
wireType: child.wireType,
bytes: child.bytes,
})),
...(decoded ? { decoded } : {}),
};
}
function summarizeNativeToolConfig(toolCfgBuf) {
const fields = parseFields(toolCfgBuf);
const subconfigs = [...NATIVE_TOOL_CONFIG_FIELDS.keys()]
.map(num => summarizeNativeToolSubconfig(fields, num))
.filter(Boolean);
const known = new Set([...NATIVE_TOOL_CONFIG_FIELDS.keys(), 32]);
const unknownFields = fields
.filter(f => f.wireType === 2 && !known.has(f.field))
.slice(0, positiveIntEnv('WINDSURFAPI_PROTO_TRACE_TOOL_CONFIG_UNKNOWN_LIMIT', 24))
.map(f => ({
field: f.field,
bytes: f.value.length,
summary: summarizeMessageChildren(f.value, 10),
}));
return {
subconfigFields: subconfigs.map(s => s.field),
subconfigs,
unknownFields,
allowlist: getAllFields(fields, 32)
.filter(f => f.wireType === 2)
.map(f => f.value.toString('utf8')),
};
}
function summarizeSendUserCascadeMessage(payload) {
const top = parseFields(payload);
const out = {
cascadeIdHash: null,
hasText: !!getField(top, 2, 2),
hasMetadata: !!getField(top, 3, 2),
imageCount: countRepeatedMessageFields(top, 6),
additionalStepCount: countRepeatedMessageFields(top, 9),
plannerMode: null,
hasNativeToolConfig: false,
nativeToolConfig: null,
};
const cascadeId = stringField(top, 1);
if (cascadeId) out.cascadeIdHash = shortHash(Buffer.from(cascadeId, 'utf8'));
const cfgField = getField(top, 5, 2);
if (!cfgField) return out;
const cfg = parseFields(cfgField.value);
const plannerField = getField(cfg, 1, 2);
if (!plannerField) return out;
const planner = parseFields(plannerField.value);
const convField = getField(planner, 2, 2);
if (convField) {
const conv = parseFields(convField.value);
out.plannerMode = numberField(conv, 4);
out.hasToolCallingSection = !!getField(conv, 10, 2);
out.hasAdditionalInstructionsSection = !!getField(conv, 12, 2);
}
const toolCfgField = getField(planner, 13, 2);
if (toolCfgField) {
out.hasNativeToolConfig = true;
out.nativeToolConfig = summarizeNativeToolConfig(toolCfgField.value);
}
return out;
}
const NATIVE_STEP_FIELDS = new Map([
[13, 'grep_search'],
[14, 'view_file'],
[15, 'list_directory'],
[23, 'write_to_file'],
[28, 'run_command'],
[34, 'find'],
[40, 'read_url_content'],
[42, 'search_web'],
[105, 'grep_search_v2'],
]);
const REQUESTED_INTERACTION_FIELDS = new Map([
[2, 'deploy'],
[3, 'run_command'],
[5, 'run_extension_code'],
[11, 'resolve_task'],
[13, 'upsert_codemap'],
[14, 'read_url_content'],
[15, 'ask_user_question'],
]);
function summarizeNativeStepBody(kind, bodyBuf) {
const f = parseFields(bodyBuf);
if (kind === 'view_file') {
return {
absolutePathUriBytes: stringField(f, 1).length,
contentBytes: stringField(f, 4).length,
offset: numberField(f, 11) || 0,
limit: numberField(f, 12) || 0,
};
}
if (kind === 'run_command') {
const combined = getField(f, 21, 2);
let combinedOutputBytes = 0;
if (combined) {
try {
combinedOutputBytes = stringField(parseFields(combined.value), 1).length;
} catch {}
}
return {
commandBytes: (stringField(f, 23) || stringField(f, 1)).length,
cwdBytes: stringField(f, 2).length,
combinedOutputBytes,
stdoutBytes: stringField(f, 4).length,
stderrBytes: stringField(f, 5).length,
};
}
if (kind === 'grep_search_v2') {
return {
patternBytes: stringField(f, 2).length,
pathBytes: stringField(f, 3).length,
globBytes: stringField(f, 4).length,
outputModeBytes: stringField(f, 5).length,
headLimit: numberField(f, 12) || 0,
rawOutputBytes: stringField(f, 15).length,
};
}
if (kind === 'grep_search') {
return {
queryBytes: stringField(f, 1).length,
searchPathUriBytes: stringField(f, 11).length,
resultBytes: stringField(f, 3).length,
};
}
if (kind === 'find') {
return {
patternBytes: stringField(f, 1).length,
searchDirectoryBytes: stringField(f, 10).length,
rawOutputBytes: stringField(f, 11).length,
};
}
if (kind === 'list_directory') {
return {
directoryPathUriBytes: stringField(f, 1).length,
childCount: getAllFields(f, 2).filter(x => x.wireType === 2).length,
};
}
if (kind === 'search_web') {
return {
queryBytes: stringField(f, 1).length,
webDocumentCount: getAllFields(f, 2).filter(x => x.wireType === 2).length,
domainBytes: stringField(f, 3).length,
summaryBytes: stringField(f, 5).length,
fieldNumbers: f.map(x => x.field),
messageFields: f
.filter(x => x.wireType === 2 && ![1, 3, 5].includes(x.field))
.slice(0, 8)
.map(x => ({ field: x.field, ...summarizeMessageChildren(x.value, 8) })),
};
}
if (kind === 'read_url_content') {
const webDocument = getField(f, 2, 2);
return {
urlBytes: stringField(f, 1).length,
webDocument: webDocument ? summarizeKnowledgeBaseItem(webDocument.value) : null,
resolvedUrlBytes: stringField(f, 3).length,
latencyMs: numberField(f, 4) || 0,
legacySummaryBytes: stringField(f, 5).length,
userRejected: boolField(f, 6),
autoRunDecision: numberField(f, 7),
fieldNumbers: f.map(x => x.field),
messageFields: f
.filter(x => x.wireType === 2 && ![1, 2, 3, 4, 5, 6, 7].includes(x.field))
.slice(0, 8)
.map(x => ({ field: x.field, ...summarizeMessageChildren(x.value, 8) })),
};
}
return { fieldCount: f.length };
}
function summarizeRequestedInteractionBody(kind, bodyBuf) {
const fields = parseFields(bodyBuf);
if (kind === 'read_url_content') {
const url = stringField(fields, 1);
const origin = stringField(fields, 2);
return {
urlBytes: url.length,
urlHash: url ? shortHash(Buffer.from(url, 'utf8')) : null,
originBytes: origin.length,
originHash: origin ? shortHash(Buffer.from(origin, 'utf8')) : null,
fieldNumbers: fields.map(f => f.field),
};
}
return summarizeMessageChildren(bodyBuf, 8);
}
function summarizeRequestedInteraction(buf) {
const fields = parseFields(buf);
const interactions = [];
for (const [fieldNum, kind] of REQUESTED_INTERACTION_FIELDS) {
const f = getField(fields, fieldNum, 2);
if (!f) continue;
interactions.push({
field: fieldNum,
kind,
bytes: f.value.length,
body: summarizeRequestedInteractionBody(kind, f.value),
});
}
return {
bytes: buf.length,
fieldNumbers: fields.map(f => f.field),
interactions,
};
}
function summarizeHandleCascadeUserInteraction(payload) {
const fields = parseFields(payload);
const cascadeId = stringField(fields, 1);
const interactionField = getField(fields, 2, 2);
const out = {
cascadeIdBytes: cascadeId.length,
cascadeIdHash: cascadeId ? shortHash(Buffer.from(cascadeId, 'utf8')) : null,
fieldNumbers: fields.map(f => f.field),
};
if (!interactionField) return out;
const interaction = parseFields(interactionField.value);
const trajectoryId = stringField(interaction, 1);
const readUrlField = getField(interaction, 15, 2);
out.interaction = {
trajectoryIdBytes: trajectoryId.length,
trajectoryIdHash: trajectoryId ? shortHash(Buffer.from(trajectoryId, 'utf8')) : null,
stepIndex: numberField(interaction, 2),
fieldNumbers: interaction.map(f => f.field),
};
if (!readUrlField) return out;
const readUrl = parseFields(readUrlField.value);
const url = stringField(readUrl, 2);
const origin = stringField(readUrl, 3);
out.interaction.readUrlContent = {
action: enumSummary(numberField(readUrl, 1), READ_URL_CONTENT_ACTION),
urlBytes: url.length,
urlHash: url ? shortHash(Buffer.from(url, 'utf8')) : null,
originBytes: origin.length,
originHash: origin ? shortHash(Buffer.from(origin, 'utf8')) : null,
fieldNumbers: readUrl.map(f => f.field),
};
return out;
}
function summarizeReadWrapperField19(wrapperBuf) {
const fields = parseFields(wrapperBuf);
const candidates = [];
let acceptedField = null;
return {
bytes: wrapperBuf.length,
fieldNumbers: fields.map(f => f.field),
children: fields.slice(0, positiveIntEnv('WINDSURFAPI_PROTO_TRACE_READ_WRAPPER_CHILD_LIMIT', 24))
.map((f) => {
const out = {
field: f.field,
wireType: f.wireType,
};
if (f.wireType === 0) {
out.type = 'varint';
out.value = typeof f.value === 'bigint' ? f.value.toString() : f.value;
return out;
}
if (!Buffer.isBuffer(f.value)) return out;
out.bytes = f.value.length;
out.sha256 = shortHash(f.value);
if (looksLikeMessage(f.value)) {
out.type = 'message_or_bytes';
out.summary = summarizeMessageChildren(f.value, 8);
return out;
}
if (mostlyText(f.value)) {
const text = f.value.toString('utf8');
const candidate = summarizeReadWrapperStringField(f.field, text, f.value.length, out.sha256);
candidates.push(candidate);
if (acceptedField == null && candidate.acceptedByParser) acceptedField = f.field;
out.type = 'string';
out.hasNewline = /[\r\n]/.test(text);
out.hasAngleBracket = /[<>]/.test(text);
out.looksPathLike = candidate.looksPathLike;
out.looksPromptLike = candidate.looksPromptLike;
out.basename = candidate.basename;
out.acceptedByParser = candidate.acceptedByParser;
if (process.env.WINDSURFAPI_PROTO_TRACE_READ_WRAPPER_STRINGS === '1') {
out.preview = redactPreview(text);
}
return out;
}
out.type = 'message_or_bytes';
out.summary = summarizeMessageChildren(f.value, 8);
return out;
}),
candidateSummary: {
acceptedField,
pathLikeFields: candidates.filter(c => c.looksPathLike).map(c => c.field),
rejectedPromptFields: candidates.filter(c => c.looksPromptLike && !c.acceptedByParser).map(c => c.field),
ambiguous: candidates.filter(c => c.acceptedByParser).length > 1,
candidates,
},
};
}
function classifyErrorText(text) {
const value = String(text || '').toLowerCase();
return {
permissionDenied: /permission[_\s-]?denied|forbidden|\b403\b|not authorized/.test(value),
failedPrecondition: /failed[_\s-]?precondition|precondition/.test(value),
modelNotAvailable: /model[_\s-]?not[_\s-]?available|model not available/.test(value),
internalError: /internal[_\s-]?error|an internal error occurred/.test(value),
unauthenticated: /unauthenticated|\b401\b|invalid api key|invalid token/.test(value),
rateLimited: /rate[_\s-]?limit|too many requests|\b429\b/.test(value),
quotaOrEntitlement: /quota|credit|billing|subscription|entitlement/.test(value),
};
}
function compactTrueFlags(flags) {
return Object.fromEntries(Object.entries(flags || {}).filter(([, v]) => !!v));
}
function mergeErrorClassifications(target, source) {
for (const [key, value] of Object.entries(source || {})) {
if (value) target[key] = true;
}
return target;
}
function summarizeErrorString(path, buf) {
const text = buf.toString('utf8');
const classifications = compactTrueFlags(classifyErrorText(text));
const out = {
path,
bytes: buf.length,
sha256: shortHash(buf),
classifications,
};
if (process.env.WINDSURFAPI_PROTO_TRACE_ERROR_STRINGS === '1') {
out.preview = redactPreview(text);
}
return out;
}
function collectErrorStrings(buf, path, out, depth = 0) {
if (!Buffer.isBuffer(buf) || out.length >= positiveIntEnv('WINDSURFAPI_PROTO_TRACE_ERROR_STRING_LIMIT', 8)) return;
const maxDepth = positiveIntEnv('WINDSURFAPI_PROTO_TRACE_ERROR_DEPTH', 4);
if (depth < maxDepth && looksLikeMessage(buf)) {
const before = out.length;
try {
const fields = parseFields(buf);
for (const f of fields) {
if (out.length >= positiveIntEnv('WINDSURFAPI_PROTO_TRACE_ERROR_STRING_LIMIT', 8)) return;
if (f.wireType !== 2 || !Buffer.isBuffer(f.value)) continue;
collectErrorStrings(f.value, `${path}.${f.field}`, out, depth + 1);
}
if (out.length > before) return;
} catch {}
}
if (mostlyText(buf)) {
out.push(summarizeErrorString(path, buf));
}
}
function summarizeErrorSource(field, payload) {
const source = {
field,
bytes: payload.length,
sha256: shortHash(payload),
fieldNumbers: [],
varints: [],
strings: [],
classifications: {},
};
try {
const fields = parseFields(payload);
source.fieldNumbers = fields.map(f => f.field);
source.varints = fields
.filter(f => f.wireType === 0)
.slice(0, 8)
.map(f => ({
field: f.field,
value: typeof f.value === 'bigint' ? f.value.toString() : f.value,
}));
collectErrorStrings(payload, String(field), source.strings);
for (const s of source.strings) {
mergeErrorClassifications(source.classifications, s.classifications);
}
} catch (err) {
source.parseError = err.message;
}
source.classifications = compactTrueFlags(source.classifications);
return source;
}
function summarizeErrorStep(fields) {
const sources = [];
for (const fieldNum of [24, 31]) {
for (const f of getAllFields(fields, fieldNum)) {
if (f.wireType !== 2 || !Buffer.isBuffer(f.value)) continue;
sources.push(summarizeErrorSource(fieldNum, f.value));
}
}
if (!sources.length) return null;
const classifications = {};
for (const source of sources) {
mergeErrorClassifications(classifications, source.classifications);
}
return {
sources,
classifications: compactTrueFlags(classifications),
};
}
function summarizeWebFetchTrajectoryBranch({ type, status, nativeOneofs, requestedInteraction, errorStep }) {
const readUrlOneof = nativeOneofs.find(x => x.kind === 'read_url_content') || null;
const pendingReadUrl = requestedInteraction?.interactions?.find(x => x.kind === 'read_url_content') || null;
const classifications = errorStep?.classifications || {};
const hasWebDocument = !!readUrlOneof?.body?.webDocument;
const legacySummaryBytes = readUrlOneof?.body?.legacySummaryBytes || 0;
const hasLegacySummary = legacySummaryBytes > 0;
const autoRunDecision = readUrlOneof?.body?.autoRunDecision ?? null;
let state = null;
if (readUrlOneof && hasWebDocument) state = 'completed_web_document';
else if (pendingReadUrl) state = 'pending_permission';
else if (readUrlOneof && autoRunDecision != null && !hasWebDocument && !hasLegacySummary) state = 'auto_run_decision_only';
else if (readUrlOneof && hasLegacySummary && !hasWebDocument) state = 'legacy_summary_only';
else if (readUrlOneof) state = 'native_oneof_no_document';
else if (type === 17 && (classifications.permissionDenied || classifications.failedPrecondition)) state = 'error';
if (!state) return null;
return {
state,
stepType: type,
status,
hasRequestedInteraction: !!pendingReadUrl,
hasReadUrlOneof: !!readUrlOneof,
hasWebDocument,
legacySummaryBytes,
autoRunDecision,
readUrlFieldNumbers: readUrlOneof?.body?.fieldNumbers || [],
requestedFieldNumbers: pendingReadUrl?.body?.fieldNumbers || [],
errorClassifications: compactTrueFlags(classifications),
};
}
function summarizeTrajectoryStep(stepBuf, index) {
const fields = parseFields(stepBuf);
const oneofFields = [];
for (const [fieldNum, kind] of NATIVE_STEP_FIELDS) {
const oneof = getField(fields, fieldNum, 2);
if (!oneof) continue;
oneofFields.push({
field: fieldNum,
kind,
bodyBytes: oneof.value.length,
body: summarizeNativeStepBody(kind, oneof.value),
});
}
const interestingFields = fields
.filter(f => f.wireType === 2 && ![5, 56].includes(f.field))
.slice(0, positiveIntEnv('WINDSURFAPI_PROTO_TRACE_SEMANTIC_FIELD_LIMIT', 12))
.map(f => ({
field: f.field,
...summarizeMessageChildren(f.value, 8),
}));
const type = numberField(fields, 1);
const status = numberField(fields, 4);
// type 14 is CortexStepType.USER_INPUT; field 19 may carry the observed
// read-wrapper shape that the inbound parser promotes to a view_file call.
const wrapper19 = type === 14 ? getField(fields, 19, 2) : null;
const requestedInteraction = getField(fields, 56, 2);
const requestedInteractionSummary = requestedInteraction ? summarizeRequestedInteraction(requestedInteraction.value) : null;
const errorStep = summarizeErrorStep(fields);
const webFetchTrace = summarizeWebFetchTrajectoryBranch({
type,
status,
nativeOneofs: oneofFields,
requestedInteraction: requestedInteractionSummary,
errorStep,
});
return {
index,
type,
status,
fieldNumbers: fields.map(f => f.field),
nativeOneofs: oneofFields,
messageFields: interestingFields,
...(wrapper19 ? { readWrapperField19: summarizeReadWrapperField19(wrapper19.value) } : {}),
...(requestedInteractionSummary ? { requestedInteraction: requestedInteractionSummary } : {}),
...(webFetchTrace ? { webFetchTrace } : {}),
...(errorStep ? { errorStep } : {}),
};
}
function summarizeGetCascadeTrajectorySteps(payload) {
const top = parseFields(payload);
return {
stepCount: countRepeatedMessageFields(top, 1),
steps: getAllFields(top, 1)
.filter(f => f.wireType === 2)
.slice(0, positiveIntEnv('WINDSURFAPI_PROTO_TRACE_SEMANTIC_STEP_LIMIT', 40))
.map((f, index) => summarizeTrajectoryStep(f.value, index)),
};
}
function semanticSummary(method, direction, payload) {
try {
if (method === 'SendUserCascadeMessage' && direction === 'request') {
return summarizeSendUserCascadeMessage(payload);
}
if (method === 'GetCascadeTrajectorySteps' && direction === 'response') {
return summarizeGetCascadeTrajectorySteps(payload);
}
if (method === 'HandleCascadeUserInteraction' && direction === 'request') {
return summarizeHandleCascadeUserInteraction(payload);
}
} catch (err) {
return { error: err.message };
}
return null;
}
function summarizeBytes(buf, depth, maxDepth) {
const bytes = buf.length;
const hash = shortHash(buf);
if (depth < maxDepth) {
try {
const parsed = parseFields(buf);
if (parsed.length && parsed.every(f => f.field > 0)) {
const children = summarizeProtoForTrace(buf, { depth: depth + 1, maxDepth });
if (children.length) return { type: 'message', bytes, sha256: hash, fields: children };
}
} catch {}
}
if (mostlyText(buf)) {
const out = { type: 'string', bytes, sha256: hash };
if (process.env.WINDSURFAPI_PROTO_TRACE_STRINGS === '1') {
out.preview = redactPreview(buf.toString('utf8'));
}
return out;
}
if (depth >= maxDepth) return { type: 'bytes', bytes, sha256: hash, truncatedDepth: true };
try {
const children = summarizeProtoForTrace(buf, { depth: depth + 1, maxDepth });
if (children.length) return { type: 'message', bytes, sha256: hash, fields: children };
} catch {}
return { type: 'bytes', bytes, sha256: hash };
}
export function summarizeProtoForTrace(buf, opts = {}) {
const depth = opts.depth || 0;
const maxDepth = opts.maxDepth || positiveIntEnv('WINDSURFAPI_PROTO_TRACE_DEPTH', 8);
const fields = parseFields(Buffer.isBuffer(buf) ? buf : Buffer.from(buf || []));
return fields.map((f) => {
const out = { field: f.field, wireType: f.wireType };
if (f.wireType === 0) {
out.type = 'varint';
out.value = typeof f.value === 'bigint' ? f.value.toString() : f.value;
} else if (f.wireType === 1 || f.wireType === 5) {
out.type = f.wireType === 1 ? 'fixed64' : 'fixed32';
out.bytes = f.value.length;
out.hex = f.value.toString('hex');
} else if (f.wireType === 2) {
Object.assign(out, summarizeBytes(f.value, depth, maxDepth));
}
return out;
});
}
export function unwrapTracePayload(body, transport = 'grpc') {
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body || []);
if (transport === 'connect') {
if (buf.length >= 5) {
const flags = buf[0];
const len = buf.readUInt32BE(1);
if (len === buf.length - 5) {
let payload = buf.subarray(5);
if (flags & 0x01) payload = gunzipSync(payload);
return payload;
}
}
return buf;
}
if (buf.length >= 5 && buf[0] === 0) {
const len = buf.readUInt32BE(1);
if (len <= buf.length - 5) return buf.subarray(5, 5 + len);
}
return buf;
}
export function traceGrpcPayload({ port, path, direction, body, transport = 'grpc', framed = false } = {}) {
if (!enabled()) return;
try {
const payload = framed ? unwrapTracePayload(body, transport) : (Buffer.isBuffer(body) ? body : Buffer.from(body || []));
const maxBytes = positiveIntEnv('WINDSURFAPI_PROTO_TRACE_MAX_BYTES', 512 * 1024);
const record = {
ts: new Date().toISOString(),
seq: ++_seq,
pid: process.pid,
port,
path,
method: safeMethodName(path),
direction,
transport,
payloadBytes: payload.length,
payloadSha256: shortHash(payload),
};
const semantic = semanticSummary(record.method, direction, payload);
if (semantic) record.semantic = semantic;
if (payload.length > maxBytes) {
record.skipped = `payload exceeds ${maxBytes} bytes`;
} else {
record.fields = summarizeProtoForTrace(payload);
}
const dir = traceDir();
mkdirSync(dir, { recursive: true });
const file = join(dir, `ls-proto-${process.pid}-${safeMethodName(path)}.jsonl`);
appendFileSync(file, JSON.stringify(record) + '\n');
} catch (err) {
try {
const dir = traceDir();
mkdirSync(dir, { recursive: true });
appendFileSync(join(dir, `ls-proto-${process.pid}-errors.log`), `${new Date().toISOString()} ${path || ''} ${direction || ''}: ${err.message}\n`);
} catch {}
}
}
export function _resetProtoTraceForTests() {
_seq = 0;
}