File size: 4,272 Bytes
76289e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | // Diagnostic session context helpers capture session metadata for support bundles.
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import {
loadSessionEntryReadOnly,
readLatestTranscriptAssistantText,
} from "../config/sessions/session-accessor.js";
import { loadCronJobsStoreSync, resolveCronJobsStorePath } from "../cron/store.js";
import {
isIncognitoSessionKey,
isValidAgentId,
parseAgentSessionKey,
type ParsedAgentSessionKey,
} from "../routing/session-key.js";
const MAX_QUOTED_FIELD_CHARS = 140;
type SessionDiagnosticContext = {
agentId?: string;
cronJobId?: string;
cronRunId?: string;
cronJobName?: string;
lastAssistant?: string;
};
function quoteLogField(value: string): string {
const oneLine = value.replace(/\s+/g, " ").trim();
const truncated =
oneLine.length > MAX_QUOTED_FIELD_CHARS
? `${truncateUtf16Safe(oneLine, Math.max(0, MAX_QUOTED_FIELD_CHARS - 3))}...`
: oneLine;
return `"${truncated.replace(/["\\]/g, "\\$&")}"`;
}
function parseCronRunSessionKey(parsed: ParsedAgentSessionKey): {
cronJobId?: string;
cronRunId?: string;
} {
const parts = parsed.rest.split(":");
if (parts[0] !== "cron" || !parts[1]) {
return {};
}
const runIndex = parts.indexOf("run", 2);
return {
cronJobId: parts[1],
cronRunId: runIndex >= 0 ? parts[runIndex + 1] : undefined,
};
}
function readCronJobName(cronJobId: string | undefined): string | undefined {
if (!cronJobId) {
return undefined;
}
try {
const store = loadCronJobsStoreSync(resolveCronJobsStorePath());
const job = store.jobs.find((entry) => entry.id === cronJobId);
return typeof job?.name === "string" && job.name.trim() ? job.name.trim() : undefined;
} catch {
return undefined;
}
}
export function resolveCronSessionDiagnosticContext(params: {
sessionKey?: string;
activeSessionId?: string;
}): SessionDiagnosticContext {
const sessionKey = params.sessionKey?.trim();
// Incognito transcripts live only in memory; copying their replies into
// durable operator logs would bypass both persistence and sharing boundaries.
if (isIncognitoSessionKey(sessionKey)) {
return {};
}
const parsedAgent = parseAgentSessionKey(sessionKey);
if (!sessionKey || !parsedAgent || !isValidAgentId(parsedAgent.agentId)) {
return {};
}
const cron = parseCronRunSessionKey(parsedAgent);
const context: SessionDiagnosticContext = {
agentId: parsedAgent.agentId,
...cron,
...(cron.cronJobId ? { cronJobName: readCronJobName(cron.cronJobId) } : {}),
};
const activeSessionId = params.activeSessionId?.trim();
if (!activeSessionId) {
return context;
}
try {
const sessionScope = { agentId: parsedAgent.agentId, sessionKey };
// The read-only owner proves both identity and existence before transcript
// access, preventing rotated-session leaks and phantom agent databases.
if (loadSessionEntryReadOnly(sessionScope)?.sessionId === activeSessionId) {
context.lastAssistant = readLatestTranscriptAssistantText({
...sessionScope,
sessionId: activeSessionId,
})?.text;
}
} catch {
// Session context is best-effort and must not interrupt diagnostics.
}
return context;
}
export function formatCronSessionDiagnosticFields(context: SessionDiagnosticContext): string {
const fields: string[] = [];
if (context.cronJobId) {
fields.push(`cronJobId=${context.cronJobId}`);
}
if (context.cronRunId) {
fields.push(`cronRunId=${context.cronRunId}`);
}
if (context.cronJobName) {
fields.push(`cronJob=${quoteLogField(context.cronJobName)}`);
}
if (context.lastAssistant) {
fields.push(`lastAssistant=${quoteLogField(context.lastAssistant)}`);
}
return fields.join(" ");
}
export function formatStoppedCronSessionDiagnosticFields(
context: SessionDiagnosticContext,
): string {
const fields: string[] = [];
if (context.cronJobName) {
fields.push(`stopped=${quoteLogField(context.cronJobName)}`);
}
const rest = formatCronSessionDiagnosticFields({
cronJobId: context.cronJobId,
cronRunId: context.cronRunId,
lastAssistant: context.lastAssistant,
});
if (rest) {
fields.push(rest);
}
return fields.join(" ");
}
|