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(" ");
}