@stackmemoryai/stackmemory 0.5.49 → 0.5.51
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/README.md +17 -3
- package/dist/cli/claude-sm.js +246 -5
- package/dist/cli/claude-sm.js.map +3 -3
- package/dist/cli/commands/sweep.js +190 -421
- package/dist/cli/commands/sweep.js.map +3 -3
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +2 -2
- package/dist/core/config/feature-flags.js +7 -1
- package/dist/core/config/feature-flags.js.map +2 -2
- package/dist/core/context/enhanced-rehydration.js +355 -9
- package/dist/core/context/enhanced-rehydration.js.map +3 -3
- package/dist/core/context/shared-context-layer.js +229 -0
- package/dist/core/context/shared-context-layer.js.map +2 -2
- package/dist/features/sweep/index.js +20 -0
- package/dist/features/sweep/index.js.map +7 -0
- package/dist/features/sweep/prediction-client.js +155 -0
- package/dist/features/sweep/prediction-client.js.map +7 -0
- package/dist/features/sweep/prompt-builder.js +85 -0
- package/dist/features/sweep/prompt-builder.js.map +7 -0
- package/dist/features/sweep/pty-wrapper.js +171 -0
- package/dist/features/sweep/pty-wrapper.js.map +7 -0
- package/dist/features/sweep/state-watcher.js +87 -0
- package/dist/features/sweep/state-watcher.js.map +7 -0
- package/dist/features/sweep/status-bar.js +88 -0
- package/dist/features/sweep/status-bar.js.map +7 -0
- package/dist/features/sweep/sweep-server-manager.js +226 -0
- package/dist/features/sweep/sweep-server-manager.js.map +7 -0
- package/dist/features/sweep/tab-interceptor.js +38 -0
- package/dist/features/sweep/tab-interceptor.js.map +7 -0
- package/dist/features/sweep/types.js +18 -0
- package/dist/features/sweep/types.js.map +7 -0
- package/package.json +1 -1
- package/scripts/test-setup-e2e.sh +154 -0
|
@@ -5,6 +5,327 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import * as fs from "fs/promises";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
import { logger } from "../monitoring/logger.js";
|
|
8
|
+
class CompactionHandler {
|
|
9
|
+
frameManager;
|
|
10
|
+
metrics;
|
|
11
|
+
tokenAccumulator = 0;
|
|
12
|
+
preservedAnchors = /* @__PURE__ */ new Map();
|
|
13
|
+
constructor(frameManager) {
|
|
14
|
+
this.frameManager = frameManager;
|
|
15
|
+
this.metrics = {
|
|
16
|
+
estimatedTokens: 0,
|
|
17
|
+
warningThreshold: 15e4,
|
|
18
|
+
// 150K tokens
|
|
19
|
+
criticalThreshold: 17e4,
|
|
20
|
+
// 170K tokens
|
|
21
|
+
anchorsPreserved: 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Track token usage from a message
|
|
26
|
+
*/
|
|
27
|
+
trackTokens(content) {
|
|
28
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
29
|
+
this.tokenAccumulator += estimatedTokens;
|
|
30
|
+
this.metrics.estimatedTokens += estimatedTokens;
|
|
31
|
+
if (this.isApproachingCompaction()) {
|
|
32
|
+
this.preserveCriticalContext();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if approaching compaction threshold
|
|
37
|
+
*/
|
|
38
|
+
isApproachingCompaction() {
|
|
39
|
+
return this.metrics.estimatedTokens >= this.metrics.warningThreshold;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if past critical threshold
|
|
43
|
+
*/
|
|
44
|
+
isPastCriticalThreshold() {
|
|
45
|
+
return this.metrics.estimatedTokens >= this.metrics.criticalThreshold;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detect if compaction likely occurred
|
|
49
|
+
*/
|
|
50
|
+
detectCompactionEvent(content) {
|
|
51
|
+
const compactionIndicators = [
|
|
52
|
+
"earlier in this conversation",
|
|
53
|
+
"previously discussed",
|
|
54
|
+
"as mentioned before",
|
|
55
|
+
"summarized for brevity",
|
|
56
|
+
"[conversation compressed]",
|
|
57
|
+
"[context truncated]"
|
|
58
|
+
];
|
|
59
|
+
const lowerContent = content.toLowerCase();
|
|
60
|
+
return compactionIndicators.some(
|
|
61
|
+
(indicator) => lowerContent.includes(indicator)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Preserve critical context before compaction
|
|
66
|
+
*/
|
|
67
|
+
async preserveCriticalContext() {
|
|
68
|
+
try {
|
|
69
|
+
const currentFrameId = this.frameManager.getCurrentFrameId();
|
|
70
|
+
if (!currentFrameId) {
|
|
71
|
+
logger.warn("No active frame to preserve context from");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const events = this.frameManager.getFrameEvents(currentFrameId);
|
|
75
|
+
const toolCalls = this.extractToolCalls(events);
|
|
76
|
+
const fileOps = this.extractFileOperations(events);
|
|
77
|
+
const decisions = this.extractDecisions(events);
|
|
78
|
+
const errorPatterns = this.extractErrorPatterns(events);
|
|
79
|
+
const anchor = {
|
|
80
|
+
anchor_id: `compact_${Date.now()}`,
|
|
81
|
+
type: "COMPACTION_PRESERVE",
|
|
82
|
+
priority: 10,
|
|
83
|
+
content: {
|
|
84
|
+
tool_calls: toolCalls,
|
|
85
|
+
file_operations: fileOps,
|
|
86
|
+
decisions,
|
|
87
|
+
error_resolutions: errorPatterns
|
|
88
|
+
},
|
|
89
|
+
created_at: Date.now(),
|
|
90
|
+
token_estimate: this.metrics.estimatedTokens
|
|
91
|
+
};
|
|
92
|
+
this.frameManager.addAnchor(
|
|
93
|
+
"CONSTRAINT",
|
|
94
|
+
// Using CONSTRAINT type for now
|
|
95
|
+
JSON.stringify(anchor),
|
|
96
|
+
10,
|
|
97
|
+
{
|
|
98
|
+
compaction_preserve: true,
|
|
99
|
+
token_count: this.metrics.estimatedTokens
|
|
100
|
+
},
|
|
101
|
+
currentFrameId
|
|
102
|
+
);
|
|
103
|
+
this.preservedAnchors.set(anchor.anchor_id, anchor);
|
|
104
|
+
this.metrics.anchorsPreserved++;
|
|
105
|
+
logger.info(
|
|
106
|
+
`Preserved critical context at ${this.metrics.estimatedTokens} tokens`
|
|
107
|
+
);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error(
|
|
110
|
+
"Failed to preserve critical context:",
|
|
111
|
+
error instanceof Error ? error : void 0
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Extract tool calls from events
|
|
117
|
+
*/
|
|
118
|
+
extractToolCalls(events) {
|
|
119
|
+
const toolCalls = [];
|
|
120
|
+
const toolEvents = events.filter((e) => e.event_type === "tool_call");
|
|
121
|
+
for (const event of toolEvents) {
|
|
122
|
+
const resultEvent = events.find(
|
|
123
|
+
(e) => e.event_type === "tool_result" && e.seq > event.seq && e.payload.tool_name === event.payload.tool_name
|
|
124
|
+
);
|
|
125
|
+
toolCalls.push({
|
|
126
|
+
tool: event.payload.tool_name || "unknown",
|
|
127
|
+
timestamp: event.ts,
|
|
128
|
+
key_inputs: this.extractKeyInputs(event.payload),
|
|
129
|
+
key_outputs: resultEvent ? this.extractKeyOutputs(resultEvent.payload) : {},
|
|
130
|
+
files_affected: this.extractAffectedFiles(
|
|
131
|
+
event.payload,
|
|
132
|
+
resultEvent?.payload
|
|
133
|
+
),
|
|
134
|
+
success: resultEvent ? !resultEvent.payload.error : false,
|
|
135
|
+
error: resultEvent?.payload.error
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return toolCalls;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Extract key inputs from tool call
|
|
142
|
+
*/
|
|
143
|
+
extractKeyInputs(payload) {
|
|
144
|
+
const keys = [
|
|
145
|
+
"file_path",
|
|
146
|
+
"command",
|
|
147
|
+
"query",
|
|
148
|
+
"path",
|
|
149
|
+
"pattern",
|
|
150
|
+
"content"
|
|
151
|
+
];
|
|
152
|
+
const result = {};
|
|
153
|
+
for (const key of keys) {
|
|
154
|
+
if (payload.arguments?.[key]) {
|
|
155
|
+
result[key] = payload.arguments[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extract key outputs from tool result
|
|
162
|
+
*/
|
|
163
|
+
extractKeyOutputs(payload) {
|
|
164
|
+
return {
|
|
165
|
+
success: !payload.error,
|
|
166
|
+
error: payload.error,
|
|
167
|
+
result_type: payload.result_type,
|
|
168
|
+
files_created: payload.files_created,
|
|
169
|
+
files_modified: payload.files_modified
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Extract affected files from tool events
|
|
174
|
+
*/
|
|
175
|
+
extractAffectedFiles(callPayload, resultPayload) {
|
|
176
|
+
const files = /* @__PURE__ */ new Set();
|
|
177
|
+
if (callPayload?.arguments?.file_path) {
|
|
178
|
+
files.add(callPayload.arguments.file_path);
|
|
179
|
+
}
|
|
180
|
+
if (callPayload?.arguments?.path) {
|
|
181
|
+
files.add(callPayload.arguments.path);
|
|
182
|
+
}
|
|
183
|
+
if (resultPayload?.files_created) {
|
|
184
|
+
resultPayload.files_created.forEach((f) => files.add(f));
|
|
185
|
+
}
|
|
186
|
+
if (resultPayload?.files_modified) {
|
|
187
|
+
resultPayload.files_modified.forEach((f) => files.add(f));
|
|
188
|
+
}
|
|
189
|
+
return Array.from(files);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Extract file operations from events
|
|
193
|
+
*/
|
|
194
|
+
extractFileOperations(events) {
|
|
195
|
+
const fileOps = [];
|
|
196
|
+
const fileTools = ["Read", "Write", "Edit", "MultiEdit", "Delete"];
|
|
197
|
+
const toolEvents = events.filter(
|
|
198
|
+
(e) => e.event_type === "tool_call" && fileTools.includes(e.payload.tool_name)
|
|
199
|
+
);
|
|
200
|
+
for (const event of toolEvents) {
|
|
201
|
+
const operation = this.mapToolToOperation(event.payload.tool_name);
|
|
202
|
+
const path2 = event.payload.arguments?.file_path || event.payload.arguments?.path || "unknown";
|
|
203
|
+
fileOps.push({
|
|
204
|
+
type: operation,
|
|
205
|
+
path: path2,
|
|
206
|
+
timestamp: event.ts,
|
|
207
|
+
success: true,
|
|
208
|
+
// Will be updated from result
|
|
209
|
+
error: void 0
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return fileOps;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Map tool name to file operation type
|
|
216
|
+
*/
|
|
217
|
+
mapToolToOperation(toolName) {
|
|
218
|
+
const mapping = {
|
|
219
|
+
Read: "read",
|
|
220
|
+
Write: "write",
|
|
221
|
+
Edit: "edit",
|
|
222
|
+
MultiEdit: "edit",
|
|
223
|
+
Delete: "delete"
|
|
224
|
+
};
|
|
225
|
+
return mapping[toolName] || "read";
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Extract decisions from events
|
|
229
|
+
*/
|
|
230
|
+
extractDecisions(events) {
|
|
231
|
+
const decisions = [];
|
|
232
|
+
const decisionEvents = events.filter((e) => e.event_type === "decision");
|
|
233
|
+
for (const event of decisionEvents) {
|
|
234
|
+
if (event.payload.text) {
|
|
235
|
+
decisions.push(event.payload.text);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return decisions;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Extract error patterns and resolutions
|
|
242
|
+
*/
|
|
243
|
+
extractErrorPatterns(events) {
|
|
244
|
+
const patterns = [];
|
|
245
|
+
const errorEvents = events.filter(
|
|
246
|
+
(e) => e.event_type === "tool_result" && e.payload.error
|
|
247
|
+
);
|
|
248
|
+
for (const errorEvent of errorEvents) {
|
|
249
|
+
const subsequentTools = events.filter((e) => e.event_type === "tool_call" && e.seq > errorEvent.seq).slice(0, 3);
|
|
250
|
+
if (subsequentTools.length > 0) {
|
|
251
|
+
patterns.push({
|
|
252
|
+
error: errorEvent.payload.error,
|
|
253
|
+
resolution: `Attempted resolution with ${subsequentTools.map((t) => t.payload.tool_name).join(", ")}`,
|
|
254
|
+
tool_sequence: subsequentTools.map((t) => t.payload.tool_name),
|
|
255
|
+
timestamp: errorEvent.ts
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return patterns;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Restore context after compaction detected
|
|
263
|
+
*/
|
|
264
|
+
async restoreContext() {
|
|
265
|
+
if (this.preservedAnchors.size === 0) {
|
|
266
|
+
logger.warn("No preserved anchors to restore from");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const anchors = Array.from(this.preservedAnchors.values());
|
|
270
|
+
anchors.sort((a, b) => b.created_at - a.created_at);
|
|
271
|
+
const latestAnchor = anchors[0];
|
|
272
|
+
const restorationFrame = this.frameManager.createFrame({
|
|
273
|
+
type: "review",
|
|
274
|
+
name: "Context Restoration After Compaction",
|
|
275
|
+
inputs: { reason: "autocompaction_detected" }
|
|
276
|
+
});
|
|
277
|
+
this.frameManager.addAnchor(
|
|
278
|
+
"FACT",
|
|
279
|
+
`Context restored from token position ${latestAnchor.token_estimate}`,
|
|
280
|
+
10,
|
|
281
|
+
{ restoration: true },
|
|
282
|
+
restorationFrame
|
|
283
|
+
);
|
|
284
|
+
const toolSequence = latestAnchor.content.tool_calls.map((t) => t.tool).join(" \u2192 ");
|
|
285
|
+
this.frameManager.addAnchor(
|
|
286
|
+
"FACT",
|
|
287
|
+
`Tool sequence: ${toolSequence}`,
|
|
288
|
+
9,
|
|
289
|
+
{},
|
|
290
|
+
restorationFrame
|
|
291
|
+
);
|
|
292
|
+
const files = /* @__PURE__ */ new Set();
|
|
293
|
+
latestAnchor.content.file_operations.forEach((op) => files.add(op.path));
|
|
294
|
+
if (files.size > 0) {
|
|
295
|
+
this.frameManager.addAnchor(
|
|
296
|
+
"FACT",
|
|
297
|
+
`Files touched: ${Array.from(files).join(", ")}`,
|
|
298
|
+
8,
|
|
299
|
+
{},
|
|
300
|
+
restorationFrame
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
for (const decision of latestAnchor.content.decisions) {
|
|
304
|
+
this.frameManager.addAnchor(
|
|
305
|
+
"DECISION",
|
|
306
|
+
decision,
|
|
307
|
+
7,
|
|
308
|
+
{},
|
|
309
|
+
restorationFrame
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
logger.info("Context restored after compaction detection");
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get current metrics
|
|
316
|
+
*/
|
|
317
|
+
getMetrics() {
|
|
318
|
+
return { ...this.metrics };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Reset token counter (e.g., at session start)
|
|
322
|
+
*/
|
|
323
|
+
resetTokenCount() {
|
|
324
|
+
this.metrics.estimatedTokens = 0;
|
|
325
|
+
this.tokenAccumulator = 0;
|
|
326
|
+
this.metrics.lastCompactionAt = void 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
8
329
|
class EnhancedRehydrationManager {
|
|
9
330
|
frameManager;
|
|
10
331
|
compactionHandler;
|
|
@@ -303,7 +624,9 @@ class EnhancedRehydrationManager {
|
|
|
303
624
|
}
|
|
304
625
|
const dirs = files.map((f) => path.dirname(f)).filter((v, i, a) => a.indexOf(v) === i);
|
|
305
626
|
mapping.key_directories = dirs.filter(
|
|
306
|
-
(dir) => ["src", "lib", "components", "pages", "api", "utils", "types"].some(
|
|
627
|
+
(dir) => ["src", "lib", "components", "pages", "api", "utils", "types"].some(
|
|
628
|
+
(key) => dir.includes(key)
|
|
629
|
+
)
|
|
307
630
|
);
|
|
308
631
|
} catch (error) {
|
|
309
632
|
logger.warn("Failed to analyze project mapping:", error);
|
|
@@ -321,7 +644,10 @@ class EnhancedRehydrationManager {
|
|
|
321
644
|
const fileSnapshots = [];
|
|
322
645
|
const recentFiles = await this.getRecentlyModifiedFiles(workingDir);
|
|
323
646
|
for (const file of recentFiles.slice(0, 20)) {
|
|
324
|
-
const snapshot = await this.captureFileSnapshot(
|
|
647
|
+
const snapshot = await this.captureFileSnapshot(
|
|
648
|
+
file,
|
|
649
|
+
this.inferContextTags(file)
|
|
650
|
+
);
|
|
325
651
|
if (snapshot) {
|
|
326
652
|
fileSnapshots.push(snapshot);
|
|
327
653
|
}
|
|
@@ -336,13 +662,21 @@ class EnhancedRehydrationManager {
|
|
|
336
662
|
conversation_context: conversationContext,
|
|
337
663
|
project_mapping: projectMapping,
|
|
338
664
|
active_workflows: this.detectActiveWorkflows(fileSnapshots),
|
|
339
|
-
current_focus: this.inferCurrentFocus(
|
|
665
|
+
current_focus: this.inferCurrentFocus(
|
|
666
|
+
fileSnapshots,
|
|
667
|
+
conversationContext
|
|
668
|
+
)
|
|
340
669
|
},
|
|
341
|
-
recovery_anchors: this.createRecoveryAnchors(
|
|
670
|
+
recovery_anchors: this.createRecoveryAnchors(
|
|
671
|
+
fileSnapshots,
|
|
672
|
+
conversationContext
|
|
673
|
+
)
|
|
342
674
|
};
|
|
343
675
|
this.rehydrationStorage.set(checkpointId, rehydrationContext);
|
|
344
676
|
await this.persistRehydrationContext(checkpointId, rehydrationContext);
|
|
345
|
-
logger.info(
|
|
677
|
+
logger.info(
|
|
678
|
+
`Created rehydration checkpoint ${checkpointId} with ${fileSnapshots.length} file snapshots`
|
|
679
|
+
);
|
|
346
680
|
return checkpointId;
|
|
347
681
|
} catch (error) {
|
|
348
682
|
logger.error("Failed to create rehydration checkpoint:", error);
|
|
@@ -383,7 +717,10 @@ class EnhancedRehydrationManager {
|
|
|
383
717
|
logger.warn("No active frame for context injection");
|
|
384
718
|
return;
|
|
385
719
|
}
|
|
386
|
-
for (const snapshot of context.pre_compact_state.file_snapshots.slice(
|
|
720
|
+
for (const snapshot of context.pre_compact_state.file_snapshots.slice(
|
|
721
|
+
0,
|
|
722
|
+
5
|
|
723
|
+
)) {
|
|
387
724
|
this.frameManager.addAnchor(
|
|
388
725
|
"FACT",
|
|
389
726
|
`File: ${snapshot.path} (${snapshot.contextTags.join(", ")})
|
|
@@ -479,7 +816,8 @@ Stack preview: ${trace.stack_frames.slice(0, 3).join("\n")}`,
|
|
|
479
816
|
inferContextTags(filePath) {
|
|
480
817
|
const tags = [];
|
|
481
818
|
const content = filePath.toLowerCase();
|
|
482
|
-
if (content.includes("pipeline") || content.includes("migrate"))
|
|
819
|
+
if (content.includes("pipeline") || content.includes("migrate"))
|
|
820
|
+
tags.push("migration");
|
|
483
821
|
if (content.includes("hubspot")) tags.push("hubspot");
|
|
484
822
|
if (content.includes("pipedream")) tags.push("pipedream");
|
|
485
823
|
if (content.includes("test")) tags.push("test");
|
|
@@ -600,7 +938,9 @@ Stack preview: ${trace.stack_frames.slice(0, 3).join("\n")}`,
|
|
|
600
938
|
createRecoveryAnchors(snapshots, context) {
|
|
601
939
|
const anchors = [];
|
|
602
940
|
for (const snapshot of snapshots.slice(0, 3)) {
|
|
603
|
-
anchors.push(
|
|
941
|
+
anchors.push(
|
|
942
|
+
`File context: ${snapshot.path} with ${snapshot.contextTags.join(", ")}`
|
|
943
|
+
);
|
|
604
944
|
}
|
|
605
945
|
return anchors;
|
|
606
946
|
}
|
|
@@ -614,7 +954,12 @@ Stack preview: ${trace.stack_frames.slice(0, 3).join("\n")}`,
|
|
|
614
954
|
}
|
|
615
955
|
async loadPersistedContext(id) {
|
|
616
956
|
try {
|
|
617
|
-
const contextPath = path.join(
|
|
957
|
+
const contextPath = path.join(
|
|
958
|
+
process.cwd(),
|
|
959
|
+
".stackmemory",
|
|
960
|
+
"rehydration",
|
|
961
|
+
`${id}.json`
|
|
962
|
+
);
|
|
618
963
|
const content = await fs.readFile(contextPath, "utf8");
|
|
619
964
|
return JSON.parse(content);
|
|
620
965
|
} catch {
|
|
@@ -643,6 +988,7 @@ Stack preview: ${trace.stack_frames.slice(0, 3).join("\n")}`,
|
|
|
643
988
|
}
|
|
644
989
|
}
|
|
645
990
|
export {
|
|
991
|
+
CompactionHandler,
|
|
646
992
|
EnhancedRehydrationManager
|
|
647
993
|
};
|
|
648
994
|
//# sourceMappingURL=enhanced-rehydration.js.map
|