smart-context-mcp 1.4.0 → 1.6.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.
- package/README.md +129 -1
- package/package.json +12 -3
- package/scripts/doctor-state.js +89 -0
- package/scripts/init-clients.js +48 -7
- package/scripts/report-adoption-metrics.js +228 -0
- package/scripts/report-metrics.js +12 -1
- package/scripts/report-workflow-metrics.js +35 -2
- package/scripts/task-runner.js +238 -0
- package/src/analytics/product-quality.js +206 -0
- package/src/client-contract.js +153 -0
- package/src/context-patterns.js +11 -0
- package/src/hooks/claude-hooks.js +48 -49
- package/src/metrics.js +19 -16
- package/src/orchestration/headless-wrapper.js +20 -26
- package/src/repo-safety.js +21 -0
- package/src/server.js +14 -4
- package/src/storage/sqlite.js +241 -12
- package/src/task-runner/policy.js +354 -0
- package/src/task-runner.js +631 -0
- package/src/tools/smart-doctor.js +418 -0
- package/src/tools/smart-metrics.js +54 -30
- package/src/tools/smart-status.js +79 -7
- package/src/tools/smart-summary.js +29 -29
- package/src/tools/smart-turn.js +455 -14
- package/src/utils/mutation-safety.js +85 -0
- package/src/workflow-tracker.js +261 -39
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { HARD_BLOCK_REPO_SAFETY_REASONS } from '../repo-safety.js';
|
|
2
|
+
|
|
3
|
+
export const buildMutationSafety = (
|
|
4
|
+
repoSafety,
|
|
5
|
+
{
|
|
6
|
+
subject = 'Project-local context writes',
|
|
7
|
+
} = {},
|
|
8
|
+
) => {
|
|
9
|
+
if (!repoSafety || repoSafety.available === false) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const blockedBy = HARD_BLOCK_REPO_SAFETY_REASONS
|
|
14
|
+
.filter(([, field]) => repoSafety[field])
|
|
15
|
+
.map(([reason]) => reason);
|
|
16
|
+
const blocked = blockedBy.length > 0;
|
|
17
|
+
const stateDbPath = repoSafety.stateDbPath ?? '.devctx/state.sqlite';
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
blocked,
|
|
21
|
+
blockedBy,
|
|
22
|
+
stateDbPath,
|
|
23
|
+
recommendedActions: repoSafety.recommendedActions ?? [],
|
|
24
|
+
message: blocked
|
|
25
|
+
? `${subject} are blocked until git hygiene is fixed for ${stateDbPath}.`
|
|
26
|
+
: `${subject} are allowed for ${stateDbPath}.`,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const buildDegradedMode = (
|
|
31
|
+
{
|
|
32
|
+
sideEffectsSuppressed = false,
|
|
33
|
+
repoSafety,
|
|
34
|
+
reason = 'repo_safety_blocked',
|
|
35
|
+
mode = 'read_only_snapshot',
|
|
36
|
+
impact = 'Write-side effects are paused while git hygiene is blocked.',
|
|
37
|
+
} = {},
|
|
38
|
+
) => {
|
|
39
|
+
if (!sideEffectsSuppressed) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mutationSafety = buildMutationSafety(repoSafety);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
active: true,
|
|
47
|
+
reason,
|
|
48
|
+
mode,
|
|
49
|
+
impact,
|
|
50
|
+
blockedBy: mutationSafety?.blockedBy ?? [],
|
|
51
|
+
recommendedActions: mutationSafety?.recommendedActions ?? repoSafety?.recommendedActions ?? [],
|
|
52
|
+
message: mutationSafety?.blocked
|
|
53
|
+
? `${impact} Reads continue in degraded mode until repo safety is fixed.`
|
|
54
|
+
: `${impact} Reads continue in degraded mode.`,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const attachSafetyMetadata = (
|
|
59
|
+
result,
|
|
60
|
+
{
|
|
61
|
+
repoSafety = null,
|
|
62
|
+
sideEffectsSuppressed = false,
|
|
63
|
+
subject,
|
|
64
|
+
degradedReason,
|
|
65
|
+
degradedMode,
|
|
66
|
+
degradedImpact,
|
|
67
|
+
} = {},
|
|
68
|
+
) => {
|
|
69
|
+
const mutationSafety = buildMutationSafety(repoSafety, { subject });
|
|
70
|
+
const degraded = buildDegradedMode({
|
|
71
|
+
sideEffectsSuppressed,
|
|
72
|
+
repoSafety,
|
|
73
|
+
reason: degradedReason,
|
|
74
|
+
mode: degradedMode,
|
|
75
|
+
impact: degradedImpact,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...result,
|
|
80
|
+
...(mutationSafety ? { mutationSafety } : {}),
|
|
81
|
+
repoSafety,
|
|
82
|
+
sideEffectsSuppressed: Boolean(sideEffectsSuppressed),
|
|
83
|
+
...(degraded ? { degradedMode: degraded } : {}),
|
|
84
|
+
};
|
|
85
|
+
};
|
package/src/workflow-tracker.js
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getRepoMutationSafety } from './repo-safety.js';
|
|
2
|
+
import { withStateDb, withStateDbSnapshot } from './storage/sqlite.js';
|
|
3
|
+
import { getNetSavedTokens } from './metrics.js';
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_TRACKING_ENABLED_RE = /^(1|true|yes|on)$/i;
|
|
6
|
+
const EMPTY_OBJECT = Object.freeze({});
|
|
7
|
+
|
|
8
|
+
const parseJson = (value, fallback = EMPTY_OBJECT) => {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value || JSON.stringify(fallback));
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const toRoundedPct = (covered, total) => (total > 0 ? Number(((covered / total) * 100).toFixed(2)) : 0);
|
|
17
|
+
|
|
18
|
+
const buildWorkflowNetMetricsCoverage = ({ source, hasNetMetrics }) => ({
|
|
19
|
+
available: hasNetMetrics,
|
|
20
|
+
source: hasNetMetrics ? source : 'none',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const buildSummaryNetMetricsCoverage = ({ coveredWorkflows, totalWorkflows }) => ({
|
|
24
|
+
coveredWorkflows,
|
|
25
|
+
totalWorkflows,
|
|
26
|
+
uncoveredWorkflows: Math.max(0, totalWorkflows - coveredWorkflows),
|
|
27
|
+
coveragePct: toRoundedPct(coveredWorkflows, totalWorkflows),
|
|
28
|
+
complete: totalWorkflows > 0 && coveredWorkflows === totalWorkflows,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const getWorkflowMutationSafety = () => getRepoMutationSafety();
|
|
32
|
+
|
|
33
|
+
const withWorkflowReadDb = (callback) => {
|
|
34
|
+
const safety = getWorkflowMutationSafety();
|
|
35
|
+
const reader = safety.shouldBlock ? withStateDbSnapshot : withStateDb;
|
|
36
|
+
return reader(callback);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const isWorkflowTrackingEnabled = () =>
|
|
40
|
+
WORKFLOW_TRACKING_ENABLED_RE.test(process.env.DEVCTX_WORKFLOW_TRACKING ?? '');
|
|
2
41
|
|
|
3
42
|
const isWorkflowTrackingAvailable = () => {
|
|
4
43
|
try {
|
|
5
|
-
return
|
|
44
|
+
return withWorkflowReadDb((db) => workflowTableExists(db));
|
|
6
45
|
} catch {
|
|
7
46
|
return false;
|
|
8
47
|
}
|
|
@@ -93,6 +132,10 @@ export const getWorkflowBaseline = (workflowType) => {
|
|
|
93
132
|
*/
|
|
94
133
|
export const startWorkflow = (workflowType, sessionId, metadata = {}) => {
|
|
95
134
|
try {
|
|
135
|
+
if (getWorkflowMutationSafety().shouldBlock) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
96
139
|
return withStateDb((db) => {
|
|
97
140
|
if (!workflowTableExists(db)) {
|
|
98
141
|
return null;
|
|
@@ -131,6 +174,10 @@ export const startWorkflow = (workflowType, sessionId, metadata = {}) => {
|
|
|
131
174
|
*/
|
|
132
175
|
export const endWorkflow = (workflowId) => {
|
|
133
176
|
try {
|
|
177
|
+
if (getWorkflowMutationSafety().shouldBlock) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
134
181
|
return withStateDb((db) => {
|
|
135
182
|
if (!workflowTableExists(db)) {
|
|
136
183
|
return null;
|
|
@@ -140,7 +187,7 @@ export const endWorkflow = (workflowId) => {
|
|
|
140
187
|
const workflow = db
|
|
141
188
|
.prepare(
|
|
142
189
|
`
|
|
143
|
-
SELECT workflow_type, session_id, start_time, baseline_tokens
|
|
190
|
+
SELECT workflow_type, session_id, start_time, baseline_tokens, metadata_json
|
|
144
191
|
FROM workflow_metrics
|
|
145
192
|
WHERE workflow_id = ?
|
|
146
193
|
`,
|
|
@@ -160,7 +207,7 @@ export const endWorkflow = (workflowId) => {
|
|
|
160
207
|
const metrics = db
|
|
161
208
|
.prepare(
|
|
162
209
|
`
|
|
163
|
-
SELECT tool, raw_tokens, compressed_tokens, saved_tokens
|
|
210
|
+
SELECT tool, raw_tokens, compressed_tokens, saved_tokens, metadata_json
|
|
164
211
|
FROM metrics_events
|
|
165
212
|
WHERE session_id = ? AND created_at >= ?
|
|
166
213
|
ORDER BY created_at ASC
|
|
@@ -172,11 +219,33 @@ export const endWorkflow = (workflowId) => {
|
|
|
172
219
|
const rawTokens = metrics.reduce((sum, m) => sum + (m.raw_tokens || 0), 0);
|
|
173
220
|
const compressedTokens = metrics.reduce((sum, m) => sum + (m.compressed_tokens || 0), 0);
|
|
174
221
|
const savedTokens = metrics.reduce((sum, m) => sum + (m.saved_tokens || 0), 0);
|
|
222
|
+
const overheadTokens = metrics.reduce((sum, metric) => {
|
|
223
|
+
try {
|
|
224
|
+
const metadata = JSON.parse(metric.metadata_json || '{}');
|
|
225
|
+
return sum + Math.max(0, Number(metadata.overheadTokens ?? 0));
|
|
226
|
+
} catch {
|
|
227
|
+
return sum;
|
|
228
|
+
}
|
|
229
|
+
}, 0);
|
|
230
|
+
const netSavedTokens = getNetSavedTokens(savedTokens, overheadTokens);
|
|
175
231
|
const savingsPct = rawTokens > 0 ? ((savedTokens / rawTokens) * 100).toFixed(2) : 0;
|
|
232
|
+
const netSavingsPct = rawTokens > 0 ? ((netSavedTokens / rawTokens) * 100).toFixed(2) : 0;
|
|
176
233
|
|
|
177
234
|
// Calculate vs baseline
|
|
178
235
|
const baselineTokens = workflow.baseline_tokens || 0;
|
|
179
236
|
const vsBaselinePct = baselineTokens > 0 ? (((baselineTokens - compressedTokens) / baselineTokens) * 100).toFixed(2) : 0;
|
|
237
|
+
const vsBaselineNetPct = baselineTokens > 0 ? (((baselineTokens - (compressedTokens + overheadTokens)) / baselineTokens) * 100).toFixed(2) : 0;
|
|
238
|
+
const persistedMetadata = parseJson(workflow.metadata_json);
|
|
239
|
+
const metadata = {
|
|
240
|
+
...persistedMetadata,
|
|
241
|
+
summary: {
|
|
242
|
+
...(persistedMetadata.summary ?? {}),
|
|
243
|
+
overheadTokens,
|
|
244
|
+
netSavedTokens,
|
|
245
|
+
netSavingsPct: Number(netSavingsPct),
|
|
246
|
+
vsBaselineNetPct: Number(vsBaselineNetPct),
|
|
247
|
+
},
|
|
248
|
+
};
|
|
180
249
|
|
|
181
250
|
// Get unique tools used
|
|
182
251
|
const toolsUsed = [...new Set(metrics.map((m) => m.tool))];
|
|
@@ -192,11 +261,24 @@ export const endWorkflow = (workflowId) => {
|
|
|
192
261
|
compressed_tokens = ?,
|
|
193
262
|
saved_tokens = ?,
|
|
194
263
|
savings_pct = ?,
|
|
195
|
-
vs_baseline_pct =
|
|
264
|
+
vs_baseline_pct = ?,
|
|
265
|
+
metadata_json = ?
|
|
196
266
|
WHERE workflow_id = ?
|
|
197
267
|
`);
|
|
198
268
|
|
|
199
|
-
stmt.run(
|
|
269
|
+
stmt.run(
|
|
270
|
+
now,
|
|
271
|
+
durationMs,
|
|
272
|
+
JSON.stringify(toolsUsed),
|
|
273
|
+
metrics.length,
|
|
274
|
+
rawTokens,
|
|
275
|
+
compressedTokens,
|
|
276
|
+
savedTokens,
|
|
277
|
+
savingsPct,
|
|
278
|
+
vsBaselinePct,
|
|
279
|
+
JSON.stringify(metadata),
|
|
280
|
+
workflowId,
|
|
281
|
+
);
|
|
200
282
|
|
|
201
283
|
return {
|
|
202
284
|
workflowId,
|
|
@@ -207,9 +289,13 @@ export const endWorkflow = (workflowId) => {
|
|
|
207
289
|
rawTokens,
|
|
208
290
|
compressedTokens,
|
|
209
291
|
savedTokens,
|
|
292
|
+
overheadTokens,
|
|
293
|
+
netSavedTokens,
|
|
210
294
|
savingsPct: Number(savingsPct),
|
|
295
|
+
netSavingsPct: Number(netSavingsPct),
|
|
211
296
|
baselineTokens,
|
|
212
297
|
vsBaselinePct: Number(vsBaselinePct),
|
|
298
|
+
vsBaselineNetPct: Number(vsBaselineNetPct),
|
|
213
299
|
};
|
|
214
300
|
});
|
|
215
301
|
} catch {
|
|
@@ -222,7 +308,7 @@ export const endWorkflow = (workflowId) => {
|
|
|
222
308
|
*/
|
|
223
309
|
export const getWorkflowMetrics = (options = {}) => {
|
|
224
310
|
try {
|
|
225
|
-
return
|
|
311
|
+
return withWorkflowReadDb((db) => {
|
|
226
312
|
if (!workflowTableExists(db)) {
|
|
227
313
|
return [];
|
|
228
314
|
}
|
|
@@ -279,8 +365,30 @@ export const getWorkflowMetrics = (options = {}) => {
|
|
|
279
365
|
|
|
280
366
|
return workflows.map((w) => ({
|
|
281
367
|
...w,
|
|
282
|
-
|
|
283
|
-
|
|
368
|
+
...(() => {
|
|
369
|
+
const metadata = parseJson(w.metadata_json);
|
|
370
|
+
const summary = metadata.summary ?? EMPTY_OBJECT;
|
|
371
|
+
const overheadTokens = Number(summary.overheadTokens ?? metadata.overheadTokens);
|
|
372
|
+
const hasOverhead = Number.isFinite(overheadTokens);
|
|
373
|
+
const hasPersistedNetMetrics = Number.isFinite(Number(summary.netSavedTokens));
|
|
374
|
+
const derivedNetSavedTokens = hasOverhead
|
|
375
|
+
? getNetSavedTokens(w.saved_tokens, overheadTokens)
|
|
376
|
+
: undefined;
|
|
377
|
+
const hasNetMetrics = hasPersistedNetMetrics || hasOverhead;
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
toolsUsed: parseJson(w.tools_used_json, []),
|
|
381
|
+
metadata,
|
|
382
|
+
overheadTokens: hasOverhead ? overheadTokens : undefined,
|
|
383
|
+
netSavedTokens: summary.netSavedTokens ?? derivedNetSavedTokens,
|
|
384
|
+
netSavingsPct: summary.netSavingsPct,
|
|
385
|
+
vsBaselineNetPct: summary.vsBaselineNetPct,
|
|
386
|
+
netMetricsCoverage: buildWorkflowNetMetricsCoverage({
|
|
387
|
+
source: hasPersistedNetMetrics ? 'persisted' : 'derived',
|
|
388
|
+
hasNetMetrics,
|
|
389
|
+
}),
|
|
390
|
+
};
|
|
391
|
+
})(),
|
|
284
392
|
}));
|
|
285
393
|
});
|
|
286
394
|
} catch {
|
|
@@ -293,39 +401,95 @@ export const getWorkflowMetrics = (options = {}) => {
|
|
|
293
401
|
*/
|
|
294
402
|
export const getWorkflowSummaryByType = () => {
|
|
295
403
|
try {
|
|
296
|
-
return
|
|
404
|
+
return withWorkflowReadDb((db) => {
|
|
297
405
|
if (!workflowTableExists(db)) {
|
|
298
406
|
return [];
|
|
299
407
|
}
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
408
|
+
const workflows = db
|
|
409
|
+
.prepare(
|
|
410
|
+
`
|
|
411
|
+
SELECT
|
|
412
|
+
workflow_type,
|
|
413
|
+
raw_tokens,
|
|
414
|
+
compressed_tokens,
|
|
415
|
+
saved_tokens,
|
|
416
|
+
savings_pct,
|
|
417
|
+
baseline_tokens,
|
|
418
|
+
vs_baseline_pct,
|
|
419
|
+
duration_ms,
|
|
420
|
+
steps_count,
|
|
421
|
+
metadata_json
|
|
422
|
+
FROM workflow_metrics
|
|
423
|
+
WHERE end_time IS NOT NULL
|
|
424
|
+
`,
|
|
425
|
+
)
|
|
426
|
+
.all();
|
|
427
|
+
|
|
428
|
+
const grouped = new Map();
|
|
429
|
+
|
|
430
|
+
for (const workflow of workflows) {
|
|
431
|
+
const existing = grouped.get(workflow.workflow_type) ?? {
|
|
432
|
+
workflow_type: workflow.workflow_type,
|
|
433
|
+
count: 0,
|
|
434
|
+
total_raw_tokens: 0,
|
|
435
|
+
total_compressed_tokens: 0,
|
|
436
|
+
total_saved_tokens: 0,
|
|
437
|
+
total_overhead_tokens: 0,
|
|
438
|
+
total_net_saved_tokens: 0,
|
|
439
|
+
net_metrics_count: 0,
|
|
440
|
+
total_baseline_tokens: 0,
|
|
441
|
+
savingsPctSum: 0,
|
|
442
|
+
vsBaselinePctSum: 0,
|
|
443
|
+
durationMsSum: 0,
|
|
444
|
+
stepsCountSum: 0,
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const metadata = parseJson(workflow.metadata_json);
|
|
448
|
+
const summary = metadata.summary ?? EMPTY_OBJECT;
|
|
449
|
+
const overheadTokens = Number(summary.overheadTokens);
|
|
450
|
+
const netSavedTokens = Number(summary.netSavedTokens);
|
|
451
|
+
const hasNetMetrics = Number.isFinite(overheadTokens) && Number.isFinite(netSavedTokens);
|
|
452
|
+
|
|
453
|
+
existing.count += 1;
|
|
454
|
+
existing.total_raw_tokens += workflow.raw_tokens || 0;
|
|
455
|
+
existing.total_compressed_tokens += workflow.compressed_tokens || 0;
|
|
456
|
+
existing.total_saved_tokens += workflow.saved_tokens || 0;
|
|
457
|
+
existing.total_baseline_tokens += workflow.baseline_tokens || 0;
|
|
458
|
+
existing.savingsPctSum += workflow.savings_pct || 0;
|
|
459
|
+
existing.vsBaselinePctSum += workflow.vs_baseline_pct || 0;
|
|
460
|
+
existing.durationMsSum += workflow.duration_ms || 0;
|
|
461
|
+
existing.stepsCountSum += workflow.steps_count || 0;
|
|
462
|
+
|
|
463
|
+
if (hasNetMetrics) {
|
|
464
|
+
existing.total_overhead_tokens += overheadTokens;
|
|
465
|
+
existing.total_net_saved_tokens += netSavedTokens;
|
|
466
|
+
existing.net_metrics_count += 1;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
grouped.set(workflow.workflow_type, existing);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return [...grouped.values()]
|
|
473
|
+
.sort((left, right) => right.count - left.count)
|
|
474
|
+
.map((item) => ({
|
|
475
|
+
workflow_type: item.workflow_type,
|
|
476
|
+
count: item.count,
|
|
477
|
+
total_raw_tokens: item.total_raw_tokens,
|
|
478
|
+
total_compressed_tokens: item.total_compressed_tokens,
|
|
479
|
+
total_saved_tokens: item.total_saved_tokens,
|
|
480
|
+
total_overhead_tokens: item.total_overhead_tokens,
|
|
481
|
+
total_net_saved_tokens: item.total_net_saved_tokens,
|
|
482
|
+
net_metrics_count: item.net_metrics_count,
|
|
483
|
+
netMetricsCoverage: buildSummaryNetMetricsCoverage({
|
|
484
|
+
coveredWorkflows: item.net_metrics_count,
|
|
485
|
+
totalWorkflows: item.count,
|
|
486
|
+
}),
|
|
487
|
+
total_baseline_tokens: item.total_baseline_tokens,
|
|
488
|
+
avgSavingsPct: Number((item.savingsPctSum / item.count || 0).toFixed(2)),
|
|
489
|
+
avgVsBaselinePct: Number((item.vsBaselinePctSum / item.count || 0).toFixed(2)),
|
|
490
|
+
avgDurationMs: Math.round(item.durationMsSum / item.count || 0),
|
|
491
|
+
avgStepsCount: Math.round(item.stepsCountSum / item.count || 0),
|
|
492
|
+
}));
|
|
329
493
|
});
|
|
330
494
|
} catch {
|
|
331
495
|
return [];
|
|
@@ -351,10 +515,68 @@ const workflowTableExists = (db) => {
|
|
|
351
515
|
}
|
|
352
516
|
};
|
|
353
517
|
|
|
518
|
+
export const getActiveWorkflowForSession = (sessionId) => {
|
|
519
|
+
try {
|
|
520
|
+
return withWorkflowReadDb((db) => {
|
|
521
|
+
if (!workflowTableExists(db)) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const workflow = db
|
|
526
|
+
.prepare(
|
|
527
|
+
`
|
|
528
|
+
SELECT
|
|
529
|
+
workflow_id,
|
|
530
|
+
workflow_type,
|
|
531
|
+
session_id,
|
|
532
|
+
start_time,
|
|
533
|
+
end_time,
|
|
534
|
+
duration_ms,
|
|
535
|
+
tools_used_json,
|
|
536
|
+
steps_count,
|
|
537
|
+
raw_tokens,
|
|
538
|
+
compressed_tokens,
|
|
539
|
+
saved_tokens,
|
|
540
|
+
savings_pct,
|
|
541
|
+
baseline_tokens,
|
|
542
|
+
vs_baseline_pct,
|
|
543
|
+
metadata_json,
|
|
544
|
+
created_at
|
|
545
|
+
FROM workflow_metrics
|
|
546
|
+
WHERE session_id = ? AND end_time IS NULL
|
|
547
|
+
ORDER BY created_at DESC
|
|
548
|
+
LIMIT 1
|
|
549
|
+
`,
|
|
550
|
+
)
|
|
551
|
+
.get(sessionId);
|
|
552
|
+
|
|
553
|
+
if (!workflow) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
...workflow,
|
|
559
|
+
toolsUsed: parseJson(workflow.tools_used_json, []),
|
|
560
|
+
metadata: parseJson(workflow.metadata_json),
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
354
568
|
/**
|
|
355
569
|
* Auto-detect and track workflow from session
|
|
356
570
|
*/
|
|
357
571
|
export const autoTrackWorkflow = (sessionId, sessionGoal) => {
|
|
572
|
+
if (!sessionId || !isWorkflowTrackingEnabled()) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (getWorkflowMutationSafety().shouldBlock) {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
358
580
|
try {
|
|
359
581
|
return withStateDb((db) => {
|
|
360
582
|
// Check if table exists (migration v5)
|