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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/observer.ts +104 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmem",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "On-device memory layer for AI agents. Claude Code, OpenClaw, and Hermes. Hooks + MCP server + hybrid RAG search.",
5
5
  "type": "module",
6
6
  "bin": {
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
- function prepareTranscript(messages: TranscriptMessage[]): string {
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
- for (const msg of recent) {
139
- if (charCount >= charBudget) break;
140
-
141
- const maxChars = msg.role === "user" ? MAX_USER_MSG_CHARS : MAX_ASSISTANT_MSG_CHARS;
142
- const content = msg.content.length > maxChars
143
- ? msg.content.slice(0, maxChars) + "..."
144
- : msg.content;
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
- const line = `[${msg.role}]: ${content}`;
147
- lines.push(line);
148
- charCount += line.length;
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
- return lines.join("\n");
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
  // =============================================================================