@memorax/memorax-code 0.1.6 → 0.1.8
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 +36 -5
- package/bin/memorax-code-setup.mjs +0 -3
- package/docs/configuration.md +1 -3
- package/docs/troubleshooting.md +27 -8
- package/lib/memorax-code-backend/dist/app/backend-server.js +2 -29
- package/lib/memorax-code-backend/dist/app/memory-observability.js +1 -4
- package/lib/memorax-code-backend/dist/app/state.js +1 -12
- package/lib/memorax-code-backend/dist/clients/claude/transcript-turn.js +0 -182
- package/lib/memorax-code-backend/dist/clients/opencode/message-turn.js +68 -2
- package/lib/memorax-code-backend/dist/lifecycle/backend/service.js +1 -5
- package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +1 -14
- package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +8 -5
- package/lib/memorax-code-backend/dist/memory/project.js +4 -26
- package/lib/memorax-code-backend/dist/memory/quota-notice.js +79 -19
- package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +1 -4
- package/lib/memorax-code-backend/dist/provider/memorax/http.js +0 -28
- package/lib/memorax-code-backend/dist/transport/http/request.js +1 -39
- package/lib/memorax-code-backend/package.json +2 -3
- package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-adapter/skills/memorax-code/SKILL.md +0 -6
- package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +1 -1
- package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/SKILL.md +0 -6
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +1 -1
- package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
- package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-codex-adapter/package.json +1 -1
- package/lib/memorax-code-codex-adapter/skills/memorax-code/SKILL.md +0 -6
- package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +1 -1
- package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +1 -1
- package/lib/memorax-code-dsh-adapter/package.json +1 -1
- package/lib/memorax-code-dsh-adapter/skills/memorax-code/SKILL.md +0 -6
- package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +1 -1
- package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +1 -1
- package/lib/memorax-code-opencode-adapter/package.json +1 -1
- package/lib/memorax-code-opencode-adapter/skills/memorax-code/SKILL.md +0 -6
- package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +1 -1
- package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +1 -1
- package/lib/memorax-code-opencode-adapter/src/plugin.mjs +121 -16
- package/lib/memorax-code-opencode-adapter/src/repo-memory-server-runner.mjs +2 -0
- package/lib/package-transition.mjs +4 -3
- package/package.json +1 -1
- package/lib/memorax-code-backend/dist/clients/codex/effective-prompt.js +0 -54
- package/lib/memorax-code-backend/dist/memory/writeback-reconciler.js +0 -244
- package/lib/memorax-code-backend/dist/memory/writeback-task-projection.js +0 -377
- package/lib/memorax-code-backend/dist/repository/readiness.js +0 -188
- package/lib/memorax-code-backend/dist/shared/incremental-jsonl-projection.js +0 -271
- package/lib/memorax-code-backend/dist/viewer/history/claude-transcript.js +0 -648
- package/lib/memorax-code-backend/dist/viewer/history/session-title.js +0 -93
- package/lib/memorax-code-backend/dist/viewer/http/public-routes.js +0 -146
- package/lib/memorax-code-backend/dist/viewer/model.js +0 -1
- package/lib/memorax-code-backend/dist/viewer/projection/activity.js +0 -113
- package/lib/memorax-code-backend/dist/viewer/projection/cache.js +0 -69
- package/lib/memorax-code-backend/dist/viewer/projection/event-identity.js +0 -3
- package/lib/memorax-code-backend/dist/viewer/projection/history.js +0 -79
- package/lib/memorax-code-backend/dist/viewer/projection/observability.js +0 -8
- package/lib/memorax-code-backend/dist/viewer/projection/redaction.js +0 -52
- package/lib/memorax-code-backend/dist/viewer/projection/turn-reference.js +0 -12
- package/lib/memorax-code-backend/dist/viewer/projection/user.js +0 -168
- package/lib/memorax-code-backend/dist/viewer/projection/writeback-status.js +0 -97
- package/lib/memorax-code-backend/dist/viewer/store.js +0 -991
- package/lib/memorax-code-backend/dist/viewer/ui/icon.js +0 -1
- package/lib/memorax-code-backend/dist/viewer/ui/logo.js +0 -1
- package/lib/memorax-code-backend/dist/viewer/ui/user-html.js +0 -117
|
@@ -1,648 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { open, readdir, stat } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, join, resolve as resolvePath } from "node:path";
|
|
5
|
-
import { memoryViewerEventKey } from "../projection/event-identity.js";
|
|
6
|
-
import { claudeTranscriptMemoryActivitiesFromJsonLines } from "../../clients/claude/transcript-turn.js";
|
|
7
|
-
import { resolveMemoryProject, } from "../../memory/project.js";
|
|
8
|
-
const projections = new Map();
|
|
9
|
-
const EMPTY_TRANSCRIPT_HISTORY = Object.freeze([]);
|
|
10
|
-
const MAX_TRANSCRIPT_FILES = 250;
|
|
11
|
-
const MAX_TRANSCRIPT_FILE_BYTES = 8 * 1024 * 1024;
|
|
12
|
-
const MAX_TRANSCRIPT_REFRESH_BYTES = 64 * 1024 * 1024;
|
|
13
|
-
const MAX_TRANSCRIPT_EVENTS_PER_FILE = 2_000;
|
|
14
|
-
const MAX_TRANSCRIPT_RETAINED_EVENTS = 4_000;
|
|
15
|
-
const MAX_TRANSCRIPT_RETAINED_BYTES = 32 * 1024 * 1024;
|
|
16
|
-
export function claudeLocalProjectsRoot(env = process.env) {
|
|
17
|
-
const claudeHome = env.CLAUDE_CONFIG_DIR?.trim()
|
|
18
|
-
|| env.CLAUDE_HOME?.trim()
|
|
19
|
-
|| join(homedir(), ".claude");
|
|
20
|
-
return join(resolvePath(claudeHome), "projects");
|
|
21
|
-
}
|
|
22
|
-
export async function readClaudeLocalTranscriptHistory(projectsRoot, resolveProject = resolveMemoryProject) {
|
|
23
|
-
if (typeof projectsRoot !== "string" || !projectsRoot.trim()) {
|
|
24
|
-
return EMPTY_TRANSCRIPT_HISTORY;
|
|
25
|
-
}
|
|
26
|
-
const normalizedRoot = resolvePath(projectsRoot.trim());
|
|
27
|
-
let projection = projections.get(normalizedRoot);
|
|
28
|
-
if (projection && projection.identity !== resolveProject)
|
|
29
|
-
projection = undefined;
|
|
30
|
-
if (!projection) {
|
|
31
|
-
projection = { identity: resolveProject, files: new Map(), values: [] };
|
|
32
|
-
projections.set(normalizedRoot, projection);
|
|
33
|
-
}
|
|
34
|
-
if (projection.refresh)
|
|
35
|
-
return projection.refresh;
|
|
36
|
-
const refresh = refreshProjection(projection, normalizedRoot, resolveProject);
|
|
37
|
-
projection.refresh = refresh;
|
|
38
|
-
try {
|
|
39
|
-
return await refresh;
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
if (projection.refresh === refresh)
|
|
43
|
-
projection.refresh = undefined;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
export function clearClaudeLocalTranscriptProjections() {
|
|
47
|
-
projections.clear();
|
|
48
|
-
}
|
|
49
|
-
async function refreshProjection(projection, projectsRoot, resolveProject) {
|
|
50
|
-
const files = await discoverTranscriptFiles(projectsRoot);
|
|
51
|
-
const activePaths = new Set(files.map((file) => file.path));
|
|
52
|
-
let changed = false;
|
|
53
|
-
let allowBackfill = false;
|
|
54
|
-
for (const path of projection.files.keys()) {
|
|
55
|
-
if (activePaths.has(path))
|
|
56
|
-
continue;
|
|
57
|
-
projection.files.delete(path);
|
|
58
|
-
changed = true;
|
|
59
|
-
allowBackfill = true;
|
|
60
|
-
}
|
|
61
|
-
let remainingBytes = MAX_TRANSCRIPT_REFRESH_BYTES;
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
const cached = projection.files.get(file.path);
|
|
64
|
-
const unchanged = cached
|
|
65
|
-
&& cached.observedSize === file.size
|
|
66
|
-
&& cached.modifiedAtMs === file.modifiedAtMs
|
|
67
|
-
&& cached.changedAtMs === file.changedAtMs
|
|
68
|
-
&& cached.inode === file.inode;
|
|
69
|
-
if (unchanged && (!allowBackfill || cached.projectionTruncated !== true)) {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (remainingBytes <= 0)
|
|
73
|
-
continue;
|
|
74
|
-
const bytesToRead = Math.min(file.size, MAX_TRANSCRIPT_FILE_BYTES);
|
|
75
|
-
if (bytesToRead > remainingBytes)
|
|
76
|
-
continue;
|
|
77
|
-
const rangeStart = Math.max(0, file.size - bytesToRead);
|
|
78
|
-
let buffer;
|
|
79
|
-
try {
|
|
80
|
-
buffer = await readFileRange(file.path, rangeStart, file.size);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
if (cached) {
|
|
84
|
-
projection.files.delete(file.path);
|
|
85
|
-
changed = true;
|
|
86
|
-
allowBackfill = true;
|
|
87
|
-
}
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
remainingBytes -= buffer.byteLength;
|
|
91
|
-
const parsed = projectTranscriptChunk(buffer, resolveProject, initialParserState(file.sessionId), rangeStart > 0);
|
|
92
|
-
const events = parsed.events.slice(-MAX_TRANSCRIPT_EVENTS_PER_FILE);
|
|
93
|
-
projection.files.set(file.path, {
|
|
94
|
-
observedSize: file.size,
|
|
95
|
-
modifiedAtMs: file.modifiedAtMs,
|
|
96
|
-
changedAtMs: file.changedAtMs,
|
|
97
|
-
inode: file.inode,
|
|
98
|
-
events,
|
|
99
|
-
projectionTruncated: false,
|
|
100
|
-
});
|
|
101
|
-
if (!sameTranscriptEvents(events, cached?.events)) {
|
|
102
|
-
changed = true;
|
|
103
|
-
allowBackfill = true;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (changed) {
|
|
107
|
-
projection.values = retainTranscriptProjectionEvents(projection);
|
|
108
|
-
}
|
|
109
|
-
return projection.values;
|
|
110
|
-
}
|
|
111
|
-
function retainTranscriptProjectionEvents(projection) {
|
|
112
|
-
const candidates = [...projection.files.values()]
|
|
113
|
-
.flatMap((file) => file.events)
|
|
114
|
-
.sort(compareEvents);
|
|
115
|
-
const retained = [];
|
|
116
|
-
let retainedBytes = 0;
|
|
117
|
-
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
118
|
-
const event = candidates[index];
|
|
119
|
-
if (!event || retained.length >= MAX_TRANSCRIPT_RETAINED_EVENTS)
|
|
120
|
-
break;
|
|
121
|
-
const eventBytes = transcriptEventRetainedBytes(event);
|
|
122
|
-
if (retainedBytes + eventBytes > MAX_TRANSCRIPT_RETAINED_BYTES)
|
|
123
|
-
break;
|
|
124
|
-
retained.push(event);
|
|
125
|
-
retainedBytes += eventBytes;
|
|
126
|
-
}
|
|
127
|
-
const retainedEvents = new Set(retained);
|
|
128
|
-
for (const [path, cached] of projection.files) {
|
|
129
|
-
const events = cached.events.filter((event) => retainedEvents.has(event));
|
|
130
|
-
const projectionTruncated = events.length !== cached.events.length;
|
|
131
|
-
if (projectionTruncated || cached.projectionTruncated === true) {
|
|
132
|
-
projection.files.set(path, { ...cached, events, projectionTruncated });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return retained.reverse();
|
|
136
|
-
}
|
|
137
|
-
function transcriptEventRetainedBytes(event) {
|
|
138
|
-
return 256 + Buffer.byteLength([
|
|
139
|
-
event.eventKey,
|
|
140
|
-
event.timestamp,
|
|
141
|
-
event.projectId ?? "",
|
|
142
|
-
event.projectLabel ?? "",
|
|
143
|
-
event.content,
|
|
144
|
-
].join("\u0000"));
|
|
145
|
-
}
|
|
146
|
-
function sameTranscriptEvents(current, cached) {
|
|
147
|
-
return cached !== undefined
|
|
148
|
-
&& current.length === cached.length
|
|
149
|
-
&& current.every((event, index) => {
|
|
150
|
-
const previous = cached[index];
|
|
151
|
-
return previous !== undefined
|
|
152
|
-
&& event.eventKey === previous.eventKey
|
|
153
|
-
&& event.timestamp === previous.timestamp
|
|
154
|
-
&& event.projectId === previous.projectId
|
|
155
|
-
&& event.projectLabel === previous.projectLabel
|
|
156
|
-
&& event.content === previous.content
|
|
157
|
-
&& event.ok === previous.ok
|
|
158
|
-
&& event.turnOutcome === previous.turnOutcome;
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async function discoverTranscriptFiles(projectsRoot) {
|
|
162
|
-
let projectDirectories;
|
|
163
|
-
try {
|
|
164
|
-
projectDirectories = await readdir(projectsRoot, { withFileTypes: true });
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
return [];
|
|
168
|
-
}
|
|
169
|
-
const candidates = [];
|
|
170
|
-
for (const directory of projectDirectories) {
|
|
171
|
-
if (!directory.isDirectory())
|
|
172
|
-
continue;
|
|
173
|
-
const directoryPath = join(projectsRoot, directory.name);
|
|
174
|
-
let entries;
|
|
175
|
-
try {
|
|
176
|
-
entries = await readdir(directoryPath, { withFileTypes: true });
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
for (const entry of entries) {
|
|
182
|
-
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
183
|
-
continue;
|
|
184
|
-
const sessionId = safeIdentifier(basename(entry.name, ".jsonl"));
|
|
185
|
-
if (sessionId)
|
|
186
|
-
candidates.push({ path: join(directoryPath, entry.name), sessionId });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
const files = [];
|
|
190
|
-
for (const candidate of candidates) {
|
|
191
|
-
try {
|
|
192
|
-
const metadata = await stat(candidate.path);
|
|
193
|
-
if (!metadata.isFile())
|
|
194
|
-
continue;
|
|
195
|
-
files.push({
|
|
196
|
-
...candidate,
|
|
197
|
-
size: metadata.size,
|
|
198
|
-
modifiedAtMs: metadata.mtimeMs,
|
|
199
|
-
changedAtMs: metadata.ctimeMs,
|
|
200
|
-
inode: metadata.ino,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
// A live Claude session may rotate a file between directory listing and stat.
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return files
|
|
208
|
-
.sort((left, right) => right.modifiedAtMs - left.modifiedAtMs || left.path.localeCompare(right.path))
|
|
209
|
-
.slice(0, MAX_TRANSCRIPT_FILES);
|
|
210
|
-
}
|
|
211
|
-
function projectTranscriptChunk(buffer, resolveProject, initialState, skipLeadingPartial) {
|
|
212
|
-
const events = [];
|
|
213
|
-
const recordTurns = new Map();
|
|
214
|
-
const recordParents = new Map();
|
|
215
|
-
const terminalRecords = new Map();
|
|
216
|
-
const ambiguousTurns = new Set();
|
|
217
|
-
const partialAnswers = new Map();
|
|
218
|
-
let state = initialState;
|
|
219
|
-
let lineStart = 0;
|
|
220
|
-
if (skipLeadingPartial) {
|
|
221
|
-
const newline = buffer.indexOf(0x0a);
|
|
222
|
-
if (newline < 0)
|
|
223
|
-
return { consumedByteLength: buffer.byteLength, events, state };
|
|
224
|
-
lineStart = newline + 1;
|
|
225
|
-
}
|
|
226
|
-
const completeChunkStart = lineStart;
|
|
227
|
-
let consumedByteLength = lineStart;
|
|
228
|
-
while (lineStart < buffer.byteLength) {
|
|
229
|
-
const newline = buffer.indexOf(0x0a, lineStart);
|
|
230
|
-
if (newline < 0)
|
|
231
|
-
break;
|
|
232
|
-
const lineEnd = newline > lineStart && buffer[newline - 1] === 0x0d ? newline - 1 : newline;
|
|
233
|
-
const raw = parseRecord(buffer.subarray(lineStart, lineEnd).toString("utf8"));
|
|
234
|
-
consumedByteLength = newline + 1;
|
|
235
|
-
lineStart = newline + 1;
|
|
236
|
-
if (!raw)
|
|
237
|
-
continue;
|
|
238
|
-
const type = stringValue(raw.type);
|
|
239
|
-
const message = record(raw.message);
|
|
240
|
-
const recordId = safeIdentifier(raw.uuid);
|
|
241
|
-
const messageId = safeIdentifier(message?.id);
|
|
242
|
-
const parentId = safeIdentifier(raw.parentUuid ?? raw.parent_uuid);
|
|
243
|
-
if (parentId) {
|
|
244
|
-
if (recordId)
|
|
245
|
-
recordParents.set(recordId, parentId);
|
|
246
|
-
if (messageId)
|
|
247
|
-
recordParents.set(messageId, parentId);
|
|
248
|
-
}
|
|
249
|
-
const inheritedLineage = parentId ? recordTurns.get(parentId) : undefined;
|
|
250
|
-
const declaredSessionId = stringValue(raw.sessionId);
|
|
251
|
-
const sessionId = declaredSessionId
|
|
252
|
-
? safeIdentifier(declaredSessionId)
|
|
253
|
-
: state.activeSessionId;
|
|
254
|
-
const cwd = stringValue(raw.cwd);
|
|
255
|
-
const project = cwd ? resolveProject(cwd) : state.activeProject;
|
|
256
|
-
const compatibleInheritedLineage = inheritedLineage
|
|
257
|
-
&& transcriptLineageMatchesRecord(inheritedLineage, declaredSessionId, sessionId, cwd, project)
|
|
258
|
-
? inheritedLineage
|
|
259
|
-
: undefined;
|
|
260
|
-
if (isHiddenRecord(raw)) {
|
|
261
|
-
if (raw.isSidechain !== true && compatibleInheritedLineage) {
|
|
262
|
-
if (recordId)
|
|
263
|
-
recordTurns.set(recordId, compatibleInheritedLineage);
|
|
264
|
-
if (messageId)
|
|
265
|
-
recordTurns.set(messageId, compatibleInheritedLineage);
|
|
266
|
-
}
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
if (type !== "user" && type !== "assistant")
|
|
270
|
-
continue;
|
|
271
|
-
const timestamp = safeTimestamp(raw.timestamp);
|
|
272
|
-
if (!sessionId || !timestamp)
|
|
273
|
-
continue;
|
|
274
|
-
state = {
|
|
275
|
-
...state,
|
|
276
|
-
activeSessionId: sessionId,
|
|
277
|
-
...(project ? { activeProject: project } : {}),
|
|
278
|
-
};
|
|
279
|
-
if (type === "user") {
|
|
280
|
-
const interruptedMessageId = safeIdentifier(raw.interruptedMessageId ?? raw.interrupted_message_id);
|
|
281
|
-
if (interruptedMessageId) {
|
|
282
|
-
const interruptedCandidate = recordTurns.get(interruptedMessageId) ?? inheritedLineage;
|
|
283
|
-
const interruptedLineage = interruptedCandidate
|
|
284
|
-
&& transcriptLineageMatchesRecord(interruptedCandidate, declaredSessionId, sessionId, cwd, project)
|
|
285
|
-
? interruptedCandidate
|
|
286
|
-
: undefined;
|
|
287
|
-
if (!interruptedLineage)
|
|
288
|
-
continue;
|
|
289
|
-
if (recordId)
|
|
290
|
-
recordTurns.set(recordId, interruptedLineage);
|
|
291
|
-
if (messageId)
|
|
292
|
-
recordTurns.set(messageId, interruptedLineage);
|
|
293
|
-
const interruptedTurnId = interruptedLineage.turnId;
|
|
294
|
-
if (ambiguousTurns.has(interruptedTurnId))
|
|
295
|
-
continue;
|
|
296
|
-
const terminalRecord = terminalRecords.get(interruptedTurnId);
|
|
297
|
-
if (terminalRecord) {
|
|
298
|
-
ambiguousTurns.add(interruptedTurnId);
|
|
299
|
-
removeTurnEndEvent(events, interruptedLineage.sessionId, interruptedTurnId);
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
terminalRecords.set(interruptedTurnId, recordId || interruptedMessageId);
|
|
303
|
-
events.push(turnEndEvent({
|
|
304
|
-
sessionId: interruptedLineage.sessionId,
|
|
305
|
-
turnId: interruptedTurnId,
|
|
306
|
-
timestamp,
|
|
307
|
-
answer: partialAnswers.get(interruptedTurnId) ?? "",
|
|
308
|
-
interrupted: true,
|
|
309
|
-
error: "Claude Code turn was interrupted.",
|
|
310
|
-
project: interruptedLineage.project,
|
|
311
|
-
}));
|
|
312
|
-
if (state.activeTurnId === interruptedTurnId
|
|
313
|
-
&& state.activeSessionId === interruptedLineage.sessionId) {
|
|
314
|
-
state = { ...state, activeTurnId: "", activeTurnLineage: undefined };
|
|
315
|
-
}
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (!isInteractiveUserRecord(raw, message) || hasOnlyToolResults(message?.content)) {
|
|
319
|
-
if (recordId && compatibleInheritedLineage)
|
|
320
|
-
recordTurns.set(recordId, compatibleInheritedLineage);
|
|
321
|
-
if (messageId && compatibleInheritedLineage)
|
|
322
|
-
recordTurns.set(messageId, compatibleInheritedLineage);
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const prompt = visibleText(message?.content);
|
|
326
|
-
const turnId = safeIdentifier(raw.promptId ?? raw.prompt_id)
|
|
327
|
-
|| safeIdentifier(raw.uuid);
|
|
328
|
-
if (!prompt || !turnId)
|
|
329
|
-
continue;
|
|
330
|
-
const lineage = {
|
|
331
|
-
turnId,
|
|
332
|
-
sessionId,
|
|
333
|
-
...(project ? { project } : {}),
|
|
334
|
-
};
|
|
335
|
-
state = { ...state, activeTurnId: turnId, activeTurnLineage: lineage };
|
|
336
|
-
if (recordId)
|
|
337
|
-
recordTurns.set(recordId, lineage);
|
|
338
|
-
if (messageId)
|
|
339
|
-
recordTurns.set(messageId, lineage);
|
|
340
|
-
events.push(turnStartEvent({ sessionId, turnId, timestamp, prompt, project }));
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
const assistantLineage = parentId
|
|
344
|
-
? compatibleInheritedLineage
|
|
345
|
-
: state.activeTurnLineage
|
|
346
|
-
&& transcriptLineageMatchesRecord(state.activeTurnLineage, declaredSessionId, sessionId, cwd, project)
|
|
347
|
-
? state.activeTurnLineage
|
|
348
|
-
: undefined;
|
|
349
|
-
if (!assistantLineage)
|
|
350
|
-
continue;
|
|
351
|
-
const assistantTurnId = assistantLineage.turnId;
|
|
352
|
-
if (ambiguousTurns.has(assistantTurnId))
|
|
353
|
-
continue;
|
|
354
|
-
if (recordId)
|
|
355
|
-
recordTurns.set(recordId, assistantLineage);
|
|
356
|
-
if (messageId)
|
|
357
|
-
recordTurns.set(messageId, assistantLineage);
|
|
358
|
-
const answer = visibleText(message?.content);
|
|
359
|
-
if (answer)
|
|
360
|
-
partialAnswers.set(assistantTurnId, answer);
|
|
361
|
-
if (!isTerminalAssistantRecord(raw, message))
|
|
362
|
-
continue;
|
|
363
|
-
const terminalRecord = recordId || `${parentId}\u0000${timestamp}`;
|
|
364
|
-
const previousTerminalRecord = terminalRecords.get(assistantTurnId);
|
|
365
|
-
if (previousTerminalRecord && previousTerminalRecord !== terminalRecord) {
|
|
366
|
-
if (!recordId || !recordDescendsFrom(recordId, previousTerminalRecord, recordParents)) {
|
|
367
|
-
ambiguousTurns.add(assistantTurnId);
|
|
368
|
-
removeTurnEndEvent(events, assistantLineage.sessionId, assistantTurnId);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
removeTurnEndEvent(events, assistantLineage.sessionId, assistantTurnId);
|
|
372
|
-
}
|
|
373
|
-
if (previousTerminalRecord === terminalRecord)
|
|
374
|
-
continue;
|
|
375
|
-
terminalRecords.set(assistantTurnId, terminalRecord);
|
|
376
|
-
const reachedTokenLimit = stringValue(message?.stop_reason) === "max_tokens";
|
|
377
|
-
const interrupted = raw.isApiErrorMessage === true || Boolean(raw.error) || reachedTokenLimit;
|
|
378
|
-
events.push(turnEndEvent({
|
|
379
|
-
sessionId: assistantLineage.sessionId,
|
|
380
|
-
turnId: assistantTurnId,
|
|
381
|
-
timestamp,
|
|
382
|
-
answer,
|
|
383
|
-
interrupted,
|
|
384
|
-
...(interrupted ? {
|
|
385
|
-
error: reachedTokenLimit
|
|
386
|
-
? "Claude Code response reached its token limit."
|
|
387
|
-
: "Claude Code recorded an API error.",
|
|
388
|
-
} : {}),
|
|
389
|
-
project: assistantLineage.project,
|
|
390
|
-
}));
|
|
391
|
-
}
|
|
392
|
-
const activityEvents = projectTranscriptMemoryActivities(buffer.subarray(completeChunkStart, consumedByteLength).toString("utf8"), initialState.activeSessionId, events, terminalRecords);
|
|
393
|
-
return {
|
|
394
|
-
consumedByteLength,
|
|
395
|
-
events: mergeTranscriptEvents([], [...events, ...activityEvents]),
|
|
396
|
-
state,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function projectTranscriptMemoryActivities(transcript, sessionId, lifecycleEvents, acceptedTerminalRecords) {
|
|
400
|
-
const finalizedTurnScopes = new Set(lifecycleEvents
|
|
401
|
-
.filter((event) => event.type === "turn_end" && event.sessionId === sessionId && event.turnId)
|
|
402
|
-
.map((event) => JSON.stringify([event.sessionId, event.turnId, event.projectId ?? null])));
|
|
403
|
-
const starts = new Map(lifecycleEvents
|
|
404
|
-
.filter((event) => event.type === "turn_start" && event.sessionId === sessionId && event.turnId)
|
|
405
|
-
.map((event) => [event.turnId, event]));
|
|
406
|
-
return claudeTranscriptMemoryActivitiesFromJsonLines(transcript, {
|
|
407
|
-
sessionId,
|
|
408
|
-
includeAuthority: true,
|
|
409
|
-
})
|
|
410
|
-
.flatMap((activity) => {
|
|
411
|
-
const turnId = safeIdentifier(activity.promptId);
|
|
412
|
-
const toolUseId = safeIdentifier(activity.toolUseId);
|
|
413
|
-
const terminalRecordId = safeIdentifier(activity.terminalRecordId);
|
|
414
|
-
const start = turnId ? starts.get(turnId) : undefined;
|
|
415
|
-
if (!start
|
|
416
|
-
|| !turnId
|
|
417
|
-
|| !toolUseId
|
|
418
|
-
|| !terminalRecordId
|
|
419
|
-
|| acceptedTerminalRecords.get(turnId) !== terminalRecordId
|
|
420
|
-
|| activity.ok === undefined) {
|
|
421
|
-
return [];
|
|
422
|
-
}
|
|
423
|
-
const turnScope = JSON.stringify([sessionId, turnId, start.projectId ?? null]);
|
|
424
|
-
if (!finalizedTurnScopes.has(turnScope))
|
|
425
|
-
return [];
|
|
426
|
-
const timestamp = safeTimestamp(activity.timestamp)
|
|
427
|
-
|| syntheticActivityTimestamp(start.timestamp, activity.index, activity.occurrence);
|
|
428
|
-
if (!timestamp)
|
|
429
|
-
return [];
|
|
430
|
-
const identity = createHash("sha256")
|
|
431
|
-
.update(JSON.stringify([
|
|
432
|
-
"claude-native-memory-activity-v1",
|
|
433
|
-
sessionId,
|
|
434
|
-
turnId,
|
|
435
|
-
toolUseId,
|
|
436
|
-
activity.occurrence,
|
|
437
|
-
activity.type,
|
|
438
|
-
]))
|
|
439
|
-
.digest("hex")
|
|
440
|
-
.slice(0, 32);
|
|
441
|
-
const id = `claude-local:${sessionId}:${activity.type}:${identity}`;
|
|
442
|
-
const itemCount = activity.type === "memory_cli_search"
|
|
443
|
-
&& activity.ok
|
|
444
|
-
&& Number.isSafeInteger(activity.itemCount)
|
|
445
|
-
&& (activity.itemCount ?? -1) >= 0
|
|
446
|
-
&& (activity.itemCount ?? 101) <= 100
|
|
447
|
-
? activity.itemCount
|
|
448
|
-
: undefined;
|
|
449
|
-
return [{
|
|
450
|
-
id,
|
|
451
|
-
eventKey: memoryViewerEventKey({ id, client: "claude", sessionId }),
|
|
452
|
-
client: "claude",
|
|
453
|
-
type: activity.type,
|
|
454
|
-
timestamp,
|
|
455
|
-
source: "memory_cli",
|
|
456
|
-
operation: activity.type === "memory_cli_add" ? "writeback" : "query",
|
|
457
|
-
ok: activity.ok,
|
|
458
|
-
...(start.projectId ? { projectId: start.projectId } : {}),
|
|
459
|
-
...(start.projectLabel ? { projectLabel: start.projectLabel } : {}),
|
|
460
|
-
...(start.project ? { project: start.project } : {}),
|
|
461
|
-
sessionId,
|
|
462
|
-
turnId,
|
|
463
|
-
content: activity.type === "memory_cli_add"
|
|
464
|
-
? "Claude Code invoked memory add."
|
|
465
|
-
: "Claude Code invoked memory search.",
|
|
466
|
-
...(itemCount === undefined ? {} : { itemCount }),
|
|
467
|
-
...(activity.ok === false ? { error: "Claude Code recorded a failed memory command." } : {}),
|
|
468
|
-
}];
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
function syntheticActivityTimestamp(turnTimestamp, index, occurrence) {
|
|
472
|
-
const parsed = Date.parse(turnTimestamp);
|
|
473
|
-
return Number.isFinite(parsed)
|
|
474
|
-
? new Date(parsed + index * 100 + occurrence).toISOString()
|
|
475
|
-
: "";
|
|
476
|
-
}
|
|
477
|
-
function transcriptLineageMatchesRecord(lineage, declaredSessionId, sessionId, cwd, project) {
|
|
478
|
-
if (declaredSessionId && sessionId !== lineage.sessionId)
|
|
479
|
-
return false;
|
|
480
|
-
if (!cwd)
|
|
481
|
-
return true;
|
|
482
|
-
return project?.projectId === lineage.project?.projectId;
|
|
483
|
-
}
|
|
484
|
-
function turnStartEvent(input) {
|
|
485
|
-
const id = `claude-local:${input.sessionId}:turn_start:${input.turnId}`;
|
|
486
|
-
return {
|
|
487
|
-
id,
|
|
488
|
-
eventKey: memoryViewerEventKey({ id, client: "claude", sessionId: input.sessionId }),
|
|
489
|
-
client: "claude",
|
|
490
|
-
type: "turn_start",
|
|
491
|
-
timestamp: input.timestamp,
|
|
492
|
-
source: "unknown",
|
|
493
|
-
operation: "query",
|
|
494
|
-
ok: true,
|
|
495
|
-
...projectFields(input.project),
|
|
496
|
-
sessionId: input.sessionId,
|
|
497
|
-
turnId: input.turnId,
|
|
498
|
-
content: input.prompt,
|
|
499
|
-
prompt: input.prompt,
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
function turnEndEvent(input) {
|
|
503
|
-
const id = `claude-local:${input.sessionId}:turn_end:${input.turnId}`;
|
|
504
|
-
return {
|
|
505
|
-
id,
|
|
506
|
-
eventKey: memoryViewerEventKey({ id, client: "claude", sessionId: input.sessionId }),
|
|
507
|
-
client: "claude",
|
|
508
|
-
type: "turn_end",
|
|
509
|
-
timestamp: input.timestamp,
|
|
510
|
-
source: "unknown",
|
|
511
|
-
operation: "reply",
|
|
512
|
-
ok: !input.interrupted,
|
|
513
|
-
...projectFields(input.project),
|
|
514
|
-
sessionId: input.sessionId,
|
|
515
|
-
turnId: input.turnId,
|
|
516
|
-
content: input.answer,
|
|
517
|
-
...(input.answer ? { answer: input.answer } : {}),
|
|
518
|
-
turnOutcome: input.interrupted ? "interrupted" : "completed",
|
|
519
|
-
...(input.error ? { error: input.error } : {}),
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
function removeTurnEndEvent(events, sessionId, turnId) {
|
|
523
|
-
const index = events.findIndex((event) => (event.type === "turn_end"
|
|
524
|
-
&& event.sessionId === sessionId
|
|
525
|
-
&& event.turnId === turnId));
|
|
526
|
-
if (index >= 0)
|
|
527
|
-
events.splice(index, 1);
|
|
528
|
-
}
|
|
529
|
-
function recordDescendsFrom(recordId, ancestorId, parents) {
|
|
530
|
-
const visited = new Set();
|
|
531
|
-
let current = recordId;
|
|
532
|
-
while (current && !visited.has(current)) {
|
|
533
|
-
if (current === ancestorId)
|
|
534
|
-
return true;
|
|
535
|
-
visited.add(current);
|
|
536
|
-
current = parents.get(current) ?? "";
|
|
537
|
-
}
|
|
538
|
-
return false;
|
|
539
|
-
}
|
|
540
|
-
function initialParserState(sessionId) {
|
|
541
|
-
return { activeTurnId: "", activeSessionId: sessionId };
|
|
542
|
-
}
|
|
543
|
-
function mergeTranscriptEvents(current, appended) {
|
|
544
|
-
if (appended.length === 0)
|
|
545
|
-
return current;
|
|
546
|
-
const events = new Map(current.map((event) => [event.eventKey, event]));
|
|
547
|
-
for (const event of appended)
|
|
548
|
-
events.set(event.eventKey, event);
|
|
549
|
-
return [...events.values()].sort(compareEvents).slice(-MAX_TRANSCRIPT_EVENTS_PER_FILE);
|
|
550
|
-
}
|
|
551
|
-
async function readFileRange(path, start, end) {
|
|
552
|
-
const handle = await open(path, "r");
|
|
553
|
-
try {
|
|
554
|
-
const buffer = Buffer.allocUnsafe(Math.max(0, end - start));
|
|
555
|
-
let offset = 0;
|
|
556
|
-
while (offset < buffer.byteLength) {
|
|
557
|
-
const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, start + offset);
|
|
558
|
-
if (bytesRead === 0)
|
|
559
|
-
break;
|
|
560
|
-
offset += bytesRead;
|
|
561
|
-
}
|
|
562
|
-
return buffer.subarray(0, offset);
|
|
563
|
-
}
|
|
564
|
-
finally {
|
|
565
|
-
await handle.close();
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
function isHiddenRecord(raw) {
|
|
569
|
-
return raw.isMeta === true
|
|
570
|
-
|| raw.isSidechain === true
|
|
571
|
-
|| raw.isCompactSummary === true
|
|
572
|
-
|| raw.isVisibleInTranscriptOnly === true;
|
|
573
|
-
}
|
|
574
|
-
function isInteractiveUserRecord(raw, message) {
|
|
575
|
-
if (raw.userType !== "external" || message?.role !== "user")
|
|
576
|
-
return false;
|
|
577
|
-
const origin = record(raw.origin);
|
|
578
|
-
return stringValue(origin?.kind).toLowerCase() !== "task-notification"
|
|
579
|
-
&& stringValue(raw.promptSource ?? raw.prompt_source).toLowerCase() !== "system"
|
|
580
|
-
&& !stringValue(raw.interruptedMessageId ?? raw.interrupted_message_id);
|
|
581
|
-
}
|
|
582
|
-
function isTerminalAssistantRecord(raw, message) {
|
|
583
|
-
if (message?.role !== "assistant")
|
|
584
|
-
return false;
|
|
585
|
-
if (raw.isApiErrorMessage === true || raw.error)
|
|
586
|
-
return true;
|
|
587
|
-
const stopReason = stringValue(message.stop_reason);
|
|
588
|
-
return Boolean(stopReason && stopReason !== "tool_use");
|
|
589
|
-
}
|
|
590
|
-
function hasOnlyToolResults(value) {
|
|
591
|
-
return Array.isArray(value)
|
|
592
|
-
&& value.length > 0
|
|
593
|
-
&& value.every((part) => stringValue(record(part)?.type) === "tool_result");
|
|
594
|
-
}
|
|
595
|
-
function visibleText(value) {
|
|
596
|
-
if (typeof value === "string")
|
|
597
|
-
return value.trim();
|
|
598
|
-
if (!Array.isArray(value))
|
|
599
|
-
return "";
|
|
600
|
-
return value
|
|
601
|
-
.map((part) => {
|
|
602
|
-
const block = record(part);
|
|
603
|
-
return stringValue(block?.type) === "text" ? stringValue(block?.text) : "";
|
|
604
|
-
})
|
|
605
|
-
.filter(Boolean)
|
|
606
|
-
.join("\n\n");
|
|
607
|
-
}
|
|
608
|
-
function projectFields(identity) {
|
|
609
|
-
if (!identity)
|
|
610
|
-
return {};
|
|
611
|
-
return {
|
|
612
|
-
projectId: identity.projectId,
|
|
613
|
-
projectLabel: identity.projectLabel,
|
|
614
|
-
project: identity.projectLabel,
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
function safeIdentifier(value) {
|
|
618
|
-
const candidate = stringValue(value);
|
|
619
|
-
if (!candidate || candidate.length > 512 || /[\u0000-\u001f\u007f\\/]/.test(candidate))
|
|
620
|
-
return "";
|
|
621
|
-
return candidate;
|
|
622
|
-
}
|
|
623
|
-
function safeTimestamp(value) {
|
|
624
|
-
const candidate = stringValue(value);
|
|
625
|
-
const parsed = Date.parse(candidate);
|
|
626
|
-
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : "";
|
|
627
|
-
}
|
|
628
|
-
function parseRecord(line) {
|
|
629
|
-
if (!line.trim())
|
|
630
|
-
return undefined;
|
|
631
|
-
try {
|
|
632
|
-
return record(JSON.parse(line));
|
|
633
|
-
}
|
|
634
|
-
catch {
|
|
635
|
-
return undefined;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
function record(value) {
|
|
639
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
640
|
-
? value
|
|
641
|
-
: undefined;
|
|
642
|
-
}
|
|
643
|
-
function stringValue(value) {
|
|
644
|
-
return typeof value === "string" ? value.trim() : "";
|
|
645
|
-
}
|
|
646
|
-
function compareEvents(left, right) {
|
|
647
|
-
return Date.parse(left.timestamp) - Date.parse(right.timestamp) || left.eventKey.localeCompare(right.eventKey);
|
|
648
|
-
}
|