File size: 7,026 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Session memory transcript helpers persist compact session transcript excerpts.
import { classifySessionMessageOrigin } from "../../../../packages/memory-host-sdk/src/host/session-provenance.js";
import type { MemoryOriginClass } from "../../../../packages/memory-host-sdk/src/host/types.js";
import { sanitizeModelSpecialTokens } from "../../../security/external-content.js";
import { hasInterSessionUserProvenance } from "../../../sessions/input-provenance.js";
import { isOpenClawDeliveryMirrorAssistantMessage } from "../../../shared/transcript-only-openclaw-assistant.js";

const SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX = String.raw`(?:(?:\|DSML\|)|(?:\uFF5CDSML\uFF5C))?`;
const SESSION_MEMORY_TOOL_DIRECTIVE_KIND = String.raw`(?:tool_calls?|function_calls?|tool_use_error)`;
const SESSION_MEMORY_DROP_BLOCK_RE = new RegExp(
  String.raw`<${SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX}${SESSION_MEMORY_TOOL_DIRECTIVE_KIND}\b[^>]*>` +
    String.raw`[\s\S]*?(?:<\/${SESSION_MEMORY_TOOL_DIRECTIVE_PREFIX}${SESSION_MEMORY_TOOL_DIRECTIVE_KIND}>|$)`,
  "gi",
);
const SESSION_MEMORY_ROLE_DIRECTIVE_BLOCK_RE = /<(system|assistant|user)\b[^>]*>[\s\S]*?<\/\1>/gi;
const SESSION_MEMORY_ROLE_DIRECTIVE_TAG_RE = /<\/?(?:system|assistant|user)\b[^>]*>/gi;
const SESSION_MEMORY_TRAILING_NO_REPLY_RE = /(?:^|\n)\s*NO_REPLY\s*$/i;
const SESSION_MEMORY_JSON_LINE_SEPARATOR_RE = /[\u0085\u2028\u2029]/gu;

function quoteSessionMemoryText(text: string): string {
  // One JSON string per role record keeps message text from forging later
  // records while preserving every character for memory readers.
  return JSON.stringify(text).replace(
    SESSION_MEMORY_JSON_LINE_SEPARATOR_RE,
    (separator) => `\\u${separator.charCodeAt(0).toString(16).padStart(4, "0")}`,
  );
}

function isNoReplyMarker(text: string): boolean {
  const trimmed = text.trim();
  return /^NO_REPLY$/i.test(trimmed) || /^\{\s*"action"\s*:\s*"NO_REPLY"\s*\}$/i.test(trimmed);
}

function sanitizeSessionMemoryTranscriptText(text: string): string | null {
  if (isNoReplyMarker(text)) {
    return null;
  }
  const withoutArtifacts = sanitizeModelSpecialTokens(text)
    .replace(SESSION_MEMORY_DROP_BLOCK_RE, "")
    .replace(SESSION_MEMORY_ROLE_DIRECTIVE_BLOCK_RE, "")
    .replace(SESSION_MEMORY_ROLE_DIRECTIVE_TAG_RE, "")
    .replace(SESSION_MEMORY_TRAILING_NO_REPLY_RE, "")
    .trim();

  return withoutArtifacts || null;
}

function extractTextMessageContent(content: unknown): string | undefined {
  if (typeof content === "string") {
    return content;
  }
  if (!Array.isArray(content)) {
    return undefined;
  }
  for (const block of content) {
    if (!block || typeof block !== "object") {
      continue;
    }
    const candidate = block as { type?: unknown; text?: unknown };
    if (candidate.type === "text" && typeof candidate.text === "string") {
      return candidate.text;
    }
  }
  return undefined;
}

type RenderedSessionMemoryMessage = {
  isDeliveryMirror: boolean;
  originClass: MemoryOriginClass;
  role: "assistant" | "user";
  text?: string;
};

type SessionMemoryMessageRenderResult = {
  message?: RenderedSessionMemoryMessage;
  turnOrigin: MemoryOriginClass;
};

