wendkeep 0.65.0 → 0.66.1
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/CHANGELOG.md +44 -0
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/docs/en/commands/getting-started.md +8 -0
- package/docs/en/commands/memory-migration.md +2 -1
- package/docs/en/commands/memory.md +14 -3
- package/docs/pt-BR/commands/getting-started.md +8 -0
- package/docs/pt-BR/commands/memory-migration.md +2 -1
- package/docs/pt-BR/commands/memory.md +13 -3
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/obsidian-common.mjs +24 -124
- package/hooks/session-identity.mjs +21 -136
- package/hooks/session-stop.mjs +18 -384
- package/hooks/token-usage.mjs +12 -56
- package/package.json +13 -4
- package/packages/cli/src/index.mjs +1 -1
- package/packages/integrations/package.json +2 -1
- package/packages/integrations/src/hook-envelope.mjs +103 -0
- package/packages/integrations/src/host-hooks.mjs +80 -0
- package/packages/integrations/src/index.mjs +6 -0
- package/packages/integrations/src/prompt-content.mjs +20 -0
- package/packages/integrations/src/session-identity.mjs +171 -0
- package/packages/integrations/src/transcript-usage.mjs +53 -0
- package/packages/integrations/src/transcripts.mjs +423 -0
- package/packages/vault/src/memory-schema.mjs +25 -0
- package/packages/vault/src/memory-store.mjs +22 -2
- package/src/memory.mjs +177 -17
- package/src/taxonomy.mjs +24 -78
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { isBootstrapPrompt, redactSecrets } from './prompt-content.mjs';
|
|
2
|
+
import {
|
|
3
|
+
addUsage,
|
|
4
|
+
emptyTokenUsage,
|
|
5
|
+
normalizeClaudeUsage,
|
|
6
|
+
normalizeCodexUsage,
|
|
7
|
+
} from './transcript-usage.mjs';
|
|
8
|
+
|
|
9
|
+
function extractContentText(content) {
|
|
10
|
+
if (typeof content === 'string') return content;
|
|
11
|
+
if (!Array.isArray(content)) return '';
|
|
12
|
+
return content
|
|
13
|
+
.map((item) => item?.text || item?.input_text || item?.output_text || '')
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join('\n')
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SYNTHETIC_EVENT_TAG = /^<\/?(?:task-notification|system-reminder|local-command-stdout|local-command-stderr|command-message|command-name|command-args|user-prompt-submit-hook|ide_selection|ide_opened_file|environment_context)\b/i;
|
|
20
|
+
|
|
21
|
+
function shouldIgnoreUserText(text) {
|
|
22
|
+
const trimmed = String(text || '').trim();
|
|
23
|
+
return SYNTHETIC_EVENT_TAG.test(trimmed)
|
|
24
|
+
|| isBootstrapPrompt(trimmed)
|
|
25
|
+
|| /^Generate a concise( UI)? title/i.test(trimmed)
|
|
26
|
+
|| /^You are a helpful assistant\. You will be presented with a user prompt/i.test(trimmed);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function addUnique(list, value) {
|
|
30
|
+
const clean = redactSecrets(String(value || '').trim());
|
|
31
|
+
if (clean && !list.includes(clean)) list.push(clean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createTurn(turnId = '', timestamp = '') {
|
|
35
|
+
return {
|
|
36
|
+
turnId,
|
|
37
|
+
timestamp,
|
|
38
|
+
userPrompts: [],
|
|
39
|
+
assistantMessages: [],
|
|
40
|
+
tools: [],
|
|
41
|
+
consultedFiles: [],
|
|
42
|
+
changedFiles: [],
|
|
43
|
+
conversation: [],
|
|
44
|
+
usage: emptyTokenUsage(),
|
|
45
|
+
model: '',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createResult(provider) {
|
|
50
|
+
return {
|
|
51
|
+
provider,
|
|
52
|
+
sessionId: '',
|
|
53
|
+
model: '',
|
|
54
|
+
latestTurnId: '',
|
|
55
|
+
latestUserPrompt: '',
|
|
56
|
+
latestAssistantMessage: '',
|
|
57
|
+
userPrompts: [],
|
|
58
|
+
assistantMessages: [],
|
|
59
|
+
tools: [],
|
|
60
|
+
consultedFiles: [],
|
|
61
|
+
changedFiles: [],
|
|
62
|
+
turns: [],
|
|
63
|
+
rawTextForDetection: '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function addConversation(turn, role, value) {
|
|
68
|
+
if (!turn) return;
|
|
69
|
+
const text = redactSecrets(String(value || '').trim());
|
|
70
|
+
if (!text) return;
|
|
71
|
+
if (!turn.conversation.some((item) => item.role === role && item.text === text)) {
|
|
72
|
+
turn.conversation.push({ role, text });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeRoot(value) {
|
|
77
|
+
return String(value || '').replace(/\\+/g, '/').replace(/\/+$/, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pathContext(options = {}) {
|
|
81
|
+
const repoRoot = normalizeRoot(options.repoRoot);
|
|
82
|
+
const vaultRoot = normalizeRoot(options.vaultRoot).toLowerCase();
|
|
83
|
+
const repoLower = repoRoot.toLowerCase();
|
|
84
|
+
const vaultRel = vaultRoot && repoLower && vaultRoot.startsWith(`${repoLower}/`)
|
|
85
|
+
? vaultRoot.slice(repoLower.length + 1)
|
|
86
|
+
: '';
|
|
87
|
+
return { repoRoot, repoLower, vaultRoot, vaultRel };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeExtractedPath(value, context) {
|
|
91
|
+
const cleaned = String(value || '')
|
|
92
|
+
.replace(/\\+/g, '/')
|
|
93
|
+
.replace(/\/+/g, '/')
|
|
94
|
+
.replace(/^(?:\.\/)+/, '')
|
|
95
|
+
.replace(/[:.,;)}\]]+$/, '');
|
|
96
|
+
if (context.repoRoot && cleaned.toLowerCase().startsWith(`${context.repoLower}/`)) {
|
|
97
|
+
return cleaned.slice(context.repoRoot.length + 1);
|
|
98
|
+
}
|
|
99
|
+
return cleaned;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shouldIgnoreExtractedPath(path, context) {
|
|
103
|
+
if (!path) return true;
|
|
104
|
+
const lower = path.toLowerCase();
|
|
105
|
+
if (context.vaultRoot && lower.startsWith(`${context.vaultRoot}/`)) return true;
|
|
106
|
+
if (context.vaultRel && lower.startsWith(`${context.vaultRel}/`)) return true;
|
|
107
|
+
if (lower.includes('/.codex/sessions/')) return true;
|
|
108
|
+
if (lower.includes('/.claude/projects/')) return true;
|
|
109
|
+
if (path.startsWith('../') || path.includes('/../')) return true;
|
|
110
|
+
if (/(?:^|\/)(?:CURRENT_SESSION\.md|SESSION_REGISTRY\.json)$/i.test(path)) return true;
|
|
111
|
+
if (/^[A-Za-z]:\/[A-Za-z]:\//.test(path)) return true;
|
|
112
|
+
if (/^Alves\/\.codex\//i.test(path)) return true;
|
|
113
|
+
if (/\/\.[A-Za-z0-9]+(?::\d+)?$/.test(path)) return true;
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function extractPaths(text, context) {
|
|
118
|
+
const paths = [];
|
|
119
|
+
const addPath = (value) => {
|
|
120
|
+
const path = normalizeExtractedPath(value, context);
|
|
121
|
+
if (!shouldIgnoreExtractedPath(path, context) && !paths.includes(path)) paths.push(path);
|
|
122
|
+
};
|
|
123
|
+
const windowsRegex = /[A-Za-z]:[\\/]+[^"'`\r\n{}()[\],]+\.[A-Za-z0-9]+(?::\d+)?/g;
|
|
124
|
+
const source = String(text || '');
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = windowsRegex.exec(source)) !== null) addPath(match[0]);
|
|
127
|
+
const masked = source.replace(windowsRegex, ' ');
|
|
128
|
+
const regex = /(?:^|[\s"'`(])((?:\/(?:home|mnt)\/|\.{1,2}\/|[A-Za-z0-9_.-]+\/)[A-Za-z0-9_./@+:-]+\.[A-Za-z0-9]+(?::\d+)?)/g;
|
|
129
|
+
while ((match = regex.exec(masked)) !== null) addPath(match[1]);
|
|
130
|
+
return paths.slice(0, 20);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractPatchFiles(text) {
|
|
134
|
+
const files = [];
|
|
135
|
+
const regex = /^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm;
|
|
136
|
+
let match;
|
|
137
|
+
while ((match = regex.exec(text || '')) !== null) addUnique(files, match[1]);
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseToolArguments(args) {
|
|
142
|
+
if (!args) return {};
|
|
143
|
+
if (typeof args === 'object') return args;
|
|
144
|
+
try { return JSON.parse(args); } catch { return { raw: String(args) }; }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toolArgumentText(value) {
|
|
148
|
+
if (value == null) return '';
|
|
149
|
+
if (typeof value === 'string') return value;
|
|
150
|
+
if (Array.isArray(value)) return value.map(toolArgumentText).filter(Boolean).join('\n');
|
|
151
|
+
if (typeof value === 'object') return Object.values(value).map(toolArgumentText).filter(Boolean).join('\n');
|
|
152
|
+
return String(value);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function jsonLines(content) {
|
|
156
|
+
return String(content || '').split('\n').filter(Boolean).map((line) => {
|
|
157
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
158
|
+
}).filter(Boolean);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function parseCodexTranscriptContent(content, options = {}) {
|
|
162
|
+
const result = createResult('codex');
|
|
163
|
+
const eventUserPrompts = [];
|
|
164
|
+
const paths = pathContext(options);
|
|
165
|
+
let currentTurn = null;
|
|
166
|
+
const ensureTurn = (turnId = '', timestamp = '') => {
|
|
167
|
+
const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
|
|
168
|
+
const existing = result.turns.find((turn) => turn.turnId === normalized);
|
|
169
|
+
if (existing) {
|
|
170
|
+
currentTurn = existing;
|
|
171
|
+
return existing;
|
|
172
|
+
}
|
|
173
|
+
currentTurn = createTurn(normalized, timestamp);
|
|
174
|
+
result.turns.push(currentTurn);
|
|
175
|
+
return currentTurn;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const event of jsonLines(content)) {
|
|
179
|
+
if (event.type === 'session_meta') {
|
|
180
|
+
result.sessionId = event.payload?.id || result.sessionId;
|
|
181
|
+
result.model = event.payload?.model || event.payload?.model_provider || result.model;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (event.type === 'event_msg' && event.payload?.type === 'task_started') {
|
|
185
|
+
result.latestTurnId = event.payload.turn_id || result.latestTurnId;
|
|
186
|
+
ensureTurn(result.latestTurnId, event.timestamp);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (event.type === 'turn_context') {
|
|
190
|
+
result.latestTurnId = event.payload?.turn_id || result.latestTurnId;
|
|
191
|
+
result.model = event.payload?.model || result.model;
|
|
192
|
+
ensureTurn(result.latestTurnId, event.timestamp);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (event.type === 'event_msg' && event.payload?.type === 'user_message') {
|
|
196
|
+
const text = event.payload.message || '';
|
|
197
|
+
if (text && !shouldIgnoreUserText(text)) {
|
|
198
|
+
const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
|
|
199
|
+
addUnique(eventUserPrompts, text);
|
|
200
|
+
addUnique(result.userPrompts, text);
|
|
201
|
+
addUnique(turn.userPrompts, text);
|
|
202
|
+
addConversation(turn, 'Usuário', text);
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (event.type === 'event_msg' && event.payload?.type === 'agent_message') {
|
|
207
|
+
const text = event.payload.message || event.payload.text || '';
|
|
208
|
+
if (text) {
|
|
209
|
+
const turn = ensureTurn(event.payload.turn_id || result.latestTurnId, event.timestamp);
|
|
210
|
+
addUnique(result.assistantMessages, text);
|
|
211
|
+
addUnique(turn.assistantMessages, text);
|
|
212
|
+
addConversation(turn, 'Assistente', text);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (event.type === 'event_msg' && event.payload?.type === 'token_count') {
|
|
217
|
+
const raw = event.payload?.info?.last_token_usage;
|
|
218
|
+
if (raw) {
|
|
219
|
+
const turn = currentTurn || ensureTurn(result.latestTurnId, event.timestamp);
|
|
220
|
+
addUsage(turn.usage, normalizeCodexUsage(raw));
|
|
221
|
+
if (event.payload?.info?.model) turn.model = event.payload.info.model;
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (event.type !== 'response_item') continue;
|
|
226
|
+
const payload = event.payload || {};
|
|
227
|
+
if (payload.type === 'message') {
|
|
228
|
+
const text = extractContentText(payload.content);
|
|
229
|
+
if (!text) continue;
|
|
230
|
+
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
231
|
+
if (payload.role === 'user' && !shouldIgnoreUserText(text)) {
|
|
232
|
+
addUnique(result.userPrompts, text);
|
|
233
|
+
addUnique(turn.userPrompts, text);
|
|
234
|
+
addConversation(turn, 'Usuário', text);
|
|
235
|
+
}
|
|
236
|
+
if (payload.role === 'assistant') {
|
|
237
|
+
addUnique(result.assistantMessages, text);
|
|
238
|
+
addUnique(turn.assistantMessages, text);
|
|
239
|
+
addConversation(turn, 'Assistente', text);
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (payload.type === 'function_call') {
|
|
244
|
+
const name = payload.name || 'function_call';
|
|
245
|
+
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
246
|
+
addUnique(result.tools, name);
|
|
247
|
+
addUnique(turn.tools, name);
|
|
248
|
+
const parsed = parseToolArguments(payload.arguments);
|
|
249
|
+
const combined = typeof parsed.raw === 'string' ? parsed.raw : toolArgumentText(parsed);
|
|
250
|
+
for (const path of extractPaths(combined, paths)) {
|
|
251
|
+
addUnique(result.consultedFiles, path);
|
|
252
|
+
addUnique(turn.consultedFiles, path);
|
|
253
|
+
}
|
|
254
|
+
for (const path of extractPatchFiles(combined)) {
|
|
255
|
+
addUnique(result.changedFiles, path);
|
|
256
|
+
addUnique(turn.changedFiles, path);
|
|
257
|
+
}
|
|
258
|
+
if (/apply_patch|edit|write|create/i.test(name)) {
|
|
259
|
+
for (const path of extractPaths(combined, paths)) {
|
|
260
|
+
addUnique(result.changedFiles, path);
|
|
261
|
+
addUnique(turn.changedFiles, path);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (payload.type === 'tool_search_call') {
|
|
266
|
+
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
267
|
+
addUnique(result.tools, 'tool_search');
|
|
268
|
+
addUnique(turn.tools, 'tool_search');
|
|
269
|
+
}
|
|
270
|
+
if (payload.type === 'web_search_call') {
|
|
271
|
+
const turn = ensureTurn(payload.turn_id || event.turn_id || result.latestTurnId, event.timestamp);
|
|
272
|
+
addUnique(result.tools, 'web_search');
|
|
273
|
+
addUnique(turn.tools, 'web_search');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const prompt of eventUserPrompts) addUnique(result.userPrompts, prompt);
|
|
278
|
+
const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
|
|
279
|
+
|| result.turns.at(-1);
|
|
280
|
+
result.latestUserPrompt = latestTurn?.userPrompts.at(-1)
|
|
281
|
+
|| eventUserPrompts.at(-1)
|
|
282
|
+
|| result.userPrompts.at(-1)
|
|
283
|
+
|| '';
|
|
284
|
+
result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1)
|
|
285
|
+
|| result.assistantMessages.at(-1)
|
|
286
|
+
|| '';
|
|
287
|
+
result.rawTextForDetection = redactSecrets([
|
|
288
|
+
...result.userPrompts,
|
|
289
|
+
...result.assistantMessages,
|
|
290
|
+
].join('\n\n'));
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function claudeUserText(content) {
|
|
295
|
+
if (typeof content === 'string') return content.trim();
|
|
296
|
+
if (!Array.isArray(content)) return '';
|
|
297
|
+
return content
|
|
298
|
+
.map((block) => (typeof block === 'string' ? block : (block?.type === 'text' ? block.text || '' : '')))
|
|
299
|
+
.map((text) => String(text || '').trim())
|
|
300
|
+
.filter((text) => text && !text.startsWith('<'))
|
|
301
|
+
.join('\n')
|
|
302
|
+
.trim();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function parseClaudeTranscriptContent(content, options = {}) {
|
|
306
|
+
const result = createResult('claude');
|
|
307
|
+
const paths = pathContext(options);
|
|
308
|
+
let currentTurn = null;
|
|
309
|
+
const ensureTurn = (turnId = '', timestamp = '') => {
|
|
310
|
+
const normalized = turnId || currentTurn?.turnId || `turn-${result.turns.length + 1}`;
|
|
311
|
+
const existing = result.turns.find((turn) => turn.turnId === normalized);
|
|
312
|
+
if (existing) {
|
|
313
|
+
currentTurn = existing;
|
|
314
|
+
return existing;
|
|
315
|
+
}
|
|
316
|
+
currentTurn = createTurn(normalized, timestamp);
|
|
317
|
+
result.turns.push(currentTurn);
|
|
318
|
+
return currentTurn;
|
|
319
|
+
};
|
|
320
|
+
const recordToolFiles = (turn, name, input) => {
|
|
321
|
+
const text = toolArgumentText(input);
|
|
322
|
+
for (const path of extractPaths(text, paths)) {
|
|
323
|
+
addUnique(result.consultedFiles, path);
|
|
324
|
+
addUnique(turn.consultedFiles, path);
|
|
325
|
+
}
|
|
326
|
+
for (const path of extractPatchFiles(text)) {
|
|
327
|
+
addUnique(result.changedFiles, path);
|
|
328
|
+
addUnique(turn.changedFiles, path);
|
|
329
|
+
}
|
|
330
|
+
if (/edit|write|create|apply_patch|notebook/i.test(name)) {
|
|
331
|
+
for (const path of extractPaths(text, paths)) {
|
|
332
|
+
addUnique(result.changedFiles, path);
|
|
333
|
+
addUnique(turn.changedFiles, path);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
for (const event of jsonLines(content)) {
|
|
339
|
+
if (event.isSidechain || event.isMeta) continue;
|
|
340
|
+
if (event.sessionId && !result.sessionId) result.sessionId = event.sessionId;
|
|
341
|
+
if (event.type === 'user') {
|
|
342
|
+
const text = claudeUserText(event.message?.content);
|
|
343
|
+
if (!text || shouldIgnoreUserText(text)) continue;
|
|
344
|
+
const turn = ensureTurn(event.uuid || event.promptId || event.timestamp || '', event.timestamp || '');
|
|
345
|
+
result.latestTurnId = turn.turnId;
|
|
346
|
+
addUnique(result.userPrompts, text);
|
|
347
|
+
addUnique(turn.userPrompts, text);
|
|
348
|
+
addConversation(turn, 'Usuário', text);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (event.type === 'assistant') {
|
|
352
|
+
const turn = currentTurn || ensureTurn(event.uuid || event.timestamp || '', event.timestamp || '');
|
|
353
|
+
result.model = event.message?.model || result.model;
|
|
354
|
+
if (event.message?.model) turn.model = event.message.model;
|
|
355
|
+
if (event.message?.usage) addUsage(turn.usage, normalizeClaudeUsage(event.message.usage));
|
|
356
|
+
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
357
|
+
for (const block of blocks) {
|
|
358
|
+
if (block?.type === 'text' && block.text && block.text.trim()) {
|
|
359
|
+
addUnique(result.assistantMessages, block.text);
|
|
360
|
+
addUnique(turn.assistantMessages, block.text);
|
|
361
|
+
addConversation(turn, 'Assistente', block.text);
|
|
362
|
+
} else if (block?.type === 'tool_use') {
|
|
363
|
+
const name = block.name || 'tool_use';
|
|
364
|
+
addUnique(result.tools, name);
|
|
365
|
+
addUnique(turn.tools, name);
|
|
366
|
+
recordToolFiles(turn, name, block.input);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const latestTurn = result.turns.find((turn) => turn.turnId === result.latestTurnId)
|
|
373
|
+
|| result.turns.at(-1);
|
|
374
|
+
result.latestUserPrompt = latestTurn?.userPrompts.at(-1) || result.userPrompts.at(-1) || '';
|
|
375
|
+
result.latestAssistantMessage = latestTurn?.assistantMessages.at(-1)
|
|
376
|
+
|| result.assistantMessages.at(-1)
|
|
377
|
+
|| '';
|
|
378
|
+
result.rawTextForDetection = redactSecrets([
|
|
379
|
+
...result.userPrompts,
|
|
380
|
+
...result.assistantMessages,
|
|
381
|
+
].join('\n\n'));
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function looksLikeCodexEvent(event) {
|
|
386
|
+
return event.payload !== undefined
|
|
387
|
+
|| event.type === 'session_meta'
|
|
388
|
+
|| event.type === 'response_item'
|
|
389
|
+
|| event.type === 'turn_context'
|
|
390
|
+
|| event.type === 'event_msg';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function looksLikeClaudeEvent(event) {
|
|
394
|
+
return (event.type === 'user' || event.type === 'assistant') && event.message !== undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function parseTranscriptContent(content, options = {}) {
|
|
398
|
+
for (const event of jsonLines(content)) {
|
|
399
|
+
if (looksLikeCodexEvent(event)) return parseCodexTranscriptContent(content, options);
|
|
400
|
+
if (looksLikeClaudeEvent(event)) return parseClaudeTranscriptContent(content, options);
|
|
401
|
+
}
|
|
402
|
+
return parseCodexTranscriptContent(content, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function resolveTurnIdentity(transcript, requestedTurnId = '') {
|
|
406
|
+
const turns = Array.isArray(transcript?.turns) ? transcript.turns : [];
|
|
407
|
+
const requested = String(requestedTurnId || '');
|
|
408
|
+
let index = requested
|
|
409
|
+
? turns.findIndex((turn) => String(turn?.turnId || '') === requested)
|
|
410
|
+
: -1;
|
|
411
|
+
if (requested && index < 0) return null;
|
|
412
|
+
if (index < 0 && transcript?.latestTurnId) {
|
|
413
|
+
index = turns.findIndex((turn) => String(turn?.turnId || '') === String(transcript.latestTurnId));
|
|
414
|
+
}
|
|
415
|
+
if (index < 0) index = turns.length - 1;
|
|
416
|
+
const turn = turns[index];
|
|
417
|
+
if (!turn?.turnId) return null;
|
|
418
|
+
return {
|
|
419
|
+
id: String(turn.turnId),
|
|
420
|
+
order: index + 1,
|
|
421
|
+
observedAt: String(turn.timestamp || ''),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
@@ -108,6 +108,31 @@ export function validateMemoryEvent(event, { projectId } = {}) {
|
|
|
108
108
|
errors.push(`project_id não pertence ao vault esperado (${projectId}).`);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
if (event.candidate_decision !== undefined) {
|
|
112
|
+
const decision = event.candidate_decision;
|
|
113
|
+
if (!decision || typeof decision !== 'object' || Array.isArray(decision)) {
|
|
114
|
+
errors.push('candidate_decision deve ser objeto.');
|
|
115
|
+
} else {
|
|
116
|
+
if (typeof decision.candidate_id !== 'string' || !decision.candidate_id) {
|
|
117
|
+
errors.push('candidate_decision.candidate_id deve ser string não vazia.');
|
|
118
|
+
}
|
|
119
|
+
if (!['promote', 'reject'].includes(decision.action)) {
|
|
120
|
+
errors.push('candidate_decision.action deve ser promote ou reject.');
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(decision.event_ids)
|
|
123
|
+
|| decision.event_ids.some((item) => typeof item !== 'string' || !item)) {
|
|
124
|
+
errors.push('candidate_decision.event_ids deve ser array de strings não vazias.');
|
|
125
|
+
}
|
|
126
|
+
if (decision.action === 'promote' && decision.selected_event_id !== undefined
|
|
127
|
+
&& (typeof decision.selected_event_id !== 'string' || !decision.selected_event_id)) {
|
|
128
|
+
errors.push('candidate_decision.selected_event_id deve ser string não vazia.');
|
|
129
|
+
}
|
|
130
|
+
if (decision.action === 'reject' && decision.selected_event_id !== undefined) {
|
|
131
|
+
errors.push('candidate_decision.selected_event_id não é permitido em reject.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
111
136
|
for (const field of ['value', 'evidence']) sanitizedField(event, field, errors);
|
|
112
137
|
return { ok: errors.length === 0, errors, warnings };
|
|
113
138
|
}
|
|
@@ -461,6 +461,19 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
461
461
|
if (!existing) unique.set(event.event_id, event);
|
|
462
462
|
}
|
|
463
463
|
const events = [...unique.values()].sort(eventOrder);
|
|
464
|
+
const candidateDecisions = new Map();
|
|
465
|
+
for (const item of events) {
|
|
466
|
+
const decision = item.candidate_decision;
|
|
467
|
+
if (!decision) continue;
|
|
468
|
+
const existing = candidateDecisions.get(decision.candidate_id);
|
|
469
|
+
if (existing && canonicalMemoryJson(existing.decision) !== canonicalMemoryJson(decision)) {
|
|
470
|
+
throw new MemoryEventCollision(
|
|
471
|
+
decision.candidate_id,
|
|
472
|
+
`Ledger contains incompatible decisions for candidate ${decision.candidate_id}`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (!existing) candidateDecisions.set(decision.candidate_id, { decision, event: item });
|
|
476
|
+
}
|
|
464
477
|
|
|
465
478
|
const peerGroups = new Map();
|
|
466
479
|
for (const item of events) {
|
|
@@ -488,6 +501,11 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
488
501
|
let revision = 0;
|
|
489
502
|
|
|
490
503
|
for (const item of events) {
|
|
504
|
+
if (item.candidate_decision && item.candidate_decision.action === 'reject') {
|
|
505
|
+
appliedEventIds.push(item.event_id);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
491
509
|
if (protectedValues.has(item.memory_key)) {
|
|
492
510
|
const coreValue = protectedValues.get(item.memory_key);
|
|
493
511
|
const agreesWithCore = item.operation === 'assert'
|
|
@@ -604,7 +622,9 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
604
622
|
const state = sortedObject(stateEntries);
|
|
605
623
|
const recordObject = sortedObject(recordEntries);
|
|
606
624
|
const tombstoneObject = sortedObject(tombstoneEntries);
|
|
607
|
-
|
|
625
|
+
const unresolvedCandidates = candidates
|
|
626
|
+
.filter((item) => !candidateDecisions.has(item.candidate_id));
|
|
627
|
+
unresolvedCandidates.sort((left, right) => left.candidate_id.localeCompare(right.candidate_id));
|
|
608
628
|
superseded.sort((left, right) => left.event_id.localeCompare(right.event_id));
|
|
609
629
|
const activeEvents = Object.entries(recordObject).map(([memoryKey, record]) => ({
|
|
610
630
|
...record.source,
|
|
@@ -618,7 +638,7 @@ export function reduceMemoryEvents(inputEvents = [], { coreInvariants = new Map(
|
|
|
618
638
|
return {
|
|
619
639
|
state,
|
|
620
640
|
records: recordObject,
|
|
621
|
-
candidates,
|
|
641
|
+
candidates: unresolvedCandidates,
|
|
622
642
|
tombstones: tombstoneObject,
|
|
623
643
|
superseded,
|
|
624
644
|
appliedEventIds,
|