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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { countTokens } from '../tokenCounter.js';
|
|
2
2
|
import { persistMetrics } from '../metrics.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getRepoMutationSafety, getRepoSafety } from '../repo-safety.js';
|
|
4
|
+
import { attachSafetyMetadata, buildMutationSafety } from '../utils/mutation-safety.js';
|
|
4
5
|
import { recordToolUsage } from '../usage-feedback.js';
|
|
5
6
|
import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
|
|
6
7
|
import { recordDevctxOperation } from '../missed-opportunities.js';
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
SQLITE_SCHEMA_VERSION,
|
|
10
11
|
cleanupLegacyState,
|
|
11
12
|
compactState,
|
|
13
|
+
getStateStorageHealth,
|
|
12
14
|
importLegacyState,
|
|
13
15
|
withStateDb,
|
|
14
16
|
withStateDbSnapshot,
|
|
@@ -105,11 +107,6 @@ const CHECKPOINT_POLICY_BY_EVENT = {
|
|
|
105
107
|
},
|
|
106
108
|
};
|
|
107
109
|
const SUMMARY_WRITE_ACTIONS = new Set(['update', 'append', 'auto_append', 'checkpoint', 'reset', 'compact']);
|
|
108
|
-
const HARD_BLOCK_REPO_SAFETY_REASONS = [
|
|
109
|
-
['tracked', 'isTracked'],
|
|
110
|
-
['staged', 'isStaged'],
|
|
111
|
-
];
|
|
112
|
-
|
|
113
110
|
const getTimestamp = (value, fallback = Date.now()) => {
|
|
114
111
|
const parsed = Date.parse(value ?? '');
|
|
115
112
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -974,22 +971,21 @@ const buildResumeCandidates = (sessions) =>
|
|
|
974
971
|
isStale: session.isStale,
|
|
975
972
|
}));
|
|
976
973
|
|
|
977
|
-
const addRepoSafety = (result, repoSafety = getRepoSafety()) =>
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
974
|
+
const addRepoSafety = (result, repoSafety = getRepoSafety(), sideEffectsSuppressed = false) =>
|
|
975
|
+
attachSafetyMetadata({
|
|
976
|
+
...result,
|
|
977
|
+
storageHealth: getStateStorageHealth(),
|
|
978
|
+
}, {
|
|
979
|
+
repoSafety,
|
|
980
|
+
sideEffectsSuppressed,
|
|
981
|
+
subject: 'Project-local context writes',
|
|
982
|
+
degradedReason: 'repo_safety_blocked',
|
|
983
|
+
degradedMode: 'read_only_snapshot',
|
|
984
|
+
degradedImpact: 'Checkpoint maintenance side effects are paused while git hygiene is blocked.',
|
|
985
|
+
});
|
|
981
986
|
|
|
982
987
|
const getMutationSafetyPolicy = () => {
|
|
983
|
-
|
|
984
|
-
const reasons = HARD_BLOCK_REPO_SAFETY_REASONS
|
|
985
|
-
.filter(([, field]) => repoSafety[field])
|
|
986
|
-
.map(([reason]) => reason);
|
|
987
|
-
|
|
988
|
-
return {
|
|
989
|
-
repoSafety,
|
|
990
|
-
shouldBlock: reasons.length > 0,
|
|
991
|
-
reasons,
|
|
992
|
-
};
|
|
988
|
+
return getRepoMutationSafety();
|
|
993
989
|
};
|
|
994
990
|
|
|
995
991
|
const buildMutationBlockedMessage = (reasons, stateDbPath = '.devctx/state.sqlite') => {
|
|
@@ -1008,15 +1004,20 @@ const buildMutationBlockedMessage = (reasons, stateDbPath = '.devctx/state.sqlit
|
|
|
1008
1004
|
return `Refused to mutate project-local context because ${stateDbPath} failed runtime safety checks.`;
|
|
1009
1005
|
};
|
|
1010
1006
|
|
|
1011
|
-
const buildMutationBlockedResponse = ({ action, sessionId, repoSafety, reasons }) =>
|
|
1012
|
-
|
|
1007
|
+
const buildMutationBlockedResponse = ({ action, sessionId, repoSafety, reasons }) => {
|
|
1008
|
+
const mutationSafety = buildMutationSafety(repoSafety, {
|
|
1009
|
+
subject: 'Project-local context writes',
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
return addRepoSafety({
|
|
1013
1013
|
action,
|
|
1014
1014
|
sessionId: sessionId ?? null,
|
|
1015
1015
|
blocked: true,
|
|
1016
1016
|
mutationBlocked: true,
|
|
1017
|
-
blockedBy: reasons,
|
|
1017
|
+
blockedBy: mutationSafety?.blockedBy ?? reasons,
|
|
1018
1018
|
message: buildMutationBlockedMessage(reasons, repoSafety.stateDbPath ?? '.devctx/state.sqlite'),
|
|
1019
1019
|
}, repoSafety);
|
|
1020
|
+
};
|
|
1020
1021
|
|
|
1021
1022
|
const resolveAutoResumeTarget = (db, { forceRecommended = false, cleanup = true } = {}) => {
|
|
1022
1023
|
const sessions = listSessions(db, { cleanup });
|
|
@@ -1158,7 +1159,7 @@ export const smartSummary = async ({
|
|
|
1158
1159
|
activeSessionId,
|
|
1159
1160
|
totalSessions: sessions.length,
|
|
1160
1161
|
staleSessions: sessions.filter((session) => session.isStale).length,
|
|
1161
|
-
});
|
|
1162
|
+
}, mutationSafety.repoSafety, !allowReadSideEffects);
|
|
1162
1163
|
}, allowReadSideEffects ? undefined : {});
|
|
1163
1164
|
}
|
|
1164
1165
|
|
|
@@ -1194,7 +1195,7 @@ export const smartSummary = async ({
|
|
|
1194
1195
|
candidates: resolution.candidates,
|
|
1195
1196
|
recommendedSessionId: resolution.recommendedSessionId,
|
|
1196
1197
|
message: resolution.message,
|
|
1197
|
-
});
|
|
1198
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1198
1199
|
}
|
|
1199
1200
|
|
|
1200
1201
|
targetSessionId = resolution.sessionId;
|
|
@@ -1207,7 +1208,7 @@ export const smartSummary = async ({
|
|
|
1207
1208
|
sessionId: null,
|
|
1208
1209
|
found: false,
|
|
1209
1210
|
message: 'No active session found. Use action=update to create one.',
|
|
1210
|
-
});
|
|
1211
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1211
1212
|
}
|
|
1212
1213
|
|
|
1213
1214
|
const session = hydrateSession(getSessionRow(db, targetSessionId));
|
|
@@ -1217,7 +1218,7 @@ export const smartSummary = async ({
|
|
|
1217
1218
|
sessionId: targetSessionId,
|
|
1218
1219
|
found: false,
|
|
1219
1220
|
message: 'Session not found.',
|
|
1220
|
-
});
|
|
1221
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1221
1222
|
}
|
|
1222
1223
|
|
|
1223
1224
|
if (resumeMeta?.autoResumed && allowReadSideEffects) {
|
|
@@ -1277,10 +1278,9 @@ export const smartSummary = async ({
|
|
|
1277
1278
|
ambiguous: resumeMeta?.ambiguous ?? false,
|
|
1278
1279
|
recommendedSessionId: resumeMeta?.recommendedSessionId ?? targetSessionId,
|
|
1279
1280
|
...(resumeMeta?.candidates ? { candidates: resumeMeta.candidates } : {}),
|
|
1280
|
-
...(suppressReadSideEffects ? { sideEffectsSuppressed: true } : {}),
|
|
1281
1281
|
schemaVersion: session.schemaVersion ?? 1,
|
|
1282
1282
|
updatedAt: session.updatedAt,
|
|
1283
|
-
});
|
|
1283
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1284
1284
|
}, allowReadSideEffects ? undefined : {});
|
|
1285
1285
|
}
|
|
1286
1286
|
|
package/src/tools/smart-turn.js
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
import { buildIndexIncremental, persistIndex } from '../index.js';
|
|
2
|
+
import { projectRoot } from '../utils/runtime-config.js';
|
|
3
|
+
import {
|
|
4
|
+
autoTrackWorkflow,
|
|
5
|
+
endWorkflow,
|
|
6
|
+
getActiveWorkflowForSession,
|
|
7
|
+
isWorkflowTrackingEnabled,
|
|
8
|
+
} from '../workflow-tracker.js';
|
|
9
|
+
import { persistMetrics } from '../metrics.js';
|
|
10
|
+
import { PRODUCT_QUALITY_ANALYTICS_KIND } from '../analytics/product-quality.js';
|
|
11
|
+
import { attachSafetyMetadata, buildMutationSafety } from '../utils/mutation-safety.js';
|
|
12
|
+
import { smartContext } from './smart-context.js';
|
|
1
13
|
import { smartMetrics } from './smart-metrics.js';
|
|
2
14
|
import { smartSummary } from './smart-summary.js';
|
|
3
15
|
|
|
4
16
|
const DEFAULT_START_MAX_TOKENS = 400;
|
|
5
17
|
const DEFAULT_END_MAX_TOKENS = 500;
|
|
6
18
|
const DEFAULT_END_EVENT = 'milestone';
|
|
19
|
+
const DEFAULT_REFRESH_CONTEXT_MAX_TOKENS = 1400;
|
|
7
20
|
const MAX_PROMPT_PREVIEW = 160;
|
|
21
|
+
const REFRESHED_CONTEXT_FILE_LIMIT = 3;
|
|
22
|
+
const SAFE_CONTINUITY_STATES = new Set(['aligned', 'resume']);
|
|
23
|
+
const WORKFLOW_END_EVENTS = new Set(['milestone', 'task_complete', 'session_end', 'blocker']);
|
|
24
|
+
const INDEX_REFRESH_STATES = new Set(['stale', 'unavailable']);
|
|
8
25
|
const STOP_WORDS = new Set([
|
|
9
26
|
'the', 'and', 'for', 'with', 'that', 'this', 'from', 'into', 'when', 'where',
|
|
10
27
|
'what', 'have', 'will', 'your', 'about', 'there', 'their', 'then', 'than',
|
|
@@ -71,14 +88,24 @@ const summarizeMetrics = (metrics) => {
|
|
|
71
88
|
count: metrics.summary.count,
|
|
72
89
|
savedTokens: metrics.summary.savedTokens,
|
|
73
90
|
savingsPct: metrics.summary.savingsPct,
|
|
91
|
+
netSavedTokens: metrics.summary.netSavedTokens,
|
|
92
|
+
netSavingsPct: metrics.summary.netSavingsPct,
|
|
93
|
+
overheadTokens: metrics.summary.overheadTokens,
|
|
74
94
|
topTools: metrics.summary.tools.slice(0, 3).map((tool) => ({
|
|
75
95
|
tool: tool.tool,
|
|
76
96
|
savedTokens: tool.savedTokens,
|
|
97
|
+
netSavedTokens: tool.netSavedTokens,
|
|
77
98
|
count: tool.count,
|
|
78
99
|
})),
|
|
79
100
|
};
|
|
80
101
|
};
|
|
81
102
|
|
|
103
|
+
const buildRecommendedStep = (tool, instruction, priority = 'recommended') => ({
|
|
104
|
+
tool,
|
|
105
|
+
instruction,
|
|
106
|
+
priority,
|
|
107
|
+
});
|
|
108
|
+
|
|
82
109
|
const classifyContinuity = ({ prompt, summaryResult }) => {
|
|
83
110
|
if (!summaryResult?.found) {
|
|
84
111
|
if (summaryResult?.ambiguous) {
|
|
@@ -163,6 +190,236 @@ const buildAutoCreateUpdate = (prompt) => ({
|
|
|
163
190
|
nextStep: 'Inspect relevant code, confirm the task boundaries, and checkpoint the first milestone.',
|
|
164
191
|
});
|
|
165
192
|
|
|
193
|
+
const summarizeRefreshedContext = (result, { indexRefreshed = false } = {}) => {
|
|
194
|
+
if (!result?.success) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
indexFreshness: result.indexFreshness ?? 'unavailable',
|
|
200
|
+
indexRefreshed,
|
|
201
|
+
graphCoverage: result.graphCoverage ?? result.confidence?.graphCoverage ?? null,
|
|
202
|
+
hints: Array.isArray(result.hints) ? result.hints.slice(0, 2) : [],
|
|
203
|
+
topFiles: Array.isArray(result.context)
|
|
204
|
+
? result.context.slice(0, REFRESHED_CONTEXT_FILE_LIMIT).map((item) => ({
|
|
205
|
+
file: item.file,
|
|
206
|
+
role: item.role,
|
|
207
|
+
readMode: item.readMode ?? null,
|
|
208
|
+
reasonIncluded: item.reasonIncluded ?? null,
|
|
209
|
+
symbols: Array.isArray(item.symbols) ? item.symbols.slice(0, 3) : [],
|
|
210
|
+
}))
|
|
211
|
+
: [],
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const refreshPromptContext = async (prompt) => {
|
|
216
|
+
if (!hasMeaningfulPrompt(prompt)) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const buildContext = async () => smartContext({
|
|
221
|
+
task: prompt,
|
|
222
|
+
detail: 'minimal',
|
|
223
|
+
include: ['hints'],
|
|
224
|
+
maxTokens: DEFAULT_REFRESH_CONTEXT_MAX_TOKENS,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let result = await buildContext();
|
|
228
|
+
let indexRefreshed = false;
|
|
229
|
+
|
|
230
|
+
if (INDEX_REFRESH_STATES.has(result?.indexFreshness ?? '')) {
|
|
231
|
+
try {
|
|
232
|
+
const { index } = buildIndexIncremental(projectRoot);
|
|
233
|
+
await persistIndex(index, projectRoot);
|
|
234
|
+
indexRefreshed = true;
|
|
235
|
+
result = await buildContext();
|
|
236
|
+
} catch {
|
|
237
|
+
// best-effort refresh only
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return summarizeRefreshedContext(result, { indexRefreshed });
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const shouldIsolateSession = ({ sessionId, ensureSession, prompt, continuity }) =>
|
|
245
|
+
!sessionId
|
|
246
|
+
&& ensureSession
|
|
247
|
+
&& hasMeaningfulPrompt(prompt)
|
|
248
|
+
&& continuity
|
|
249
|
+
&& !SAFE_CONTINUITY_STATES.has(continuity.state ?? '');
|
|
250
|
+
|
|
251
|
+
const shouldRefreshContext = ({ prompt, ensureSession, summaryResult, continuity, isolatedSession, autoCreated }) =>
|
|
252
|
+
hasMeaningfulPrompt(prompt)
|
|
253
|
+
&& (
|
|
254
|
+
isolatedSession
|
|
255
|
+
|| autoCreated
|
|
256
|
+
|| (ensureSession && (summaryResult?.ambiguous || !summaryResult?.found))
|
|
257
|
+
|| ['possible_shift', 'context_mismatch'].includes(continuity?.state)
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const buildStartRecommendedPath = ({
|
|
261
|
+
prompt,
|
|
262
|
+
ensureSession,
|
|
263
|
+
summaryResult,
|
|
264
|
+
continuity,
|
|
265
|
+
refreshedContext,
|
|
266
|
+
mutationSafety,
|
|
267
|
+
autoCreated,
|
|
268
|
+
isolatedSession,
|
|
269
|
+
}) => {
|
|
270
|
+
const nextTools = [];
|
|
271
|
+
const steps = [];
|
|
272
|
+
|
|
273
|
+
if (mutationSafety?.blocked) {
|
|
274
|
+
nextTools.push('repo_safety', 'smart_search', 'smart_read');
|
|
275
|
+
steps.push(buildRecommendedStep(
|
|
276
|
+
'repo_safety',
|
|
277
|
+
'Pause persisted-write workflows, surface blockedBy, and follow recommendedActions before retrying checkpoints or workflow tracking.',
|
|
278
|
+
'required',
|
|
279
|
+
));
|
|
280
|
+
steps.push(buildRecommendedStep(
|
|
281
|
+
'smart_search',
|
|
282
|
+
'Continue with read-only exploration until repo safety is fixed.',
|
|
283
|
+
'recommended',
|
|
284
|
+
));
|
|
285
|
+
} else if (refreshedContext?.topFiles?.length) {
|
|
286
|
+
nextTools.push('smart_read', 'smart_turn');
|
|
287
|
+
steps.push(buildRecommendedStep(
|
|
288
|
+
'smart_read',
|
|
289
|
+
'Start from refreshedContext.topFiles with smart_read(outline|signatures|symbol) before any full reads.',
|
|
290
|
+
));
|
|
291
|
+
} else if (hasMeaningfulPrompt(prompt)) {
|
|
292
|
+
nextTools.push('smart_context', 'smart_read', 'smart_turn');
|
|
293
|
+
steps.push(buildRecommendedStep(
|
|
294
|
+
'smart_context',
|
|
295
|
+
'Build focused multi-file context with smart_context(...) or smart_search(intent=...) before opening more files.',
|
|
296
|
+
));
|
|
297
|
+
steps.push(buildRecommendedStep(
|
|
298
|
+
'smart_read',
|
|
299
|
+
'Use smart_read(outline|signatures|symbol) as the default follow-up read path.',
|
|
300
|
+
));
|
|
301
|
+
} else {
|
|
302
|
+
nextTools.push('smart_search', 'smart_read');
|
|
303
|
+
steps.push(buildRecommendedStep(
|
|
304
|
+
'smart_search',
|
|
305
|
+
'Stay lightweight: only use devctx search/read if the task grows beyond a trivial lookup.',
|
|
306
|
+
));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (summaryResult?.ambiguous && summaryResult?.recommendedSessionId) {
|
|
310
|
+
nextTools.unshift('smart_turn');
|
|
311
|
+
steps.unshift(buildRecommendedStep(
|
|
312
|
+
'smart_turn',
|
|
313
|
+
`Reuse or pass sessionId=${summaryResult.recommendedSessionId} if you want to resume the recommended persisted session explicitly.`,
|
|
314
|
+
'required',
|
|
315
|
+
));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!mutationSafety?.blocked) {
|
|
319
|
+
nextTools.push('smart_turn');
|
|
320
|
+
steps.push(buildRecommendedStep(
|
|
321
|
+
'smart_turn',
|
|
322
|
+
'Checkpoint with smart_turn(end, event=milestone) after the first meaningful progress point.',
|
|
323
|
+
));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
phase: 'start',
|
|
328
|
+
mode: mutationSafety?.blocked
|
|
329
|
+
? 'blocked_guided'
|
|
330
|
+
: refreshedContext
|
|
331
|
+
? 'guided_refresh'
|
|
332
|
+
: hasMeaningfulPrompt(prompt)
|
|
333
|
+
? 'guided_context'
|
|
334
|
+
: 'lightweight',
|
|
335
|
+
contextSource: refreshedContext
|
|
336
|
+
? 'refreshed_context'
|
|
337
|
+
: continuity?.shouldReuseContext
|
|
338
|
+
? 'persisted_summary'
|
|
339
|
+
: 'direct_prompt',
|
|
340
|
+
continuityState: continuity?.state ?? null,
|
|
341
|
+
ensureSessionRecommended: Boolean(hasMeaningfulPrompt(prompt) && (ensureSession || !summaryResult?.found)),
|
|
342
|
+
autoCreated,
|
|
343
|
+
isolatedSession,
|
|
344
|
+
nextTools: [...new Set(nextTools)],
|
|
345
|
+
steps,
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const buildEndRecommendedPath = ({ event, checkpoint, mutationSafety, workflow }) => {
|
|
350
|
+
const nextTools = [];
|
|
351
|
+
const steps = [];
|
|
352
|
+
|
|
353
|
+
if (mutationSafety?.blocked) {
|
|
354
|
+
nextTools.push('repo_safety', 'smart_search', 'smart_read');
|
|
355
|
+
steps.push(buildRecommendedStep(
|
|
356
|
+
'repo_safety',
|
|
357
|
+
'Fix repo safety before expecting checkpoints, workflow tracking, or hook state writes to persist.',
|
|
358
|
+
'required',
|
|
359
|
+
));
|
|
360
|
+
} else if (checkpoint?.skipped) {
|
|
361
|
+
nextTools.push('smart_turn');
|
|
362
|
+
steps.push(buildRecommendedStep(
|
|
363
|
+
'smart_turn',
|
|
364
|
+
'No durable checkpoint was written; keep working and call smart_turn(end, event=milestone) once you have a concrete milestone or next step.',
|
|
365
|
+
'required',
|
|
366
|
+
));
|
|
367
|
+
} else {
|
|
368
|
+
nextTools.push('smart_turn');
|
|
369
|
+
steps.push(buildRecommendedStep(
|
|
370
|
+
'smart_turn',
|
|
371
|
+
'On the next substantial prompt, restart with smart_turn(start, prompt, ensureSession=true) to reuse this checkpoint cleanly.',
|
|
372
|
+
));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (workflow?.ended) {
|
|
376
|
+
nextTools.push('smart_turn');
|
|
377
|
+
steps.push(buildRecommendedStep(
|
|
378
|
+
'smart_turn',
|
|
379
|
+
'This workflow is closed; start a fresh turn for the next substantial task boundary.',
|
|
380
|
+
));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
phase: 'end',
|
|
385
|
+
mode: mutationSafety?.blocked
|
|
386
|
+
? 'blocked_guided'
|
|
387
|
+
: checkpoint?.skipped
|
|
388
|
+
? 'continue_until_milestone'
|
|
389
|
+
: 'checkpointed',
|
|
390
|
+
checkpointEvent: event,
|
|
391
|
+
nextTools: [...new Set(nextTools)],
|
|
392
|
+
steps,
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const persistSmartTurnQualityMetrics = async ({
|
|
397
|
+
phase,
|
|
398
|
+
sessionId,
|
|
399
|
+
target,
|
|
400
|
+
action,
|
|
401
|
+
latencyMs,
|
|
402
|
+
metadata,
|
|
403
|
+
}) => {
|
|
404
|
+
await persistMetrics({
|
|
405
|
+
tool: 'smart_turn',
|
|
406
|
+
action,
|
|
407
|
+
sessionId,
|
|
408
|
+
target,
|
|
409
|
+
rawTokens: 0,
|
|
410
|
+
compressedTokens: 0,
|
|
411
|
+
savedTokens: 0,
|
|
412
|
+
savingsPct: 0,
|
|
413
|
+
latencyMs,
|
|
414
|
+
metadata: {
|
|
415
|
+
analyticsKind: PRODUCT_QUALITY_ANALYTICS_KIND,
|
|
416
|
+
phase,
|
|
417
|
+
...metadata,
|
|
418
|
+
},
|
|
419
|
+
timestamp: new Date().toISOString(),
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
|
|
166
423
|
const startTurn = async ({
|
|
167
424
|
sessionId,
|
|
168
425
|
prompt,
|
|
@@ -172,6 +429,7 @@ const startTurn = async ({
|
|
|
172
429
|
metricsWindow = '7d',
|
|
173
430
|
latestMetrics = 5,
|
|
174
431
|
} = {}) => {
|
|
432
|
+
const startTime = Date.now();
|
|
175
433
|
let summaryResult = await smartSummary({
|
|
176
434
|
action: 'get',
|
|
177
435
|
sessionId,
|
|
@@ -179,6 +437,8 @@ const startTurn = async ({
|
|
|
179
437
|
});
|
|
180
438
|
|
|
181
439
|
let autoCreated = false;
|
|
440
|
+
let isolatedSession = false;
|
|
441
|
+
let previousSessionId = null;
|
|
182
442
|
if (!summaryResult.found && !summaryResult.ambiguous && ensureSession && hasMeaningfulPrompt(prompt)) {
|
|
183
443
|
const created = await smartSummary({
|
|
184
444
|
action: 'update',
|
|
@@ -195,8 +455,69 @@ const startTurn = async ({
|
|
|
195
455
|
}
|
|
196
456
|
}
|
|
197
457
|
|
|
198
|
-
|
|
458
|
+
let continuity = classifyContinuity({ prompt, summaryResult });
|
|
459
|
+
|
|
460
|
+
if (summaryResult.found && shouldIsolateSession({ sessionId, ensureSession, prompt, continuity })) {
|
|
461
|
+
const created = await smartSummary({
|
|
462
|
+
action: 'update',
|
|
463
|
+
update: buildAutoCreateUpdate(prompt),
|
|
464
|
+
maxTokens,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (!created.blocked) {
|
|
468
|
+
isolatedSession = true;
|
|
469
|
+
previousSessionId = summaryResult.sessionId ?? null;
|
|
470
|
+
summaryResult = await smartSummary({
|
|
471
|
+
action: 'get',
|
|
472
|
+
sessionId: created.sessionId,
|
|
473
|
+
maxTokens,
|
|
474
|
+
});
|
|
475
|
+
continuity = classifyContinuity({ prompt, summaryResult });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
199
479
|
const effectiveSessionId = summaryResult.sessionId ?? sessionId ?? summaryResult.recommendedSessionId ?? null;
|
|
480
|
+
const mutationSafety = buildMutationSafety(summaryResult.repoSafety);
|
|
481
|
+
const workflowBlocked = Boolean(isWorkflowTrackingEnabled() && mutationSafety?.blocked);
|
|
482
|
+
const refreshedContext = shouldRefreshContext({
|
|
483
|
+
prompt,
|
|
484
|
+
ensureSession,
|
|
485
|
+
summaryResult,
|
|
486
|
+
continuity,
|
|
487
|
+
isolatedSession,
|
|
488
|
+
autoCreated,
|
|
489
|
+
})
|
|
490
|
+
? await refreshPromptContext(prompt)
|
|
491
|
+
: null;
|
|
492
|
+
|
|
493
|
+
let workflow = null;
|
|
494
|
+
if (workflowBlocked) {
|
|
495
|
+
workflow = { enabled: true, blocked: true, workflowId: null, workflowType: null, autoTracked: false };
|
|
496
|
+
} else if (effectiveSessionId && isWorkflowTrackingEnabled()) {
|
|
497
|
+
const workflowId = await autoTrackWorkflow(
|
|
498
|
+
effectiveSessionId,
|
|
499
|
+
summaryResult.summary?.goal ?? prompt ?? '',
|
|
500
|
+
);
|
|
501
|
+
if (workflowId) {
|
|
502
|
+
const activeWorkflow = await getActiveWorkflowForSession(effectiveSessionId);
|
|
503
|
+
workflow = activeWorkflow
|
|
504
|
+
? {
|
|
505
|
+
enabled: true,
|
|
506
|
+
workflowId: activeWorkflow.workflow_id,
|
|
507
|
+
workflowType: activeWorkflow.workflow_type,
|
|
508
|
+
autoTracked: true,
|
|
509
|
+
}
|
|
510
|
+
: {
|
|
511
|
+
enabled: true,
|
|
512
|
+
workflowId,
|
|
513
|
+
workflowType: null,
|
|
514
|
+
autoTracked: true,
|
|
515
|
+
};
|
|
516
|
+
} else {
|
|
517
|
+
workflow = { enabled: true, workflowId: null, workflowType: null, autoTracked: false };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
200
521
|
const metrics = includeMetrics
|
|
201
522
|
? await smartMetrics({
|
|
202
523
|
window: metricsWindow,
|
|
@@ -205,26 +526,76 @@ const startTurn = async ({
|
|
|
205
526
|
})
|
|
206
527
|
: null;
|
|
207
528
|
|
|
529
|
+
const recommendedPath = buildStartRecommendedPath({
|
|
530
|
+
prompt,
|
|
531
|
+
ensureSession,
|
|
532
|
+
summaryResult,
|
|
533
|
+
continuity,
|
|
534
|
+
refreshedContext,
|
|
535
|
+
mutationSafety,
|
|
536
|
+
autoCreated,
|
|
537
|
+
isolatedSession,
|
|
538
|
+
});
|
|
208
539
|
|
|
209
|
-
|
|
540
|
+
await persistSmartTurnQualityMetrics({
|
|
541
|
+
phase: 'start',
|
|
542
|
+
sessionId: effectiveSessionId ?? null,
|
|
543
|
+
target: truncate(prompt, 120) || 'smart_turn:start',
|
|
544
|
+
action: 'start',
|
|
545
|
+
latencyMs: Date.now() - startTime,
|
|
546
|
+
metadata: {
|
|
547
|
+
continuityState: continuity?.state ?? null,
|
|
548
|
+
shouldReuseContext: Boolean(continuity?.shouldReuseContext),
|
|
549
|
+
sessionFound: Boolean(summaryResult.found),
|
|
550
|
+
ambiguousResume: Boolean(summaryResult.ambiguous),
|
|
551
|
+
autoCreated,
|
|
552
|
+
isolatedSession,
|
|
553
|
+
previousSessionId,
|
|
554
|
+
mutationBlocked: Boolean(mutationSafety?.blocked),
|
|
555
|
+
blockedBy: mutationSafety?.blockedBy ?? [],
|
|
556
|
+
recommendedActionsCount: mutationSafety?.recommendedActions?.length ?? 0,
|
|
557
|
+
refreshedContext: Boolean(refreshedContext),
|
|
558
|
+
refreshedTopFiles: refreshedContext?.topFiles?.length ?? 0,
|
|
559
|
+
indexRefreshed: Boolean(refreshedContext?.indexRefreshed),
|
|
560
|
+
recommendedPathMode: recommendedPath.mode,
|
|
561
|
+
nextToolsCount: recommendedPath.nextTools.length,
|
|
562
|
+
workflowEnabled: Boolean(workflow?.enabled),
|
|
563
|
+
workflowAutoTracked: Boolean(workflow?.autoTracked),
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
return attachSafetyMetadata({
|
|
210
568
|
phase: 'start',
|
|
211
569
|
promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
|
|
212
570
|
sessionId: effectiveSessionId,
|
|
213
571
|
found: summaryResult.found ?? false,
|
|
214
572
|
autoCreated,
|
|
573
|
+
isolatedSession,
|
|
574
|
+
...(previousSessionId ? { previousSessionId } : {}),
|
|
215
575
|
continuity,
|
|
216
576
|
summary: summaryResult.summary ?? null,
|
|
217
|
-
|
|
218
|
-
|
|
577
|
+
...(refreshedContext ? { refreshedContext } : {}),
|
|
578
|
+
...(workflow ? { workflow } : {}),
|
|
219
579
|
...(summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
|
|
220
580
|
...(summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
|
|
221
581
|
...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
582
|
+
storageHealth: summaryResult.storageHealth ?? metrics?.storageHealth ?? null,
|
|
583
|
+
recommendedPath,
|
|
584
|
+
message: mutationSafety?.blocked
|
|
585
|
+
? mutationSafety.message
|
|
586
|
+
: summaryResult.found
|
|
587
|
+
? continuity.reason
|
|
588
|
+
: autoCreated
|
|
589
|
+
? 'Created a new persisted session for this task prompt.'
|
|
590
|
+
: continuity.reason,
|
|
591
|
+
}, {
|
|
592
|
+
repoSafety: summaryResult.repoSafety ?? metrics?.repoSafety ?? null,
|
|
593
|
+
sideEffectsSuppressed: Boolean(summaryResult.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
|
|
594
|
+
subject: 'Project-local context writes',
|
|
595
|
+
degradedReason: 'repo_safety_blocked',
|
|
596
|
+
degradedMode: 'read_only_snapshot',
|
|
597
|
+
degradedImpact: 'Checkpoint and workflow side effects are paused while git hygiene is blocked.',
|
|
598
|
+
});
|
|
228
599
|
};
|
|
229
600
|
|
|
230
601
|
const endTurn = async ({
|
|
@@ -237,6 +608,7 @@ const endTurn = async ({
|
|
|
237
608
|
metricsWindow = '7d',
|
|
238
609
|
latestMetrics = 5,
|
|
239
610
|
} = {}) => {
|
|
611
|
+
const startTime = Date.now();
|
|
240
612
|
const checkpoint = await smartSummary({
|
|
241
613
|
action: 'checkpoint',
|
|
242
614
|
sessionId,
|
|
@@ -247,6 +619,39 @@ const endTurn = async ({
|
|
|
247
619
|
});
|
|
248
620
|
|
|
249
621
|
const effectiveSessionId = checkpoint.sessionId ?? sessionId ?? 'active';
|
|
622
|
+
const mutationSafety = buildMutationSafety(checkpoint.repoSafety);
|
|
623
|
+
const workflowBlocked = Boolean(isWorkflowTrackingEnabled() && mutationSafety?.blocked);
|
|
624
|
+
let workflow = null;
|
|
625
|
+
if (workflowBlocked) {
|
|
626
|
+
workflow = { enabled: true, blocked: true, workflowId: null, workflowType: null, ended: false };
|
|
627
|
+
} else if (
|
|
628
|
+
checkpoint.sessionId
|
|
629
|
+
&& !checkpoint.skipped
|
|
630
|
+
&& WORKFLOW_END_EVENTS.has(event)
|
|
631
|
+
&& isWorkflowTrackingEnabled()
|
|
632
|
+
) {
|
|
633
|
+
const activeWorkflow = await getActiveWorkflowForSession(checkpoint.sessionId);
|
|
634
|
+
if (activeWorkflow?.workflow_id) {
|
|
635
|
+
const endedWorkflow = await endWorkflow(activeWorkflow.workflow_id);
|
|
636
|
+
workflow = endedWorkflow
|
|
637
|
+
? {
|
|
638
|
+
enabled: true,
|
|
639
|
+
workflowId: endedWorkflow.workflowId,
|
|
640
|
+
workflowType: endedWorkflow.workflowType,
|
|
641
|
+
ended: true,
|
|
642
|
+
summary: endedWorkflow,
|
|
643
|
+
}
|
|
644
|
+
: {
|
|
645
|
+
enabled: true,
|
|
646
|
+
workflowId: activeWorkflow.workflow_id,
|
|
647
|
+
workflowType: activeWorkflow.workflow_type,
|
|
648
|
+
ended: false,
|
|
649
|
+
};
|
|
650
|
+
} else {
|
|
651
|
+
workflow = { enabled: true, workflowId: null, workflowType: null, ended: false };
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
250
655
|
const metrics = includeMetrics
|
|
251
656
|
? await smartMetrics({
|
|
252
657
|
window: metricsWindow,
|
|
@@ -255,15 +660,51 @@ const endTurn = async ({
|
|
|
255
660
|
})
|
|
256
661
|
: null;
|
|
257
662
|
|
|
258
|
-
|
|
663
|
+
const recommendedPath = buildEndRecommendedPath({
|
|
664
|
+
event,
|
|
665
|
+
checkpoint,
|
|
666
|
+
mutationSafety,
|
|
667
|
+
workflow,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await persistSmartTurnQualityMetrics({
|
|
671
|
+
phase: 'end',
|
|
672
|
+
sessionId: checkpoint.sessionId ?? sessionId ?? null,
|
|
673
|
+
target: event,
|
|
674
|
+
action: 'end',
|
|
675
|
+
latencyMs: Date.now() - startTime,
|
|
676
|
+
metadata: {
|
|
677
|
+
event,
|
|
678
|
+
checkpointSkipped: Boolean(checkpoint.skipped),
|
|
679
|
+
checkpointPersisted: !checkpoint.skipped && !checkpoint.blocked,
|
|
680
|
+
mutationBlocked: Boolean(mutationSafety?.blocked),
|
|
681
|
+
blockedBy: mutationSafety?.blockedBy ?? [],
|
|
682
|
+
recommendedActionsCount: mutationSafety?.recommendedActions?.length ?? 0,
|
|
683
|
+
recommendedPathMode: recommendedPath.mode,
|
|
684
|
+
workflowEnabled: Boolean(workflow?.enabled),
|
|
685
|
+
workflowEnded: Boolean(workflow?.ended),
|
|
686
|
+
checkpointScore: checkpoint.checkpoint?.score ?? null,
|
|
687
|
+
checkpointThreshold: checkpoint.checkpoint?.threshold ?? null,
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return attachSafetyMetadata({
|
|
259
692
|
phase: 'end',
|
|
260
693
|
sessionId: checkpoint.sessionId ?? sessionId ?? null,
|
|
261
694
|
checkpoint,
|
|
695
|
+
...(workflow ? { workflow } : {}),
|
|
696
|
+
...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
|
|
697
|
+
storageHealth: checkpoint.storageHealth ?? metrics?.storageHealth ?? null,
|
|
698
|
+
recommendedPath,
|
|
699
|
+
message: mutationSafety?.blocked ? mutationSafety.message : checkpoint.message,
|
|
700
|
+
}, {
|
|
262
701
|
repoSafety: checkpoint.repoSafety ?? metrics?.repoSafety ?? null,
|
|
263
702
|
sideEffectsSuppressed: Boolean(checkpoint.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
703
|
+
subject: 'Project-local context writes',
|
|
704
|
+
degradedReason: 'repo_safety_blocked',
|
|
705
|
+
degradedMode: 'read_only_snapshot',
|
|
706
|
+
degradedImpact: 'Checkpoint and workflow side effects are paused while git hygiene is blocked.',
|
|
707
|
+
});
|
|
267
708
|
};
|
|
268
709
|
|
|
269
710
|
export const smartTurn = async ({
|