function renderSessionMemoryMessage(
  entry: unknown,
  turnOrigin: MemoryOriginClass,
): SessionMemoryMessageRenderResult {
  if (!entry || typeof entry !== "object") {
    return { turnOrigin };
  }
  const record = entry as {
    message?: {
      content?: unknown;
      provenance?: unknown;
      role?: unknown;
    } & Record<string, unknown>;
    type?: unknown;
  };
  if (record.type !== "message" || !record.message) {
    return { turnOrigin };
  }
  const role = record.message.role;
  if ((role !== "user" && role !== "assistant") || !("content" in record.message)) {
    return { turnOrigin };
  }
  const nextTurnOrigin =
    role === "user" ? classifySessionMessageOrigin(record.message, turnOrigin) : turnOrigin;
  const originClass = classifySessionMessageOrigin(record.message, nextTurnOrigin);
  if (role === "user" && hasInterSessionUserProvenance(record.message)) {
    return { turnOrigin: nextTurnOrigin };
  }
  const text = extractTextMessageContent(record.message.content);
  const sanitized = text ? sanitizeSessionMemoryTranscriptText(text) : null;
  if (!sanitized) {
    return { turnOrigin: nextTurnOrigin };
  }
  if (sanitized.startsWith("/")) {
    return {
      turnOrigin: nextTurnOrigin,
      ...(role === "user" ? { message: { isDeliveryMirror: false, originClass, role } } : {}),
    };
  }
  return {
    turnOrigin: nextTurnOrigin,
    message: {
      isDeliveryMirror: isOpenClawDeliveryMirrorAssistantMessage(record.message),
      originClass,
      role,
      text: sanitized,
    },
  };
}

type SessionMemoryRecord = {
  line: string;
  originClass: MemoryOriginClass;
};

function renderSessionMemoryRecords(events: readonly unknown[]): SessionMemoryRecord[] {
  const allMessages: SessionMemoryRecord[] = [];
  let lastAssistantText: string | undefined;
  let turnOrigin: MemoryOriginClass = "untrusted";
  for (const event of events) {
    const result = renderSessionMemoryMessage(event, turnOrigin);
    turnOrigin = result.turnOrigin;
    const rendered = result.message;
    if (!rendered) {
      continue;
    }
    if (rendered.role === "user") {
      // New turn: reset even when slash commands are omitted from memory, so
      // later standalone delivery mirrors are preserved.
      lastAssistantText = undefined;
    }
    if (!rendered.text) {
      continue;
    }
    // Skip delivery-mirror rows only when they duplicate the preceding
    // assistant text. Delivery-mirror rows with unique visible content
    // (e.g., message-tool replies) are preserved.
    if (rendered.isDeliveryMirror && rendered.text === lastAssistantText) {
      continue;
    }
    allMessages.push({
      line: `${rendered.role}: ${quoteSessionMemoryText(rendered.text)}`,
      originClass: rendered.originClass,
    });
    if (rendered.role === "assistant") {
      lastAssistantText = rendered.text;
    }
  }
  return allMessages;
}

/** Counts transcript events that remain after session-memory filtering and deduplication. */
export function countSessionMemoryMessages(events: readonly unknown[]): number {
  return renderSessionMemoryRecords(events).length;
}

export type SessionMemoryProjection = {
  content: string;
  originClass: "agent" | "untrusted";
};

export function getRecentSessionProjectionFromEvents(
  events: readonly unknown[],
  messageCount = 15,
): SessionMemoryProjection | null {
  const limit = Number.isFinite(messageCount) ? Math.max(0, Math.floor(messageCount)) : 0;
  if (limit === 0) {
    return null;
  }
  const records = renderSessionMemoryRecords(events).slice(-limit);
  if (records.length === 0) {
    return null;
  }
  return {
    content: records.map((record) => record.line).join("\n"),
    originClass: records.some(
      (record) => record.originClass === "untrusted" || record.originClass === "system",
    )
      ? "untrusted"
      : "agent",
  };
}