-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathcapture.ts
More file actions
164 lines (146 loc) · 5.02 KB
/
Copy pathcapture.ts
File metadata and controls
164 lines (146 loc) · 5.02 KB
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
/**
* Shared capture logic used by both recall and flush hooks.
* Reads transcript entries since last capture, filters by signals,
* and saves to the user container.
*/
import { existsSync } from "node:fs";
import {
SupermemoryClient,
USER_ENTITY_CONTEXT,
} from "./client.js";
import { log } from "./logger.js";
import {
parseTranscript,
getEntriesSince,
formatTranscript,
findTranscriptPath,
} from "./transcript.js";
import { getLastCapturedIndex, setLastCapturedIndex } from "./tracker.js";
import { filterBySignals, groupEntriesIntoTurns } from "./signals.js";
export interface CaptureOptions {
/** Minimum number of new entries required before capturing. Default: 0 */
requireMinEntries?: number;
/** Minimum number of turns (including current) before capturing. Default: 0 */
requireMinTurns?: number;
}
/**
* Resolve a transcript path — either from the provided value or by
* searching for a file matching the session ID.
*/
export function resolveTranscriptPath(
transcriptPath: string | null | undefined,
sessionId: string,
): string | null {
if (transcriptPath) return transcriptPath;
return findTranscriptPath(sessionId);
}
/**
* Capture new transcript entries since last capture, filter by signals,
* and save to the user container.
*
* @param caller Label for log messages (e.g. "recall" or "flush")
* @param client Supermemory API client
* @param sessionId Session identifier
* @param transcriptPath Path to the transcript JSONL file (or null)
* @param tags Container tags for project and user
* @param options Optional gating thresholds
*/
export async function captureEntries(
caller: string,
client: SupermemoryClient,
sessionId: string,
transcriptPath: string | null,
tags: { project: string; user: string },
options: CaptureOptions = {},
): Promise<void> {
const { requireMinEntries = 0, requireMinTurns = 0 } = options;
if (!transcriptPath || !existsSync(transcriptPath)) {
log(`${caller}: no transcript to capture from`, { sessionId, transcriptPath });
return;
}
const entries = parseTranscript(transcriptPath);
if (entries.length === 0) {
log(`${caller}: transcript empty`, { sessionId });
return;
}
const lastIndex = getLastCapturedIndex(sessionId);
const newEntries = getEntriesSince(entries, lastIndex);
if (requireMinEntries > 0 && newEntries.length < requireMinEntries) {
log(`${caller}: not enough new entries to capture`, {
sessionId,
newCount: newEntries.length,
required: requireMinEntries,
lastIndex,
});
return;
}
if (newEntries.length === 0) {
log(`${caller}: no new entries to capture`, { sessionId });
return;
}
// Turn-based gating (used by recall to batch captures)
if (requireMinTurns > 0) {
const turns = groupEntriesIntoTurns(newEntries);
const effectiveTurnCount = turns.length + 1; // +1 for current user prompt
if (effectiveTurnCount < requireMinTurns) {
log(`${caller}: waiting for more turns before capture`, {
sessionId,
turnCount: effectiveTurnCount,
requiredTurns: requireMinTurns,
lastIndex,
});
return;
}
}
// Filter to only entries with meaningful signals (preferences, decisions, etc.)
const signalEntries = filterBySignals(newEntries);
if (signalEntries.length === 0) {
log(`${caller}: no signal entries to capture`, {
sessionId,
totalNew: newEntries.length,
lastIndex,
});
// Still update tracker so we don't re-check these entries
const lastEntry = newEntries[newEntries.length - 1];
setLastCapturedIndex(sessionId, lastEntry.index);
return;
}
log(`${caller}: capturing signal entries`, {
sessionId,
signalCount: signalEntries.length,
totalNew: newEntries.length,
lastIndex,
});
const transcript = formatTranscript(signalEntries);
const rawContent = `[Session ${sessionId}]\n${transcript}`;
const content = rawContent
.replace(/\[SUPERMEMORY CONTAINERS\][\s\S]*?\[END SUPERMEMORY CONTAINERS\]\s*/g, "")
.replace(/<supermemory-containers>[\s\S]*?<\/supermemory-containers>\s*/g, "")
.trim();
const metadata = {
type: "conversation" as const,
sessionId,
entryCount: newEntries.length,
timestamp: new Date().toISOString(),
sm_capture_mode: caller === "flush" ? "session_end" : "turn",
};
// Save automatic transcript capture to the user container. Explicit project
// knowledge is still saved via the supermemory-save skill.
// Use customId so all session turns go into the same document.
try {
await client.addMemory(content, tags.user, metadata, {
customId: sessionId,
entityContext: USER_ENTITY_CONTEXT,
});
const lastEntry = newEntries[newEntries.length - 1];
setLastCapturedIndex(sessionId, lastEntry.index);
log(`${caller}: captured entries`, {
sessionId,
count: newEntries.length,
lastIndex: lastEntry.index,
});
} catch (error) {
log(`${caller}: capture error`, { error: String(error) });
// Don't rethrow — let the caller decide how to handle
}
}