@stackmemoryai/stackmemory 1.6.2 → 1.7.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.
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { mkdirSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
function classifyErrorText(text) {
|
|
11
|
+
const lower = text.toLowerCase();
|
|
12
|
+
if (lower.includes("lint") || lower.includes("eslint") || lower.includes("prettier"))
|
|
13
|
+
return "lint_failure";
|
|
14
|
+
if (lower.includes("test") && (lower.includes("fail") || lower.includes("error")))
|
|
15
|
+
return "test_failure";
|
|
16
|
+
if (lower.includes("timeout") || lower.includes("timed out"))
|
|
17
|
+
return "timeout";
|
|
18
|
+
if (lower.includes("conflict") || lower.includes("merge"))
|
|
19
|
+
return "git_conflict";
|
|
20
|
+
if (lower.includes("429") || lower.includes("rate limit"))
|
|
21
|
+
return "rate_limit";
|
|
22
|
+
if (lower.includes("permission") || lower.includes("EACCES") || lower.includes("not found"))
|
|
23
|
+
return "permission_or_missing";
|
|
24
|
+
if (lower.includes("build") && lower.includes("error"))
|
|
25
|
+
return "build_failure";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function getTracesDbPath() {
|
|
29
|
+
return join(homedir(), ".stackmemory", "conductor", "traces.db");
|
|
30
|
+
}
|
|
31
|
+
function openTracesDb(dbPath) {
|
|
32
|
+
const path = dbPath ?? getTracesDbPath();
|
|
33
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
34
|
+
const db = new Database(path);
|
|
35
|
+
db.pragma("journal_mode = WAL");
|
|
36
|
+
db.pragma("foreign_keys = ON");
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS conductor_traces (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
issue_id TEXT NOT NULL,
|
|
41
|
+
session_id TEXT NOT NULL,
|
|
42
|
+
attempt INTEGER NOT NULL,
|
|
43
|
+
turn_number INTEGER NOT NULL,
|
|
44
|
+
timestamp INTEGER NOT NULL,
|
|
45
|
+
phase TEXT,
|
|
46
|
+
tool_names TEXT,
|
|
47
|
+
tool_count INTEGER DEFAULT 0,
|
|
48
|
+
files_modified INTEGER DEFAULT 0,
|
|
49
|
+
input_tokens INTEGER DEFAULT 0,
|
|
50
|
+
output_tokens INTEGER DEFAULT 0,
|
|
51
|
+
cache_creation_tokens INTEGER DEFAULT 0,
|
|
52
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
53
|
+
message_preview TEXT,
|
|
54
|
+
event_json TEXT NOT NULL,
|
|
55
|
+
UNIQUE(session_id, turn_number)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_traces_issue
|
|
59
|
+
ON conductor_traces(issue_id, attempt);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_traces_session
|
|
61
|
+
ON conductor_traces(session_id);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_traces_phase
|
|
63
|
+
ON conductor_traces(phase);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_traces_timestamp
|
|
65
|
+
ON conductor_traces(timestamp DESC);
|
|
66
|
+
`);
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
function withDb(db, fn) {
|
|
70
|
+
const ownDb = db ?? openTracesDb();
|
|
71
|
+
try {
|
|
72
|
+
return fn(ownDb);
|
|
73
|
+
} finally {
|
|
74
|
+
if (!db) ownDb.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class TraceCollector {
|
|
78
|
+
db;
|
|
79
|
+
ownsDb;
|
|
80
|
+
sessionId;
|
|
81
|
+
issueId;
|
|
82
|
+
attempt;
|
|
83
|
+
turnCounter = 0;
|
|
84
|
+
insertStmt;
|
|
85
|
+
constructor(opts) {
|
|
86
|
+
this.issueId = opts.issueId;
|
|
87
|
+
this.attempt = opts.attempt;
|
|
88
|
+
this.sessionId = `${opts.issueId}-${opts.attempt}-${randomUUID().slice(0, 8)}`;
|
|
89
|
+
this.ownsDb = !opts.db;
|
|
90
|
+
this.db = opts.db ?? openTracesDb();
|
|
91
|
+
this.insertStmt = this.db.prepare(`
|
|
92
|
+
INSERT INTO conductor_traces (
|
|
93
|
+
issue_id, session_id, attempt, turn_number, timestamp,
|
|
94
|
+
phase, tool_names, tool_count, files_modified,
|
|
95
|
+
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
|
|
96
|
+
message_preview, event_json
|
|
97
|
+
) VALUES (
|
|
98
|
+
@issue_id, @session_id, @attempt, @turn_number, @timestamp,
|
|
99
|
+
@phase, @tool_names, @tool_count, @files_modified,
|
|
100
|
+
@input_tokens, @output_tokens, @cache_creation_tokens, @cache_read_tokens,
|
|
101
|
+
@message_preview, @event_json
|
|
102
|
+
)
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
get session() {
|
|
106
|
+
return this.sessionId;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Record a turn using pre-extracted data from the orchestrator's stream parser.
|
|
110
|
+
* Avoids re-iterating content blocks — the caller already did that work.
|
|
111
|
+
*/
|
|
112
|
+
recordTurn(turnData, phase, eventJson) {
|
|
113
|
+
this.insertStmt.run({
|
|
114
|
+
issue_id: this.issueId,
|
|
115
|
+
session_id: this.sessionId,
|
|
116
|
+
attempt: this.attempt,
|
|
117
|
+
turn_number: this.turnCounter++,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
phase: phase ?? null,
|
|
120
|
+
tool_names: turnData.toolNames.length > 0 ? JSON.stringify(turnData.toolNames) : null,
|
|
121
|
+
tool_count: turnData.toolCount,
|
|
122
|
+
files_modified: turnData.filesModified,
|
|
123
|
+
input_tokens: turnData.inputTokens,
|
|
124
|
+
output_tokens: turnData.outputTokens,
|
|
125
|
+
cache_creation_tokens: turnData.cacheCreationTokens,
|
|
126
|
+
cache_read_tokens: turnData.cacheReadTokens,
|
|
127
|
+
message_preview: turnData.textPreview,
|
|
128
|
+
event_json: eventJson
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Record a result event (final output) */
|
|
132
|
+
recordResult(event) {
|
|
133
|
+
const resultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
134
|
+
this.insertStmt.run({
|
|
135
|
+
issue_id: this.issueId,
|
|
136
|
+
session_id: this.sessionId,
|
|
137
|
+
attempt: this.attempt,
|
|
138
|
+
turn_number: this.turnCounter++,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
phase: "result",
|
|
141
|
+
tool_names: null,
|
|
142
|
+
tool_count: 0,
|
|
143
|
+
files_modified: 0,
|
|
144
|
+
input_tokens: 0,
|
|
145
|
+
output_tokens: 0,
|
|
146
|
+
cache_creation_tokens: 0,
|
|
147
|
+
cache_read_tokens: 0,
|
|
148
|
+
message_preview: (resultText || "").slice(0, 500),
|
|
149
|
+
event_json: JSON.stringify(event).slice(0, 5e4)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** Close the DB connection only if we own it */
|
|
153
|
+
close() {
|
|
154
|
+
if (!this.ownsDb) return;
|
|
155
|
+
try {
|
|
156
|
+
this.db.close();
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function stringifyEventTruncated(event) {
|
|
162
|
+
return JSON.stringify(event, (_key, value) => {
|
|
163
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
164
|
+
const obj = value;
|
|
165
|
+
if (obj.type === "tool_use" && obj.input) {
|
|
166
|
+
const inputStr = JSON.stringify(obj.input);
|
|
167
|
+
if (inputStr.length > 2e3) {
|
|
168
|
+
return {
|
|
169
|
+
...obj,
|
|
170
|
+
input: { _truncated: true, length: inputStr.length }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (obj.type === "tool_result" && obj.content) {
|
|
175
|
+
const contentStr = typeof obj.content === "string" ? obj.content : JSON.stringify(obj.content);
|
|
176
|
+
if (contentStr.length > 2e3) {
|
|
177
|
+
return { ...obj, content: `[truncated: ${contentStr.length} chars]` };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function listSessions(issueId, db) {
|
|
185
|
+
return withDb(db, (d) => {
|
|
186
|
+
const rows = d.prepare(
|
|
187
|
+
`
|
|
188
|
+
SELECT
|
|
189
|
+
issue_id,
|
|
190
|
+
session_id,
|
|
191
|
+
attempt,
|
|
192
|
+
COUNT(*) as total_turns,
|
|
193
|
+
SUM(tool_count) as total_tool_calls,
|
|
194
|
+
SUM(files_modified) as total_files_modified,
|
|
195
|
+
SUM(input_tokens) as total_input_tokens,
|
|
196
|
+
SUM(output_tokens) as total_output_tokens,
|
|
197
|
+
GROUP_CONCAT(DISTINCT phase) as phases,
|
|
198
|
+
MIN(timestamp) as started_at,
|
|
199
|
+
MAX(timestamp) as ended_at
|
|
200
|
+
FROM conductor_traces
|
|
201
|
+
WHERE issue_id = ?
|
|
202
|
+
GROUP BY session_id
|
|
203
|
+
ORDER BY started_at DESC
|
|
204
|
+
`
|
|
205
|
+
).all(issueId);
|
|
206
|
+
return rows.map((r) => ({
|
|
207
|
+
issue_id: r.issue_id,
|
|
208
|
+
session_id: r.session_id,
|
|
209
|
+
attempt: r.attempt,
|
|
210
|
+
total_turns: r.total_turns,
|
|
211
|
+
total_tool_calls: r.total_tool_calls || 0,
|
|
212
|
+
total_files_modified: r.total_files_modified || 0,
|
|
213
|
+
total_input_tokens: r.total_input_tokens || 0,
|
|
214
|
+
total_output_tokens: r.total_output_tokens || 0,
|
|
215
|
+
phases: (r.phases || "").split(",").filter(Boolean),
|
|
216
|
+
started_at: r.started_at,
|
|
217
|
+
ended_at: r.ended_at,
|
|
218
|
+
duration_ms: r.ended_at - r.started_at
|
|
219
|
+
}));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function getSessionTurns(sessionId, db) {
|
|
223
|
+
return withDb(
|
|
224
|
+
db,
|
|
225
|
+
(d) => d.prepare(
|
|
226
|
+
`
|
|
227
|
+
SELECT * FROM conductor_traces
|
|
228
|
+
WHERE session_id = ?
|
|
229
|
+
ORDER BY turn_number ASC
|
|
230
|
+
`
|
|
231
|
+
).all(sessionId)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function getPhaseBreakdown(sessionId, db) {
|
|
235
|
+
return withDb(
|
|
236
|
+
db,
|
|
237
|
+
(d) => d.prepare(
|
|
238
|
+
`
|
|
239
|
+
SELECT
|
|
240
|
+
phase,
|
|
241
|
+
COUNT(*) as turns,
|
|
242
|
+
SUM(tool_count) as tool_calls,
|
|
243
|
+
SUM(input_tokens) as input_tokens,
|
|
244
|
+
SUM(output_tokens) as output_tokens
|
|
245
|
+
FROM conductor_traces
|
|
246
|
+
WHERE session_id = ? AND phase IS NOT NULL
|
|
247
|
+
GROUP BY phase
|
|
248
|
+
ORDER BY MIN(turn_number) ASC
|
|
249
|
+
`
|
|
250
|
+
).all(sessionId)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
function getToolFrequencies(issueId, db) {
|
|
254
|
+
return withDb(
|
|
255
|
+
db,
|
|
256
|
+
(d) => d.prepare(
|
|
257
|
+
`
|
|
258
|
+
SELECT j.value as tool_name, COUNT(*) as count
|
|
259
|
+
FROM conductor_traces, json_each(tool_names) j
|
|
260
|
+
WHERE issue_id = ? AND tool_names IS NOT NULL
|
|
261
|
+
GROUP BY j.value
|
|
262
|
+
ORDER BY count DESC
|
|
263
|
+
`
|
|
264
|
+
).all(issueId)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
function getFailureTurns(issueId, tailCount = 5, db) {
|
|
268
|
+
return withDb(
|
|
269
|
+
db,
|
|
270
|
+
(d) => d.prepare(
|
|
271
|
+
`
|
|
272
|
+
SELECT t.* FROM conductor_traces t
|
|
273
|
+
INNER JOIN (
|
|
274
|
+
SELECT session_id, MAX(turn_number) as max_turn
|
|
275
|
+
FROM conductor_traces
|
|
276
|
+
WHERE issue_id = ?
|
|
277
|
+
GROUP BY session_id
|
|
278
|
+
) latest ON t.session_id = latest.session_id
|
|
279
|
+
AND t.turn_number > latest.max_turn - ?
|
|
280
|
+
WHERE t.issue_id = ?
|
|
281
|
+
ORDER BY t.session_id, t.turn_number ASC
|
|
282
|
+
`
|
|
283
|
+
).all(issueId, tailCount, issueId)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function getTraceStats(db) {
|
|
287
|
+
return withDb(
|
|
288
|
+
db,
|
|
289
|
+
(d) => d.prepare(
|
|
290
|
+
`
|
|
291
|
+
SELECT
|
|
292
|
+
COUNT(DISTINCT session_id) as total_sessions,
|
|
293
|
+
COUNT(*) as total_turns,
|
|
294
|
+
SUM(input_tokens) as total_input_tokens,
|
|
295
|
+
SUM(output_tokens) as total_output_tokens,
|
|
296
|
+
COUNT(DISTINCT issue_id) as issues_traced
|
|
297
|
+
FROM conductor_traces
|
|
298
|
+
`
|
|
299
|
+
).get()
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
export {
|
|
303
|
+
TraceCollector,
|
|
304
|
+
classifyErrorText,
|
|
305
|
+
getFailureTurns,
|
|
306
|
+
getPhaseBreakdown,
|
|
307
|
+
getSessionTurns,
|
|
308
|
+
getToolFrequencies,
|
|
309
|
+
getTraceStats,
|
|
310
|
+
getTracesDbPath,
|
|
311
|
+
listSessions,
|
|
312
|
+
openTracesDb,
|
|
313
|
+
stringifyEventTruncated
|
|
314
|
+
};
|
|
@@ -22,6 +22,16 @@ import {
|
|
|
22
22
|
getAgentStatusDir,
|
|
23
23
|
getOutcomesLogPath
|
|
24
24
|
} from "./orchestrator.js";
|
|
25
|
+
import {
|
|
26
|
+
openTracesDb,
|
|
27
|
+
listSessions,
|
|
28
|
+
getSessionTurns,
|
|
29
|
+
getPhaseBreakdown,
|
|
30
|
+
getToolFrequencies,
|
|
31
|
+
getFailureTurns,
|
|
32
|
+
getTraceStats,
|
|
33
|
+
classifyErrorText
|
|
34
|
+
} from "./conductor-traces.js";
|
|
25
35
|
function getGlobalStorePath() {
|
|
26
36
|
const dir = join(homedir(), ".stackmemory", "conductor");
|
|
27
37
|
if (!existsSync(dir)) {
|
|
@@ -70,6 +80,17 @@ function fmtTokens(n) {
|
|
|
70
80
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
71
81
|
return String(n);
|
|
72
82
|
}
|
|
83
|
+
function formatTokens(n) {
|
|
84
|
+
return fmtTokens(n) + " tok";
|
|
85
|
+
}
|
|
86
|
+
function formatDuration(ms) {
|
|
87
|
+
const seconds = Math.floor(ms / 1e3);
|
|
88
|
+
if (seconds < 60) return `${seconds}s`;
|
|
89
|
+
const minutes = Math.floor(seconds / 60);
|
|
90
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
91
|
+
const hours = Math.floor(minutes / 60);
|
|
92
|
+
return `${hours}h ${minutes % 60}m`;
|
|
93
|
+
}
|
|
73
94
|
function budgetBar(pct, width = 30) {
|
|
74
95
|
const filled = Math.min(Math.round(pct / 100 * width), width);
|
|
75
96
|
const empty = width - filled;
|
|
@@ -354,6 +375,44 @@ function predictDifficulty(labels, description, priority, outcomes) {
|
|
|
354
375
|
}
|
|
355
376
|
return { difficulty, confidence, reasons };
|
|
356
377
|
}
|
|
378
|
+
function analyzeErrorsFromTraces(failedIssues) {
|
|
379
|
+
const patterns = {};
|
|
380
|
+
const evidence = [];
|
|
381
|
+
let db;
|
|
382
|
+
try {
|
|
383
|
+
db = openTracesDb();
|
|
384
|
+
} catch {
|
|
385
|
+
return { patterns, evidence };
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
for (const issueId of failedIssues) {
|
|
389
|
+
const turns = getFailureTurns(issueId, 3, db);
|
|
390
|
+
if (turns.length === 0) continue;
|
|
391
|
+
for (const turn of turns) {
|
|
392
|
+
const preview = turn.message_preview || "";
|
|
393
|
+
const tools = turn.tool_names ? JSON.parse(turn.tool_names) : [];
|
|
394
|
+
const pattern = classifyErrorText(preview);
|
|
395
|
+
if (pattern) {
|
|
396
|
+
patterns[pattern] = (patterns[pattern] || 0) + 1;
|
|
397
|
+
}
|
|
398
|
+
if (evidence.length < 15) {
|
|
399
|
+
evidence.push({
|
|
400
|
+
issue: issueId,
|
|
401
|
+
phase: turn.phase || "unknown",
|
|
402
|
+
tools,
|
|
403
|
+
preview: preview.slice(0, 300)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} finally {
|
|
409
|
+
db.close();
|
|
410
|
+
}
|
|
411
|
+
if (Object.keys(patterns).length === 0 && failedIssues.length > 0) {
|
|
412
|
+
patterns["unknown"] = failedIssues.length;
|
|
413
|
+
}
|
|
414
|
+
return { patterns, evidence };
|
|
415
|
+
}
|
|
357
416
|
function loadOutcomes() {
|
|
358
417
|
const logPath = getOutcomesLogPath();
|
|
359
418
|
if (!existsSync(logPath)) return [];
|
|
@@ -465,7 +524,8 @@ async function evolvePromptTemplate(input) {
|
|
|
465
524
|
errorPatterns,
|
|
466
525
|
recs,
|
|
467
526
|
outcomes,
|
|
468
|
-
dryRun
|
|
527
|
+
dryRun,
|
|
528
|
+
traceEvidence
|
|
469
529
|
} = input;
|
|
470
530
|
let currentTemplate;
|
|
471
531
|
if (existsSync(templatePath)) {
|
|
@@ -478,11 +538,24 @@ async function evolvePromptTemplate(input) {
|
|
|
478
538
|
}
|
|
479
539
|
const failPhaseSummary = Object.entries(failPhases).sort((a, b) => b[1] - a[1]).map(([phase, count]) => ` - ${phase}: ${count} failures`).join("\n");
|
|
480
540
|
const errorSummary = Object.entries(errorPatterns).sort((a, b) => b[1] - a[1]).map(([pattern, count]) => ` - ${pattern}: ${count} occurrences`).join("\n");
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
541
|
+
let failureEvidenceSection;
|
|
542
|
+
if (traceEvidence && traceEvidence.length > 0) {
|
|
543
|
+
const evidenceLines = traceEvidence.slice(0, 10).map((ev) => {
|
|
544
|
+
const tools = ev.tools.length > 0 ? ev.tools.join(", ") : "none";
|
|
545
|
+
return ` [${ev.issue}, phase: ${ev.phase}, tools: ${tools}]
|
|
546
|
+
${ev.preview.slice(0, 200)}`;
|
|
547
|
+
});
|
|
548
|
+
failureEvidenceSection = `ACTUAL FAILURE EVIDENCE (from conversation traces):
|
|
549
|
+
${evidenceLines.join("\n\n")}`;
|
|
550
|
+
} else {
|
|
551
|
+
const failedOutcomes = outcomes.filter((o) => o.outcome === "failure" && o.errorTail).slice(-5);
|
|
552
|
+
const errorTails = failedOutcomes.map(
|
|
553
|
+
(o) => ` [${o.issue} attempt ${o.attempt}, phase: ${o.phase}]
|
|
484
554
|
${o.errorTail}`
|
|
485
|
-
|
|
555
|
+
).join("\n\n");
|
|
556
|
+
failureEvidenceSection = `SAMPLE ERROR TAILS FROM RECENT FAILURES:
|
|
557
|
+
${errorTails || " (none)"}`;
|
|
558
|
+
}
|
|
486
559
|
const mutationPrompt = `You are optimizing a prompt template for autonomous AI coding agents managed by a conductor system.
|
|
487
560
|
|
|
488
561
|
CURRENT PROMPT TEMPLATE:
|
|
@@ -500,8 +573,7 @@ ${failPhaseSummary || " (none)"}
|
|
|
500
573
|
ERROR PATTERNS:
|
|
501
574
|
${errorSummary || " (none)"}
|
|
502
575
|
|
|
503
|
-
|
|
504
|
-
${errorTails || " (none)"}
|
|
576
|
+
${failureEvidenceSection}
|
|
505
577
|
|
|
506
578
|
RECOMMENDATIONS FROM ANALYSIS:
|
|
507
579
|
${recs.map((r) => `- ${r}`).join("\n")}
|
|
@@ -991,6 +1063,9 @@ function createConductorCommands() {
|
|
|
991
1063
|
"--dry-run",
|
|
992
1064
|
"Show evolved template without writing (use with --evolve)",
|
|
993
1065
|
false
|
|
1066
|
+
).option(
|
|
1067
|
+
"--no-evidence",
|
|
1068
|
+
"Disable trace-based evidence display (on by default when traces.db exists)"
|
|
994
1069
|
).option(
|
|
995
1070
|
"--predict",
|
|
996
1071
|
"Show difficulty predictions alongside actual outcomes",
|
|
@@ -1039,22 +1114,23 @@ function createConductorCommands() {
|
|
|
1039
1114
|
const retrySuccessRate = retries.length > 0 ? Math.round(
|
|
1040
1115
|
retries.filter((o) => o.outcome === "success").length / retries.length * 100
|
|
1041
1116
|
) : 0;
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1117
|
+
const failedIssueIds = [
|
|
1118
|
+
...new Set(
|
|
1119
|
+
outcomes.filter((o) => o.outcome === "failure").map((o) => o.issue)
|
|
1120
|
+
)
|
|
1121
|
+
];
|
|
1122
|
+
const traceAnalysis = analyzeErrorsFromTraces(failedIssueIds);
|
|
1123
|
+
let errorPatterns = traceAnalysis.patterns;
|
|
1124
|
+
let traceEvidence = traceAnalysis.evidence;
|
|
1125
|
+
if (Object.keys(errorPatterns).length === 0 || Object.keys(errorPatterns).length === 1 && errorPatterns["unknown"]) {
|
|
1126
|
+
errorPatterns = {};
|
|
1127
|
+
traceEvidence = [];
|
|
1128
|
+
for (const o of outcomes.filter(
|
|
1129
|
+
(o2) => o2.outcome === "failure" && o2.errorTail
|
|
1130
|
+
)) {
|
|
1131
|
+
const pattern = classifyErrorText(o.errorTail) ?? "unknown";
|
|
1132
|
+
errorPatterns[pattern] = (errorPatterns[pattern] || 0) + 1;
|
|
1133
|
+
}
|
|
1058
1134
|
}
|
|
1059
1135
|
if (options.export) {
|
|
1060
1136
|
const analysis = {
|
|
@@ -1101,16 +1177,42 @@ function createConductorCommands() {
|
|
|
1101
1177
|
}
|
|
1102
1178
|
}
|
|
1103
1179
|
if (Object.keys(errorPatterns).length > 0) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1180
|
+
const sourceLabel = traceEvidence.length > 0 ? "(from traces)" : "(from errorTail)";
|
|
1181
|
+
console.log(
|
|
1182
|
+
`
|
|
1183
|
+
${c.b}Error Patterns${c.r} ${c.d}${sourceLabel}${c.r}`
|
|
1184
|
+
);
|
|
1106
1185
|
const sorted = Object.entries(errorPatterns).sort(
|
|
1107
1186
|
(a, b) => b[1] - a[1]
|
|
1108
1187
|
);
|
|
1109
1188
|
for (const [pattern, count] of sorted) {
|
|
1110
1189
|
console.log(
|
|
1111
|
-
` ${c.red}\u25CF${c.r} ${pattern.padEnd(
|
|
1190
|
+
` ${c.red}\u25CF${c.r} ${pattern.padEnd(20)} ${c.white}${count}${c.r}`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (options.evidence && traceEvidence.length > 0) {
|
|
1195
|
+
console.log(
|
|
1196
|
+
`
|
|
1197
|
+
${c.b}Failure Evidence${c.r} ${c.d}(from traces)${c.r}`
|
|
1198
|
+
);
|
|
1199
|
+
for (const ev of traceEvidence.slice(0, 15)) {
|
|
1200
|
+
const tools = ev.tools.length > 0 ? ev.tools.join(", ") : "-";
|
|
1201
|
+
console.log(
|
|
1202
|
+
` ${c.cyan}${ev.issue}${c.r} [${ev.phase}] tools: ${tools}`
|
|
1112
1203
|
);
|
|
1204
|
+
if (ev.preview) {
|
|
1205
|
+
const lines = ev.preview.split("\n").slice(0, 3);
|
|
1206
|
+
for (const line of lines) {
|
|
1207
|
+
console.log(` ${c.d}${line.slice(0, 100)}${c.r}`);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1113
1210
|
}
|
|
1211
|
+
} else if (options.evidence && traceEvidence.length === 0) {
|
|
1212
|
+
console.log(
|
|
1213
|
+
`
|
|
1214
|
+
${c.d}No trace data available. Run conductor with trace logging enabled to collect evidence.${c.r}`
|
|
1215
|
+
);
|
|
1114
1216
|
}
|
|
1115
1217
|
console.log(`
|
|
1116
1218
|
${c.b}Recommendations${c.r}`);
|
|
@@ -1186,7 +1288,8 @@ function createConductorCommands() {
|
|
|
1186
1288
|
errorPatterns,
|
|
1187
1289
|
recs,
|
|
1188
1290
|
outcomes,
|
|
1189
|
-
dryRun: options.dryRun
|
|
1291
|
+
dryRun: options.dryRun,
|
|
1292
|
+
traceEvidence
|
|
1190
1293
|
});
|
|
1191
1294
|
}
|
|
1192
1295
|
if (options.predict) {
|
|
@@ -1704,6 +1807,204 @@ function createConductorCommands() {
|
|
|
1704
1807
|
process.on("SIGTERM", cleanup);
|
|
1705
1808
|
});
|
|
1706
1809
|
});
|
|
1810
|
+
cmd.command("traces <issue-id>").description("Show conversation traces for an agent run").option("--session <id>", "Show specific session").option("--json", "Output as JSON", false).option("--tools", "Show tool frequency breakdown", false).option("--failures", "Show failure-turn details", false).option("-n, --tail <count>", "Failure tail turns", "5").action(
|
|
1811
|
+
(issueId, options) => {
|
|
1812
|
+
const db = openTracesDb();
|
|
1813
|
+
try {
|
|
1814
|
+
if (options.tools) {
|
|
1815
|
+
const freq = getToolFrequencies(issueId, db);
|
|
1816
|
+
if (freq.length === 0) {
|
|
1817
|
+
console.log(`No traces found for ${issueId}`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
console.log(
|
|
1821
|
+
`
|
|
1822
|
+
${c.b}Tool Frequency${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1823
|
+
`
|
|
1824
|
+
);
|
|
1825
|
+
for (const { tool_name, count } of freq.slice(0, 20)) {
|
|
1826
|
+
const bar = "\u2501".repeat(Math.min(count, 40));
|
|
1827
|
+
console.log(
|
|
1828
|
+
` ${c.d}${tool_name.padEnd(20)}${c.r} ${bar} ${count}`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
console.log("");
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (options.failures) {
|
|
1835
|
+
const turns = getFailureTurns(
|
|
1836
|
+
issueId,
|
|
1837
|
+
parseInt(options.tail, 10),
|
|
1838
|
+
db
|
|
1839
|
+
);
|
|
1840
|
+
if (turns.length === 0) {
|
|
1841
|
+
console.log(`No traces found for ${issueId}`);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
console.log(
|
|
1845
|
+
`
|
|
1846
|
+
${c.b}Failure Turns${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1847
|
+
`
|
|
1848
|
+
);
|
|
1849
|
+
for (const t of turns) {
|
|
1850
|
+
const tools = t.tool_names ? JSON.parse(t.tool_names).join(", ") : "-";
|
|
1851
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1852
|
+
console.log(
|
|
1853
|
+
` ${c.d}[${ts}]${c.r} turn ${t.turn_number} | phase: ${t.phase || "-"} | tools: ${tools}`
|
|
1854
|
+
);
|
|
1855
|
+
if (t.message_preview) {
|
|
1856
|
+
const preview = t.message_preview.slice(0, 200);
|
|
1857
|
+
console.log(` ${c.d}${preview}${c.r}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
console.log("");
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (options.session) {
|
|
1864
|
+
const turns = getSessionTurns(options.session, db);
|
|
1865
|
+
if (turns.length === 0) {
|
|
1866
|
+
console.log(`No turns found for session ${options.session}`);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (options.json) {
|
|
1870
|
+
console.log(JSON.stringify(turns, null, 2));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const phases = getPhaseBreakdown(options.session, db);
|
|
1874
|
+
console.log(
|
|
1875
|
+
`
|
|
1876
|
+
${c.b}Session${c.r} ${c.cyan}${options.session}${c.r}`
|
|
1877
|
+
);
|
|
1878
|
+
console.log(` ${turns.length} turns
|
|
1879
|
+
`);
|
|
1880
|
+
if (phases.length > 0) {
|
|
1881
|
+
console.log(` ${c.b}Phase Breakdown${c.r}`);
|
|
1882
|
+
for (const p of phases) {
|
|
1883
|
+
console.log(
|
|
1884
|
+
` ${(p.phase || "unknown").padEnd(14)} ${String(p.turns).padStart(3)} turns ${String(p.tool_calls).padStart(4)} tools ${formatTokens(p.input_tokens + p.output_tokens)}`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
console.log("");
|
|
1888
|
+
}
|
|
1889
|
+
console.log(` ${c.b}Turn Log${c.r}`);
|
|
1890
|
+
for (const t of turns) {
|
|
1891
|
+
const tools = t.tool_names ? JSON.parse(t.tool_names).join(", ") : "-";
|
|
1892
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1893
|
+
const tokens = t.input_tokens + t.output_tokens;
|
|
1894
|
+
console.log(
|
|
1895
|
+
` ${c.d}${String(t.turn_number).padStart(3)}${c.r} [${ts}] ${(t.phase || "-").padEnd(12)} tools: ${tools.padEnd(30)} ${formatTokens(tokens)}`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
console.log("");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const sessions = listSessions(issueId, db);
|
|
1902
|
+
if (sessions.length === 0) {
|
|
1903
|
+
console.log(`No traces found for ${issueId}`);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (options.json) {
|
|
1907
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
console.log(
|
|
1911
|
+
`
|
|
1912
|
+
${c.b}Trace Sessions${c.r} \u2014 ${c.cyan}${issueId}${c.r}
|
|
1913
|
+
`
|
|
1914
|
+
);
|
|
1915
|
+
for (const s of sessions) {
|
|
1916
|
+
const dur = s.duration_ms > 0 ? formatDuration(s.duration_ms) : "-";
|
|
1917
|
+
const tokens = formatTokens(
|
|
1918
|
+
s.total_input_tokens + s.total_output_tokens
|
|
1919
|
+
);
|
|
1920
|
+
const time = new Date(s.started_at).toISOString().slice(0, 19);
|
|
1921
|
+
console.log(
|
|
1922
|
+
` ${c.d}attempt ${s.attempt}${c.r} ${s.total_turns} turns ${s.total_tool_calls} tools ${tokens} ${dur} ${c.d}${time}${c.r}`
|
|
1923
|
+
);
|
|
1924
|
+
console.log(` ${c.d}session: ${s.session_id}${c.r}`);
|
|
1925
|
+
console.log(` phases: ${s.phases.join(" \u2192 ")}`);
|
|
1926
|
+
console.log("");
|
|
1927
|
+
}
|
|
1928
|
+
} finally {
|
|
1929
|
+
db.close();
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
);
|
|
1933
|
+
cmd.command("replay <session-id>").description("Replay a full agent conversation from traces").option("-n, --turns <count>", "Show only last N turns").option("--json", "Output raw event JSON", false).action((sessionId, options) => {
|
|
1934
|
+
const db = openTracesDb();
|
|
1935
|
+
try {
|
|
1936
|
+
let turns = getSessionTurns(sessionId, db);
|
|
1937
|
+
if (turns.length === 0) {
|
|
1938
|
+
console.error(`No traces found for session ${sessionId}`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (options.turns) {
|
|
1942
|
+
const n = parseInt(options.turns, 10);
|
|
1943
|
+
turns = turns.slice(-n);
|
|
1944
|
+
}
|
|
1945
|
+
if (options.json) {
|
|
1946
|
+
for (const t of turns) {
|
|
1947
|
+
console.log(t.event_json);
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
console.log(
|
|
1952
|
+
`
|
|
1953
|
+
${c.b}Replay${c.r} ${c.cyan}${sessionId}${c.r} \u2014 ${turns.length} turns
|
|
1954
|
+
`
|
|
1955
|
+
);
|
|
1956
|
+
for (const t of turns) {
|
|
1957
|
+
const ts = new Date(t.timestamp).toISOString().slice(11, 19);
|
|
1958
|
+
const tokens = t.input_tokens + t.output_tokens;
|
|
1959
|
+
console.log(
|
|
1960
|
+
`${c.purple}\u2500\u2500 Turn ${t.turn_number} \u2500\u2500${c.r} [${ts}] ${t.phase || ""} ${tokens > 0 ? formatTokens(tokens) : ""}`
|
|
1961
|
+
);
|
|
1962
|
+
if (t.tool_names) {
|
|
1963
|
+
const tools = JSON.parse(t.tool_names);
|
|
1964
|
+
for (const tool of tools) {
|
|
1965
|
+
console.log(` ${c.cyan}\u25B8 ${tool}${c.r}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (t.message_preview) {
|
|
1969
|
+
const lines = t.message_preview.split("\n");
|
|
1970
|
+
for (const line of lines.slice(0, 10)) {
|
|
1971
|
+
console.log(` ${c.d}${line}${c.r}`);
|
|
1972
|
+
}
|
|
1973
|
+
if (lines.length > 10) {
|
|
1974
|
+
console.log(
|
|
1975
|
+
` ${c.d}... (${lines.length - 10} more lines)${c.r}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
console.log("");
|
|
1980
|
+
}
|
|
1981
|
+
} finally {
|
|
1982
|
+
db.close();
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
cmd.command("trace-stats").description("Show aggregate trace statistics").option("--json", "Output as JSON", false).action((options) => {
|
|
1986
|
+
const stats = getTraceStats();
|
|
1987
|
+
if (options.json) {
|
|
1988
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
console.log(`
|
|
1992
|
+
${c.b}Conductor Trace Stats${c.r}
|
|
1993
|
+
`);
|
|
1994
|
+
console.log(` Sessions: ${stats.total_sessions}`);
|
|
1995
|
+
console.log(` Turns: ${stats.total_turns}`);
|
|
1996
|
+
console.log(` Issues: ${stats.issues_traced}`);
|
|
1997
|
+
console.log(
|
|
1998
|
+
` Tokens: ${formatTokens((stats.total_input_tokens || 0) + (stats.total_output_tokens || 0))}`
|
|
1999
|
+
);
|
|
2000
|
+
console.log(
|
|
2001
|
+
` Input: ${formatTokens(stats.total_input_tokens || 0)}`
|
|
2002
|
+
);
|
|
2003
|
+
console.log(
|
|
2004
|
+
` Output: ${formatTokens(stats.total_output_tokens || 0)}`
|
|
2005
|
+
);
|
|
2006
|
+
console.log("");
|
|
2007
|
+
});
|
|
1707
2008
|
return cmd;
|
|
1708
2009
|
}
|
|
1709
2010
|
export {
|
|
@@ -30,6 +30,10 @@ import {
|
|
|
30
30
|
} from "../../core/worktree/preflight.js";
|
|
31
31
|
import { ContextCapture } from "../../core/worktree/capture.js";
|
|
32
32
|
import { extractKeywords } from "../../core/utils/text.js";
|
|
33
|
+
import {
|
|
34
|
+
TraceCollector,
|
|
35
|
+
stringifyEventTruncated
|
|
36
|
+
} from "./conductor-traces.js";
|
|
33
37
|
function getOutcomesLogPath() {
|
|
34
38
|
return join(homedir(), ".stackmemory", "conductor", "outcomes.jsonl");
|
|
35
39
|
}
|
|
@@ -1272,6 +1276,17 @@ class Conductor {
|
|
|
1272
1276
|
run.logStream = logStream;
|
|
1273
1277
|
const tee = new TeeTransform(logStream);
|
|
1274
1278
|
proc.stdout.pipe(tee);
|
|
1279
|
+
let traceCollector;
|
|
1280
|
+
try {
|
|
1281
|
+
traceCollector = new TraceCollector({
|
|
1282
|
+
issueId: issue.identifier,
|
|
1283
|
+
attempt: run.attempt
|
|
1284
|
+
});
|
|
1285
|
+
} catch {
|
|
1286
|
+
logger.warn("Failed to initialize trace collector", {
|
|
1287
|
+
identifier: issue.identifier
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1275
1290
|
this.writeAgentStatus(issue.identifier, run);
|
|
1276
1291
|
let stderr = "";
|
|
1277
1292
|
let lastResultText = "";
|
|
@@ -1303,23 +1318,53 @@ class Conductor {
|
|
|
1303
1318
|
this.trackUsage(issue.identifier, msgUsage);
|
|
1304
1319
|
}
|
|
1305
1320
|
const content = message?.content || [];
|
|
1321
|
+
const turnToolNames = [];
|
|
1322
|
+
let turnFilesModified = 0;
|
|
1323
|
+
const turnTextParts = [];
|
|
1306
1324
|
for (const block of content) {
|
|
1307
1325
|
if (block.type === "tool_use") {
|
|
1308
1326
|
run.toolCalls++;
|
|
1309
|
-
const
|
|
1327
|
+
const name = block.name || "";
|
|
1328
|
+
turnToolNames.push(name);
|
|
1329
|
+
const toolLower = name.toLowerCase();
|
|
1310
1330
|
if (toolLower.includes("edit") || toolLower.includes("write")) {
|
|
1311
1331
|
run.filesModified++;
|
|
1332
|
+
turnFilesModified++;
|
|
1312
1333
|
}
|
|
1313
1334
|
}
|
|
1314
1335
|
if (block.type === "text" && block.text) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1336
|
+
const text = block.text;
|
|
1337
|
+
run.tokensUsed += Math.ceil(text.length / 4);
|
|
1338
|
+
turnTextParts.push(text);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
if (traceCollector) {
|
|
1343
|
+
const turnData = {
|
|
1344
|
+
toolNames: turnToolNames,
|
|
1345
|
+
toolCount: turnToolNames.length,
|
|
1346
|
+
filesModified: turnFilesModified,
|
|
1347
|
+
textPreview: turnTextParts.length > 0 ? turnTextParts.join("\n").slice(0, 500) : null,
|
|
1348
|
+
inputTokens: msgUsage?.input_tokens ?? 0,
|
|
1349
|
+
outputTokens: msgUsage?.output_tokens ?? 0,
|
|
1350
|
+
cacheCreationTokens: msgUsage?.cache_creation_input_tokens ?? 0,
|
|
1351
|
+
cacheReadTokens: msgUsage?.cache_read_input_tokens ?? 0
|
|
1352
|
+
};
|
|
1353
|
+
traceCollector.recordTurn(
|
|
1354
|
+
turnData,
|
|
1355
|
+
phase,
|
|
1356
|
+
stringifyEventTruncated(event)
|
|
1317
1357
|
);
|
|
1318
1358
|
}
|
|
1359
|
+
} catch {
|
|
1319
1360
|
}
|
|
1320
1361
|
}
|
|
1321
1362
|
if (event.type === "result" && event.result) {
|
|
1322
1363
|
lastResultText = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
1364
|
+
try {
|
|
1365
|
+
traceCollector?.recordResult(event);
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1323
1368
|
}
|
|
1324
1369
|
if (run.toolCalls % 5 === 0 || phase) {
|
|
1325
1370
|
this.writeAgentStatus(issue.identifier, run);
|
|
@@ -1337,10 +1382,12 @@ class Conductor {
|
|
|
1337
1382
|
});
|
|
1338
1383
|
proc.on("error", (err) => {
|
|
1339
1384
|
clearTimeout(timer);
|
|
1385
|
+
traceCollector?.close();
|
|
1340
1386
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
1341
1387
|
});
|
|
1342
1388
|
proc.on("close", (code) => {
|
|
1343
1389
|
clearTimeout(timer);
|
|
1390
|
+
traceCollector?.close();
|
|
1344
1391
|
run.process = null;
|
|
1345
1392
|
if (run.logStream && !run.logStream.destroyed) {
|
|
1346
1393
|
run.logStream.end();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|