clawmem 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/observer.ts +104 -15
package/package.json
CHANGED
package/src/observer.ts
CHANGED
|
@@ -126,29 +126,118 @@ Rules:
|
|
|
126
126
|
- If a section has nothing relevant, write "None"`;
|
|
127
127
|
|
|
128
128
|
// =============================================================================
|
|
129
|
-
// Transcript Preparation
|
|
129
|
+
// Transcript Preparation — Priority-Based Formatting
|
|
130
|
+
//
|
|
131
|
+
// Priority levels (lower = more important):
|
|
132
|
+
// P0 — First user message (original request)
|
|
133
|
+
// P1 — Last assistant message (final response)
|
|
134
|
+
// P2 — Tool calls + tool errors
|
|
135
|
+
// P3 — Other user/assistant messages
|
|
136
|
+
// P4 — System messages
|
|
130
137
|
// =============================================================================
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
const P_USER_INSTRUCTION = 0;
|
|
140
|
+
const P_FINAL_RESPONSE = 1;
|
|
141
|
+
const P_TOOL_ACTIVITY = 2;
|
|
142
|
+
const P_CONVERSATION = 3;
|
|
143
|
+
const P_SYSTEM = 4;
|
|
144
|
+
|
|
145
|
+
export type PrioritizedMessage = {
|
|
146
|
+
priority: number;
|
|
147
|
+
index: number; // original position for chronological reassembly
|
|
148
|
+
role: string;
|
|
149
|
+
content: string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function isToolContent(content: string): boolean {
|
|
153
|
+
return content.includes("[tool_use") || content.includes("[tool_result");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function classifyMessages(messages: TranscriptMessage[]): PrioritizedMessage[] {
|
|
157
|
+
const classified: PrioritizedMessage[] = [];
|
|
158
|
+
let firstUserSeen = false;
|
|
159
|
+
|
|
160
|
+
// Find last assistant message that is NOT a tool message (real final response)
|
|
161
|
+
const lastRealAssistantIdx = messages.reduce(
|
|
162
|
+
(last, m, i) => (m.role === "assistant" && !isToolContent(m.content)) ? i : last, -1
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < messages.length; i++) {
|
|
166
|
+
const msg = messages[i]!;
|
|
167
|
+
let priority: number;
|
|
168
|
+
|
|
169
|
+
// Tool content check first — tool messages in assistant role stay P2
|
|
170
|
+
if (isToolContent(msg.content)) {
|
|
171
|
+
priority = P_TOOL_ACTIVITY;
|
|
172
|
+
} else if (msg.role === "user" && !firstUserSeen) {
|
|
173
|
+
priority = P_USER_INSTRUCTION;
|
|
174
|
+
firstUserSeen = true;
|
|
175
|
+
} else if (msg.role === "assistant" && i === lastRealAssistantIdx) {
|
|
176
|
+
priority = P_FINAL_RESPONSE;
|
|
177
|
+
} else if (msg.role === "system") {
|
|
178
|
+
priority = P_SYSTEM;
|
|
179
|
+
} else {
|
|
180
|
+
priority = P_CONVERSATION;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
classified.push({ priority, index: i, role: msg.role, content: msg.content });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return classified;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function prepareTranscript(messages: TranscriptMessage[]): string {
|
|
133
190
|
const recent = messages.slice(-MAX_TRANSCRIPT_MESSAGES);
|
|
134
|
-
const lines: string[] = [];
|
|
135
|
-
let charCount = 0;
|
|
136
191
|
const charBudget = MAX_TRANSCRIPT_TOKENS * 4; // ~4 chars per token
|
|
137
192
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
193
|
+
const classified = classifyMessages(recent);
|
|
194
|
+
|
|
195
|
+
// Phase 1: Critical (P0 + P1) — always included, truncated to per-role limits
|
|
196
|
+
const critical = classified.filter(m => m.priority <= P_FINAL_RESPONSE);
|
|
197
|
+
const criticalLines = critical.map(m => {
|
|
198
|
+
const maxChars = m.role === "user" ? MAX_USER_MSG_CHARS * 2 : MAX_ASSISTANT_MSG_CHARS * 2;
|
|
199
|
+
const content = m.content.length > maxChars ? m.content.slice(0, maxChars) + "..." : m.content;
|
|
200
|
+
return { ...m, content, formatted: `[${m.role}]: ${content}` };
|
|
201
|
+
});
|
|
202
|
+
let used = criticalLines.reduce((sum, l) => sum + l.formatted.length + 1, 0);
|
|
203
|
+
|
|
204
|
+
// Phase 2: Tool activity (P2) — budget-allocated, truncate to fit (not drop)
|
|
205
|
+
const toolMsgs = classified.filter(m => m.priority === P_TOOL_ACTIVITY);
|
|
206
|
+
const toolLines: typeof criticalLines = [];
|
|
207
|
+
for (const m of toolMsgs) {
|
|
208
|
+
if (used >= charBudget) break;
|
|
209
|
+
const remaining = charBudget - used;
|
|
210
|
+
const prefix = `[${m.role}]: `;
|
|
211
|
+
const overhead = prefix.length + 1; // +1 for newline join
|
|
212
|
+
if (remaining <= overhead + 20) break; // not enough room for meaningful content
|
|
213
|
+
const contentBudget = Math.min(500, remaining - overhead);
|
|
214
|
+
const content = m.content.length > contentBudget
|
|
215
|
+
? m.content.slice(0, contentBudget - 3) + "..."
|
|
216
|
+
: m.content;
|
|
217
|
+
const formatted = `${prefix}${content}`;
|
|
218
|
+
toolLines.push({ ...m, content, formatted });
|
|
219
|
+
used += formatted.length + 1;
|
|
220
|
+
}
|
|
145
221
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
222
|
+
// Phase 3: Conversation (P3) — fills remaining budget
|
|
223
|
+
const convMsgs = classified.filter(m => m.priority === P_CONVERSATION);
|
|
224
|
+
const convLines: typeof criticalLines = [];
|
|
225
|
+
for (const m of convMsgs) {
|
|
226
|
+
if (used >= charBudget) break;
|
|
227
|
+
const maxChars = m.role === "user" ? MAX_USER_MSG_CHARS : MAX_ASSISTANT_MSG_CHARS;
|
|
228
|
+
const content = m.content.length > maxChars ? m.content.slice(0, maxChars) + "..." : m.content;
|
|
229
|
+
const formatted = `[${m.role}]: ${content}`;
|
|
230
|
+
if (used + formatted.length + 1 <= charBudget) {
|
|
231
|
+
convLines.push({ ...m, content, formatted });
|
|
232
|
+
used += formatted.length + 1;
|
|
233
|
+
}
|
|
149
234
|
}
|
|
150
235
|
|
|
151
|
-
|
|
236
|
+
// Reassemble in chronological order
|
|
237
|
+
const all = [...criticalLines, ...toolLines, ...convLines];
|
|
238
|
+
all.sort((a, b) => a.index - b.index);
|
|
239
|
+
|
|
240
|
+
return all.map(l => l.formatted).join("\n");
|
|
152
241
|
}
|
|
153
242
|
|
|
154
243
|
// =============================================================================
|