smart-context-mcp 1.16.5 → 1.17.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 +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/client-contract.js +5 -2
- package/src/index-manager.js +21 -0
- package/src/index.js +16 -6
- package/src/orchestration/adapters/claude-adapter.js +86 -3
- package/src/orchestration/adapters/cursor-adapter.js +86 -3
- package/src/orchestration/policy/event-policy.js +28 -0
- package/src/server.js +26 -1
- package/src/storage/sqlite.js +46 -0
- package/src/tools/smart-search.js +6 -15
- package/src/tools/smart-shell.js +116 -2
- package/src/tools/smart-summary.js +146 -11
- package/src/tools/smart-turn.js +102 -30
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Restart your AI client. Done.
|
|
|
56
56
|
# Check installed version
|
|
57
57
|
npm list -g smart-context-mcp
|
|
58
58
|
|
|
59
|
-
# Should show: smart-context-mcp@1.
|
|
59
|
+
# Should show: smart-context-mcp@1.17.0 (or later)
|
|
60
60
|
|
|
61
61
|
# Update to latest version
|
|
62
62
|
npm update -g smart-context-mcp
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smart-context-mcp",
|
|
3
3
|
"mcpName": "io.github.Arrayo/smart-context-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.17.0",
|
|
5
5
|
"description": "MCP server that reduces agent token usage by 90% with intelligent context compression, task checkpoint persistence, and workflow-aware agent guidance.",
|
|
6
6
|
"author": "Francisco Caballero Portero <fcp1978@hotmail.com>",
|
|
7
7
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/Arrayo/smart-context-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.17.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "smart-context-mcp",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.17.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/src/client-contract.js
CHANGED
|
@@ -52,8 +52,11 @@ export const buildRecommendedPathLines = (
|
|
|
52
52
|
lines.push(`${nextToolsLabel}: ${recommendedPath.nextTools.slice(0, 3).join(' -> ')}`);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (includePath
|
|
56
|
-
|
|
55
|
+
if (includePath) {
|
|
56
|
+
const pathInstruction = recommendedPath.steps?.[0]?.instruction ?? recommendedPath.next ?? null;
|
|
57
|
+
if (pathInstruction) {
|
|
58
|
+
lines.push(`${pathLabel}: ${truncate(pathInstruction, maxLength)}`);
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
return lines;
|
package/src/index-manager.js
CHANGED
|
@@ -128,3 +128,24 @@ export const getIndexStatus = (root = projectRoot) => {
|
|
|
128
128
|
age: meta?.builtAt ? Date.now() - meta.builtAt : null
|
|
129
129
|
};
|
|
130
130
|
};
|
|
131
|
+
|
|
132
|
+
let backgroundBuildPromise = null;
|
|
133
|
+
|
|
134
|
+
export const triggerBackgroundIndexBuild = ({ root = projectRoot, timeoutMs = INDEX_BUILD_TIMEOUT_MS } = {}) => {
|
|
135
|
+
if (backgroundBuildPromise) {
|
|
136
|
+
return backgroundBuildPromise;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const status = getIndexStatus(root);
|
|
140
|
+
if (status.available && status.fresh) {
|
|
141
|
+
return Promise.resolve({ status: 'fresh', cached: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
backgroundBuildPromise = ensureIndexReady({ root, timeoutMs })
|
|
145
|
+
.catch((error) => ({ status: 'error', error: error?.message ?? String(error) }))
|
|
146
|
+
.finally(() => {
|
|
147
|
+
backgroundBuildPromise = null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return backgroundBuildPromise;
|
|
151
|
+
};
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import ts from 'typescript';
|
|
|
5
5
|
import { isBinaryBuffer } from './utils/fs.js';
|
|
6
6
|
import { IGNORED_DIRS } from './config/ignored-paths.js';
|
|
7
7
|
|
|
8
|
-
const INDEX_VERSION =
|
|
8
|
+
const INDEX_VERSION = 5;
|
|
9
9
|
|
|
10
10
|
const MAX_SIGNATURE_LEN = 200;
|
|
11
11
|
const MAX_SNIPPET_LEN = 280;
|
|
@@ -722,8 +722,6 @@ export const buildIndex = (root, progress = null) => {
|
|
|
722
722
|
if (!invertedIndex[key]) invertedIndex[key] = [];
|
|
723
723
|
const entry = { path: relPath, line: sym.line, kind: sym.kind };
|
|
724
724
|
if (sym.parent) entry.parent = sym.parent;
|
|
725
|
-
if (sym.signature) entry.signature = sym.signature;
|
|
726
|
-
if (sym.snippet) entry.snippet = sym.snippet;
|
|
727
725
|
invertedIndex[key].push(entry);
|
|
728
726
|
}
|
|
729
727
|
} catch { /* unreadable */ }
|
|
@@ -783,10 +781,24 @@ export const buildIndex = (root, progress = null) => {
|
|
|
783
781
|
// Query helpers
|
|
784
782
|
// ---------------------------------------------------------------------------
|
|
785
783
|
|
|
784
|
+
const findSymbolForHit = (index, key, hit) => {
|
|
785
|
+
const symbols = index.files?.[hit.path]?.symbols;
|
|
786
|
+
if (!Array.isArray(symbols)) return null;
|
|
787
|
+
return symbols.find((sym) => sym.name?.toLowerCase() === key && sym.line === hit.line) ?? null;
|
|
788
|
+
};
|
|
789
|
+
|
|
786
790
|
export const queryIndex = (index, symbolName) => {
|
|
787
791
|
if (!index?.invertedIndex) return [];
|
|
788
792
|
const key = symbolName.toLowerCase();
|
|
789
|
-
|
|
793
|
+
const hits = index.invertedIndex[key] ?? [];
|
|
794
|
+
return hits.map((hit) => {
|
|
795
|
+
const sym = findSymbolForHit(index, key, hit);
|
|
796
|
+
if (!sym) return hit;
|
|
797
|
+
const enriched = { ...hit };
|
|
798
|
+
if (sym.signature && !enriched.signature) enriched.signature = sym.signature;
|
|
799
|
+
if (sym.snippet && !enriched.snippet) enriched.snippet = sym.snippet;
|
|
800
|
+
return enriched;
|
|
801
|
+
});
|
|
790
802
|
};
|
|
791
803
|
|
|
792
804
|
export const queryRelated = (index, relPath) => {
|
|
@@ -873,8 +885,6 @@ export const reindexFile = (index, root, relPath) => {
|
|
|
873
885
|
if (!index.invertedIndex[key]) index.invertedIndex[key] = [];
|
|
874
886
|
const invEntry = { path: relPath, line: sym.line, kind: sym.kind };
|
|
875
887
|
if (sym.parent) invEntry.parent = sym.parent;
|
|
876
|
-
if (sym.signature) invEntry.signature = sym.signature;
|
|
877
|
-
if (sym.snippet) invEntry.snippet = sym.snippet;
|
|
878
888
|
index.invertedIndex[key].push(invEntry);
|
|
879
889
|
}
|
|
880
890
|
|
|
@@ -12,7 +12,15 @@ import {
|
|
|
12
12
|
upsertAgentRun,
|
|
13
13
|
} from '../../storage/sqlite.js';
|
|
14
14
|
import { DEFAULT_START_MAX_TOKENS, resolveManagedStart } from '../base-orchestrator.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MAX_READ_FILES,
|
|
17
|
+
READ_CHECKPOINT_THRESHOLD,
|
|
18
|
+
READ_CHECKPOINT_THROTTLE_MS,
|
|
19
|
+
extractNextStep,
|
|
20
|
+
extractReadPathsFromToolUse,
|
|
21
|
+
normalizeWhitespace,
|
|
22
|
+
truncate,
|
|
23
|
+
} from '../policy/event-policy.js';
|
|
16
24
|
|
|
17
25
|
export const HOOK_CLIENT = 'claude';
|
|
18
26
|
export const STOP_MAX_TOKENS = 300;
|
|
@@ -204,6 +212,11 @@ export const createClaudeAdapter = ({
|
|
|
204
212
|
const autoStartTriggered = action === 'SessionStart' || action === 'UserPromptSubmit';
|
|
205
213
|
const autoCheckpointTriggered = action === 'Stop' && autoAppended;
|
|
206
214
|
|
|
215
|
+
const shouldPersist = autoStartTriggered || autoCheckpointTriggered || blocked || overheadTokens > 0;
|
|
216
|
+
if (!shouldPersist) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
207
220
|
await persistMetric({
|
|
208
221
|
tool: 'claude_hook',
|
|
209
222
|
action,
|
|
@@ -266,15 +279,54 @@ export const createClaudeAdapter = ({
|
|
|
266
279
|
checkpointEvent: null,
|
|
267
280
|
touchedFiles: [],
|
|
268
281
|
meaningfulWriteCount: 0,
|
|
282
|
+
readFiles: [],
|
|
283
|
+
meaningfulReadCount: 0,
|
|
284
|
+
lastReadCheckpointAt: 0,
|
|
269
285
|
},
|
|
270
286
|
});
|
|
271
287
|
};
|
|
272
288
|
|
|
273
|
-
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use' } = {}) => {
|
|
289
|
+
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use', mode = 'milestone' } = {}) => {
|
|
274
290
|
if (!state?.projectSessionId || getMutationSafety().shouldBlock) {
|
|
275
291
|
return false;
|
|
276
292
|
}
|
|
277
293
|
|
|
294
|
+
if (mode === 'read_progress') {
|
|
295
|
+
const filesForUpdate = state.readFiles.slice(-MAX_READ_FILES);
|
|
296
|
+
if (filesForUpdate.length === 0) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await summaryTool({
|
|
301
|
+
action: 'auto_append',
|
|
302
|
+
sessionId: state.projectSessionId,
|
|
303
|
+
update: {
|
|
304
|
+
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
305
|
+
touchedFiles: filesForUpdate,
|
|
306
|
+
},
|
|
307
|
+
maxTokens: STOP_MAX_TOKENS,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (state.taskId) {
|
|
311
|
+
await writeTaskHandoff({
|
|
312
|
+
taskId: state.taskId,
|
|
313
|
+
sessionId: state.projectSessionId,
|
|
314
|
+
fromAgentId: state.agentId ?? null,
|
|
315
|
+
toAgentId: null,
|
|
316
|
+
trigger: 'read_progress',
|
|
317
|
+
summary: {
|
|
318
|
+
currentFocus: state.promptPreview,
|
|
319
|
+
touchedFiles: filesForUpdate,
|
|
320
|
+
pending: [],
|
|
321
|
+
nextStep: null,
|
|
322
|
+
evidence: [`Exploration: ${filesForUpdate.length} file(s) read`],
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
278
330
|
const update = {
|
|
279
331
|
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
280
332
|
...(state.touchedFiles.length > 0 ? { touchedFiles: state.touchedFiles } : {}),
|
|
@@ -388,6 +440,16 @@ export const createClaudeAdapter = ({
|
|
|
388
440
|
toolInput: input.tool_input,
|
|
389
441
|
toolResponse: input.tool_response,
|
|
390
442
|
});
|
|
443
|
+
const readPaths = touchedFiles.length === 0 && !checkpoint.matched
|
|
444
|
+
? extractReadPathsFromToolUse({ toolName: input.tool_name, toolInput: input.tool_input })
|
|
445
|
+
: [];
|
|
446
|
+
|
|
447
|
+
const previousReadFiles = Array.isArray(existing.readFiles) ? existing.readFiles : [];
|
|
448
|
+
const previousReadCount = Number.isFinite(existing.meaningfulReadCount) ? existing.meaningfulReadCount : 0;
|
|
449
|
+
const readFiles = readPaths.length > 0
|
|
450
|
+
? uniq([...previousReadFiles, ...readPaths]).slice(-MAX_READ_FILES)
|
|
451
|
+
: previousReadFiles;
|
|
452
|
+
const meaningfulReadCount = readPaths.length > 0 ? previousReadCount + 1 : previousReadCount;
|
|
391
453
|
|
|
392
454
|
let nextState = {
|
|
393
455
|
...existing,
|
|
@@ -395,6 +457,9 @@ export const createClaudeAdapter = ({
|
|
|
395
457
|
checkpointEvent: checkpoint.matched ? checkpoint.event : existing.checkpointEvent,
|
|
396
458
|
touchedFiles: uniq([...existing.touchedFiles, ...touchedFiles]).slice(0, MAX_TOUCHED_FILES),
|
|
397
459
|
meaningfulWriteCount: existing.meaningfulWriteCount + touchedFiles.length,
|
|
460
|
+
readFiles,
|
|
461
|
+
meaningfulReadCount,
|
|
462
|
+
lastReadCheckpointAt: existing.lastReadCheckpointAt ?? 0,
|
|
398
463
|
updatedAt: new Date().toISOString(),
|
|
399
464
|
};
|
|
400
465
|
|
|
@@ -407,10 +472,28 @@ export const createClaudeAdapter = ({
|
|
|
407
472
|
checkpointEvent: 'milestone',
|
|
408
473
|
};
|
|
409
474
|
}
|
|
475
|
+
} else if (
|
|
476
|
+
!checkpoint.matched
|
|
477
|
+
&& touchedFiles.length === 0
|
|
478
|
+
&& meaningfulReadCount >= READ_CHECKPOINT_THRESHOLD
|
|
479
|
+
&& Date.now() - (nextState.lastReadCheckpointAt ?? 0) >= READ_CHECKPOINT_THROTTLE_MS
|
|
480
|
+
) {
|
|
481
|
+
const autoAppended = await maybeAutoCheckpointFromState(nextState, {
|
|
482
|
+
trigger: 'read_progress',
|
|
483
|
+
mode: 'read_progress',
|
|
484
|
+
});
|
|
485
|
+
if (autoAppended) {
|
|
486
|
+
nextState = {
|
|
487
|
+
...nextState,
|
|
488
|
+
readFiles: [],
|
|
489
|
+
meaningfulReadCount: 0,
|
|
490
|
+
lastReadCheckpointAt: Date.now(),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
410
493
|
}
|
|
411
494
|
|
|
412
495
|
await maybeSetTrackedTurnState({ hookKey, state: nextState });
|
|
413
|
-
if (checkpoint.matched || touchedFiles.length > 0) {
|
|
496
|
+
if (checkpoint.matched || touchedFiles.length > 0 || readPaths.length > 0) {
|
|
414
497
|
await recordHookMetrics({
|
|
415
498
|
action: 'PostToolUse',
|
|
416
499
|
sessionId: existing.projectSessionId,
|
|
@@ -12,7 +12,15 @@ import {
|
|
|
12
12
|
upsertAgentRun,
|
|
13
13
|
} from '../../storage/sqlite.js';
|
|
14
14
|
import { DEFAULT_START_MAX_TOKENS, resolveManagedStart } from '../base-orchestrator.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MAX_READ_FILES,
|
|
17
|
+
READ_CHECKPOINT_THRESHOLD,
|
|
18
|
+
READ_CHECKPOINT_THROTTLE_MS,
|
|
19
|
+
extractNextStep,
|
|
20
|
+
extractReadPathsFromToolUse,
|
|
21
|
+
normalizeWhitespace,
|
|
22
|
+
truncate,
|
|
23
|
+
} from '../policy/event-policy.js';
|
|
16
24
|
|
|
17
25
|
export const HOOK_CLIENT = 'cursor';
|
|
18
26
|
export const STOP_MAX_TOKENS = 300;
|
|
@@ -207,6 +215,11 @@ export const createCursorAdapter = ({
|
|
|
207
215
|
const autoStartTriggered = action === 'ConversationStart' || action === 'UserMessageSubmit';
|
|
208
216
|
const autoCheckpointTriggered = action === 'ConversationEnd' && autoAppended;
|
|
209
217
|
|
|
218
|
+
const shouldPersist = autoStartTriggered || autoCheckpointTriggered || blocked || overheadTokens > 0;
|
|
219
|
+
if (!shouldPersist) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
210
223
|
await persistMetric({
|
|
211
224
|
tool: 'cursor_hook',
|
|
212
225
|
action,
|
|
@@ -269,15 +282,54 @@ export const createCursorAdapter = ({
|
|
|
269
282
|
checkpointEvent: null,
|
|
270
283
|
touchedFiles: [],
|
|
271
284
|
meaningfulWriteCount: 0,
|
|
285
|
+
readFiles: [],
|
|
286
|
+
meaningfulReadCount: 0,
|
|
287
|
+
lastReadCheckpointAt: 0,
|
|
272
288
|
},
|
|
273
289
|
});
|
|
274
290
|
};
|
|
275
291
|
|
|
276
|
-
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use' } = {}) => {
|
|
292
|
+
const maybeAutoCheckpointFromState = async (state, { trigger = 'post_tool_use', mode = 'milestone' } = {}) => {
|
|
277
293
|
if (!state?.projectSessionId || getMutationSafety().shouldBlock) {
|
|
278
294
|
return false;
|
|
279
295
|
}
|
|
280
296
|
|
|
297
|
+
if (mode === 'read_progress') {
|
|
298
|
+
const filesForUpdate = state.readFiles.slice(-MAX_READ_FILES);
|
|
299
|
+
if (filesForUpdate.length === 0) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await summaryTool({
|
|
304
|
+
action: 'auto_append',
|
|
305
|
+
sessionId: state.projectSessionId,
|
|
306
|
+
update: {
|
|
307
|
+
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
308
|
+
touchedFiles: filesForUpdate,
|
|
309
|
+
},
|
|
310
|
+
maxTokens: STOP_MAX_TOKENS,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (state.taskId) {
|
|
314
|
+
await writeTaskHandoff({
|
|
315
|
+
taskId: state.taskId,
|
|
316
|
+
sessionId: state.projectSessionId,
|
|
317
|
+
fromAgentId: state.agentId ?? null,
|
|
318
|
+
toAgentId: null,
|
|
319
|
+
trigger: 'read_progress',
|
|
320
|
+
summary: {
|
|
321
|
+
currentFocus: state.promptPreview,
|
|
322
|
+
touchedFiles: filesForUpdate,
|
|
323
|
+
pending: [],
|
|
324
|
+
nextStep: null,
|
|
325
|
+
evidence: [`Exploration: ${filesForUpdate.length} file(s) read`],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
281
333
|
const update = {
|
|
282
334
|
...(state.promptPreview ? { currentFocus: state.promptPreview } : {}),
|
|
283
335
|
...(state.touchedFiles.length > 0 ? { touchedFiles: state.touchedFiles } : {}),
|
|
@@ -391,6 +443,16 @@ export const createCursorAdapter = ({
|
|
|
391
443
|
toolInput: input.tool_input,
|
|
392
444
|
toolResponse: input.tool_response,
|
|
393
445
|
});
|
|
446
|
+
const readPaths = touchedFiles.length === 0 && !checkpoint.matched
|
|
447
|
+
? extractReadPathsFromToolUse({ toolName: input.tool_name, toolInput: input.tool_input })
|
|
448
|
+
: [];
|
|
449
|
+
|
|
450
|
+
const previousReadFiles = Array.isArray(existing.readFiles) ? existing.readFiles : [];
|
|
451
|
+
const previousReadCount = Number.isFinite(existing.meaningfulReadCount) ? existing.meaningfulReadCount : 0;
|
|
452
|
+
const readFiles = readPaths.length > 0
|
|
453
|
+
? uniq([...previousReadFiles, ...readPaths]).slice(-MAX_READ_FILES)
|
|
454
|
+
: previousReadFiles;
|
|
455
|
+
const meaningfulReadCount = readPaths.length > 0 ? previousReadCount + 1 : previousReadCount;
|
|
394
456
|
|
|
395
457
|
let nextState = {
|
|
396
458
|
...existing,
|
|
@@ -398,6 +460,9 @@ export const createCursorAdapter = ({
|
|
|
398
460
|
checkpointEvent: checkpoint.matched ? checkpoint.event : existing.checkpointEvent,
|
|
399
461
|
touchedFiles: uniq([...existing.touchedFiles, ...touchedFiles]).slice(0, MAX_TOUCHED_FILES),
|
|
400
462
|
meaningfulWriteCount: existing.meaningfulWriteCount + touchedFiles.length,
|
|
463
|
+
readFiles,
|
|
464
|
+
meaningfulReadCount,
|
|
465
|
+
lastReadCheckpointAt: existing.lastReadCheckpointAt ?? 0,
|
|
401
466
|
updatedAt: new Date().toISOString(),
|
|
402
467
|
};
|
|
403
468
|
|
|
@@ -410,10 +475,28 @@ export const createCursorAdapter = ({
|
|
|
410
475
|
checkpointEvent: 'milestone',
|
|
411
476
|
};
|
|
412
477
|
}
|
|
478
|
+
} else if (
|
|
479
|
+
!checkpoint.matched
|
|
480
|
+
&& touchedFiles.length === 0
|
|
481
|
+
&& meaningfulReadCount >= READ_CHECKPOINT_THRESHOLD
|
|
482
|
+
&& Date.now() - (nextState.lastReadCheckpointAt ?? 0) >= READ_CHECKPOINT_THROTTLE_MS
|
|
483
|
+
) {
|
|
484
|
+
const autoAppended = await maybeAutoCheckpointFromState(nextState, {
|
|
485
|
+
trigger: 'read_progress',
|
|
486
|
+
mode: 'read_progress',
|
|
487
|
+
});
|
|
488
|
+
if (autoAppended) {
|
|
489
|
+
nextState = {
|
|
490
|
+
...nextState,
|
|
491
|
+
readFiles: [],
|
|
492
|
+
meaningfulReadCount: 0,
|
|
493
|
+
lastReadCheckpointAt: Date.now(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
413
496
|
}
|
|
414
497
|
|
|
415
498
|
await maybeSetTrackedTurnState({ hookKey, state: nextState });
|
|
416
|
-
if (checkpoint.matched || touchedFiles.length > 0) {
|
|
499
|
+
if (checkpoint.matched || touchedFiles.length > 0 || readPaths.length > 0) {
|
|
417
500
|
await recordHookMetrics({
|
|
418
501
|
action: 'PostToolUse',
|
|
419
502
|
sessionId: existing.projectSessionId,
|
|
@@ -3,6 +3,11 @@ import { smartSearch } from '../../tools/smart-search.js';
|
|
|
3
3
|
|
|
4
4
|
export const SAFE_CONTINUITY_STATES = new Set(['aligned', 'resume']);
|
|
5
5
|
|
|
6
|
+
export const READ_TOOLS = new Set(['Read', 'Grep', 'Glob', 'SemanticSearch']);
|
|
7
|
+
export const READ_CHECKPOINT_THRESHOLD = 8;
|
|
8
|
+
export const READ_CHECKPOINT_THROTTLE_MS = 60_000;
|
|
9
|
+
export const MAX_READ_FILES = 12;
|
|
10
|
+
|
|
6
11
|
export const MAX_TOP_FILES = 3;
|
|
7
12
|
export const MAX_PREFLIGHT_HINTS = 2;
|
|
8
13
|
export const MAX_FOCUS_LENGTH = 140;
|
|
@@ -63,6 +68,29 @@ export const truncate = (value, maxLength = DEFAULT_TRUNCATE_LENGTH) => {
|
|
|
63
68
|
|
|
64
69
|
const asArray = (value) => Array.isArray(value) ? value : [];
|
|
65
70
|
|
|
71
|
+
const isMeaningfulPath = (value) => typeof value === 'string' && value.trim().length > 1;
|
|
72
|
+
|
|
73
|
+
export const extractReadPathsFromToolUse = ({ toolName, toolInput } = {}) => {
|
|
74
|
+
if (!toolName || !READ_TOOLS.has(toolName) || !toolInput || typeof toolInput !== 'object') {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const candidates = [];
|
|
79
|
+
|
|
80
|
+
if (toolName === 'Read') {
|
|
81
|
+
candidates.push(toolInput.path, toolInput.file_path, toolInput.filePath);
|
|
82
|
+
} else if (toolName === 'Grep' || toolName === 'SemanticSearch') {
|
|
83
|
+
candidates.push(toolInput.path);
|
|
84
|
+
if (Array.isArray(toolInput.target_directories)) {
|
|
85
|
+
candidates.push(...toolInput.target_directories);
|
|
86
|
+
}
|
|
87
|
+
} else if (toolName === 'Glob') {
|
|
88
|
+
candidates.push(toolInput.target_directory);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return [...new Set(candidates.filter(isMeaningfulPath))];
|
|
92
|
+
};
|
|
93
|
+
|
|
66
94
|
export const uniqueCompact = (values) => [...new Set(
|
|
67
95
|
asArray(values)
|
|
68
96
|
.map((value) => normalizeWhitespace(value))
|
package/src/server.js
CHANGED
|
@@ -41,6 +41,7 @@ smart_turn (session continuity — read this before calling):
|
|
|
41
41
|
- START: phase "start". Pass userPrompt (current goal). ensureSession true recommended when you want persistence. Use at the beginning of substantial work or when resuming after a break — not for one-line fixes or single-shot questions.
|
|
42
42
|
- END: phase "end". Pass event: milestone | blocker | task_complete. Pass sessionId if you have it; include update (nextStep, completed, etc.) when checkpointing progress. Call after a meaningful slice of work (close a phase), not after every trivial edit.
|
|
43
43
|
- SKIP smart_turn entirely for trivial or same-session point tasks (the tool schema also warns about this).
|
|
44
|
+
- smart_resume is the cheap shortcut for the first prompt of a substantial task — equivalent to smart_turn(start, ensureSession=true, verbosity=minimal).
|
|
44
45
|
|
|
45
46
|
Source of truth: devctx does not replace git history, PRs, or repo docs (e.g. MIGRATION.md). If end was not called or work was not committed, those remain authoritative.
|
|
46
47
|
|
|
@@ -508,8 +509,9 @@ export const createDevctxServer = () => {
|
|
|
508
509
|
includeMetrics: z.boolean().optional(),
|
|
509
510
|
metricsWindow: z.enum(['24h', '7d', '30d', 'all']).optional(),
|
|
510
511
|
latestMetrics: z.number().int().min(1).max(20).optional(),
|
|
512
|
+
verbosity: z.enum(['minimal', 'standard', 'full']).optional().describe('Default "minimal" — returns compact recommendedPath/continuity/task. Use "standard" or "full" only when you need long instructions, candidates, or full checkpoint diagnostics.'),
|
|
511
513
|
},
|
|
512
|
-
async ({ phase, sessionId, prompt, update, event, force, maxTokens, ensureSession, includeMetrics, metricsWindow, latestMetrics }) =>
|
|
514
|
+
async ({ phase, sessionId, prompt, update, event, force, maxTokens, ensureSession, includeMetrics, metricsWindow, latestMetrics, verbosity }) =>
|
|
513
515
|
asTextResult(await smartTurn({
|
|
514
516
|
phase,
|
|
515
517
|
sessionId,
|
|
@@ -522,6 +524,29 @@ export const createDevctxServer = () => {
|
|
|
522
524
|
includeMetrics,
|
|
523
525
|
metricsWindow,
|
|
524
526
|
latestMetrics,
|
|
527
|
+
verbosity,
|
|
528
|
+
})),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
server.tool(
|
|
532
|
+
'smart_resume',
|
|
533
|
+
'Lightweight entry point for the first prompt of a substantial task. Equivalent to smart_turn(phase=start, ensureSession=true, verbosity=minimal): rehydrates the most recent persisted session for this project, classifies prompt continuity, and returns a compact recommendedPath. Prefer this over smart_turn(start) when you just want to recover context cheaply at the beginning of a session. SKIP for one-off lookups, single-line fixes, or trivial questions where re-reading is faster than rehydration.',
|
|
534
|
+
{
|
|
535
|
+
prompt: z.string().optional(),
|
|
536
|
+
sessionId: z.string().optional(),
|
|
537
|
+
taskId: z.string().optional(),
|
|
538
|
+
maxTokens: z.number().int().min(100).max(2000).optional(),
|
|
539
|
+
verbosity: z.enum(['minimal', 'standard', 'full']).optional(),
|
|
540
|
+
},
|
|
541
|
+
async ({ prompt, sessionId, taskId, maxTokens, verbosity }) =>
|
|
542
|
+
asTextResult(await smartTurn({
|
|
543
|
+
phase: 'start',
|
|
544
|
+
prompt,
|
|
545
|
+
sessionId,
|
|
546
|
+
taskId,
|
|
547
|
+
maxTokens,
|
|
548
|
+
ensureSession: true,
|
|
549
|
+
verbosity: verbosity ?? 'minimal',
|
|
525
550
|
})),
|
|
526
551
|
);
|
|
527
552
|
|
package/src/storage/sqlite.js
CHANGED
|
@@ -511,6 +511,16 @@ export const getMeta = (db, key) => {
|
|
|
511
511
|
return row?.value ?? null;
|
|
512
512
|
};
|
|
513
513
|
|
|
514
|
+
const DEFAULT_GC_RETENTION_DAYS = 30;
|
|
515
|
+
const DEFAULT_GC_THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
516
|
+
const STORAGE_GC_META_KEY = 'last_storage_gc_at';
|
|
517
|
+
|
|
518
|
+
const sanitizeRetentionDays = (value) => {
|
|
519
|
+
const parsed = Number(value);
|
|
520
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_GC_RETENTION_DAYS;
|
|
521
|
+
return Math.min(365, Math.max(1, Math.floor(parsed)));
|
|
522
|
+
};
|
|
523
|
+
|
|
514
524
|
const getSchemaVersion = (db) => Number(getMeta(db, 'schema_version') ?? 0);
|
|
515
525
|
const VALID_STATUSES = new Set(['planning', 'in_progress', 'blocked', 'completed']);
|
|
516
526
|
|
|
@@ -1456,6 +1466,42 @@ export const upsertAgentRun = async ({ filePath = getStateDbPath(), runId, taskI
|
|
|
1456
1466
|
};
|
|
1457
1467
|
}, { filePath });
|
|
1458
1468
|
|
|
1469
|
+
export const runStorageMaintenance = async ({
|
|
1470
|
+
filePath = getStateDbPath(),
|
|
1471
|
+
retentionDays = DEFAULT_GC_RETENTION_DAYS,
|
|
1472
|
+
throttleMs = DEFAULT_GC_THROTTLE_MS,
|
|
1473
|
+
force = false,
|
|
1474
|
+
} = {}) => withStateDb((db) => {
|
|
1475
|
+
const now = Date.now();
|
|
1476
|
+
const lastRun = Number(getMeta(db, STORAGE_GC_META_KEY) ?? 0);
|
|
1477
|
+
if (!force && now - lastRun < throttleMs) {
|
|
1478
|
+
return { skipped: true, reason: 'throttled', lastRunAt: lastRun || null };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const days = sanitizeRetentionDays(retentionDays);
|
|
1482
|
+
const cutoffIso = new Date(now - days * 24 * 60 * 60 * 1000).toISOString();
|
|
1483
|
+
|
|
1484
|
+
const removeOlder = (sql) => db.prepare(sql).run(cutoffIso).changes;
|
|
1485
|
+
|
|
1486
|
+
const removed = {
|
|
1487
|
+
metricsEvents: removeOlder('DELETE FROM metrics_events WHERE created_at < ?'),
|
|
1488
|
+
sessionEvents: removeOlder('DELETE FROM session_events WHERE created_at < ?'),
|
|
1489
|
+
taskHandoffs: removeOlder('DELETE FROM task_handoffs WHERE created_at < ?'),
|
|
1490
|
+
agentRuns: removeOlder('DELETE FROM agent_runs WHERE updated_at < ?'),
|
|
1491
|
+
workflowMetrics: removeOlder('DELETE FROM workflow_metrics WHERE created_at < ?'),
|
|
1492
|
+
contextAccess: removeOlder('DELETE FROM context_access WHERE timestamp < ?'),
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
setMeta(db, STORAGE_GC_META_KEY, String(now));
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
skipped: false,
|
|
1499
|
+
runAt: now,
|
|
1500
|
+
retentionDays: days,
|
|
1501
|
+
removed,
|
|
1502
|
+
};
|
|
1503
|
+
}, { filePath });
|
|
1504
|
+
|
|
1459
1505
|
const listLegacySessionFiles = (sessionsDir) => {
|
|
1460
1506
|
if (!fs.existsSync(sessionsDir)) {
|
|
1461
1507
|
return [];
|
|
@@ -332,30 +332,21 @@ const buildCompactResult = (groups, totalMatches, query, root, searchMode, prove
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
const modeLabel = searchMode === 'exact' ? '' : searchMode === 'regex' ? ' [regex fallback]' : ` [term expansion: ${(provenance?.expandedTerms ?? []).join(', ')}]`;
|
|
335
|
-
|
|
336
335
|
const topGroups = groups.slice(0, MAX_RESULT_FILES);
|
|
337
336
|
|
|
338
337
|
if (totalMatches <= 20) {
|
|
339
|
-
const header = modeLabel ? `# Search mode:${modeLabel}\n
|
|
338
|
+
const header = modeLabel ? `# Search mode:${modeLabel}\n` : '';
|
|
340
339
|
return header + topGroups
|
|
341
340
|
.flatMap((group) => group.matches)
|
|
342
341
|
.map(formatMatch)
|
|
343
342
|
.join('\n');
|
|
344
343
|
}
|
|
345
344
|
|
|
346
|
-
const lines = [
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
'',
|
|
350
|
-
'# Top files',
|
|
351
|
-
];
|
|
352
|
-
|
|
353
|
-
for (const group of topGroups.slice(0, 10)) {
|
|
354
|
-
lines.push(`${group.count} match(es), score ${group.score} :: ${group.file}`);
|
|
345
|
+
const lines = [];
|
|
346
|
+
if (modeLabel) {
|
|
347
|
+
lines.push(`# Search mode:${modeLabel}`);
|
|
355
348
|
}
|
|
356
349
|
|
|
357
|
-
lines.push('', '# Sample matches');
|
|
358
|
-
|
|
359
350
|
const topScore = topGroups[0]?.score ?? 0;
|
|
360
351
|
for (const group of topGroups.slice(0, 5)) {
|
|
361
352
|
const linesPerFile = group.score >= topScore * 0.7 ? 5 : 2;
|
|
@@ -366,7 +357,7 @@ const buildCompactResult = (groups, totalMatches, query, root, searchMode, prove
|
|
|
366
357
|
|
|
367
358
|
const fileCount = totalFiles ?? groups.length;
|
|
368
359
|
if (fileCount > 30) {
|
|
369
|
-
lines.push(
|
|
360
|
+
lines.push(`# Note: ${fileCount} files matched — query may be too broad. Use Grep for exact pattern matching.`);
|
|
370
361
|
}
|
|
371
362
|
|
|
372
363
|
return lines.join('\n');
|
|
@@ -535,7 +526,7 @@ export const smartSearch = async ({ query, cwd = '.', intent, maxFiles, _testFor
|
|
|
535
526
|
totalMatches: dedupedMatches.length,
|
|
536
527
|
matchedFiles: cappedGroups.length,
|
|
537
528
|
...(groups.length > cappedGroups.length ? { totalFiles: groups.length } : {}),
|
|
538
|
-
topFiles: cappedGroups.slice(0,
|
|
529
|
+
topFiles: cappedGroups.slice(0, 5).map((group) => ({ file: group.file, count: group.count, score: group.score })),
|
|
539
530
|
matches: compressedText,
|
|
540
531
|
};
|
|
541
532
|
|
package/src/tools/smart-shell.js
CHANGED
|
@@ -185,6 +185,19 @@ const MAX_DIFF_FILES = 8;
|
|
|
185
185
|
const MAX_LINES_PER_FILE = 60;
|
|
186
186
|
const DIFF_TOTAL_LIMIT = 4000;
|
|
187
187
|
|
|
188
|
+
const TAP_HEADER = /^TAP version/;
|
|
189
|
+
const TAP_OK_LINE = /^\s*ok\s+\d+/;
|
|
190
|
+
const TAP_NOT_OK_LINE = /^\s*not ok\s+\d+/;
|
|
191
|
+
const TAP_SUBTEST_LINE = /^\s*#\s+Subtest:/;
|
|
192
|
+
const TAP_YAML_FENCE = /^\s*(---|\.\.\.)\s*$/;
|
|
193
|
+
const TAP_SUMMARY_LINE = /^\s*#\s+(tests|pass|fail|skipped|todo|duration_ms|cancelled|suites)\b/;
|
|
194
|
+
const TAP_DETECT = /(^|\n)(TAP version |# tests \d+)/;
|
|
195
|
+
const TAP_FAIL_CONTEXT = 4;
|
|
196
|
+
|
|
197
|
+
const GIT_LOG_COMMIT_HEADER = /^commit [0-9a-f]{40}/;
|
|
198
|
+
const GIT_LOG_DETECT = /^commit [0-9a-f]{40}\nAuthor:/m;
|
|
199
|
+
const MAX_GIT_LOG_COMMITS = 40;
|
|
200
|
+
|
|
188
201
|
const splitDiffByFile = (text) => {
|
|
189
202
|
const files = [];
|
|
190
203
|
let current = null;
|
|
@@ -201,6 +214,95 @@ const splitDiffByFile = (text) => {
|
|
|
201
214
|
return files;
|
|
202
215
|
};
|
|
203
216
|
|
|
217
|
+
export const compressTapOutput = (text) => {
|
|
218
|
+
const lines = text.split('\n');
|
|
219
|
+
const summary = [];
|
|
220
|
+
const failures = [];
|
|
221
|
+
const headerLines = [];
|
|
222
|
+
let inYaml = false;
|
|
223
|
+
let yamlFailureBuffer = null;
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
const line = lines[i];
|
|
227
|
+
|
|
228
|
+
if (TAP_HEADER.test(line) && headerLines.length === 0) {
|
|
229
|
+
headerLines.push(line);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (TAP_SUMMARY_LINE.test(line)) {
|
|
234
|
+
summary.push(line);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (TAP_NOT_OK_LINE.test(line)) {
|
|
239
|
+
failures.push(line);
|
|
240
|
+
yamlFailureBuffer = failures;
|
|
241
|
+
inYaml = false;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (TAP_YAML_FENCE.test(line)) {
|
|
246
|
+
if (yamlFailureBuffer) yamlFailureBuffer.push(line);
|
|
247
|
+
inYaml = !inYaml;
|
|
248
|
+
if (!inYaml) yamlFailureBuffer = null;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (inYaml && yamlFailureBuffer) {
|
|
253
|
+
if (yamlFailureBuffer.length - failures.indexOf(yamlFailureBuffer[0]) <= TAP_FAIL_CONTEXT + 6) {
|
|
254
|
+
yamlFailureBuffer.push(line);
|
|
255
|
+
}
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (TAP_OK_LINE.test(line) || TAP_SUBTEST_LINE.test(line)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (line.trim().startsWith('#') && failures.length > 0 && failures.length < 200) {
|
|
264
|
+
failures.push(line);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const parts = [];
|
|
269
|
+
if (headerLines.length > 0) parts.push(headerLines.join('\n'));
|
|
270
|
+
if (failures.length > 0) {
|
|
271
|
+
parts.push(`# ${failures.filter((l) => TAP_NOT_OK_LINE.test(l)).length} failure(s):`);
|
|
272
|
+
parts.push(failures.join('\n'));
|
|
273
|
+
} else {
|
|
274
|
+
parts.push('# all tests passed (ok lines collapsed)');
|
|
275
|
+
}
|
|
276
|
+
if (summary.length > 0) parts.push(summary.join('\n'));
|
|
277
|
+
|
|
278
|
+
return parts.join('\n');
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const compressGitLog = (text) => {
|
|
282
|
+
const lines = text.split('\n');
|
|
283
|
+
const commits = [];
|
|
284
|
+
let current = null;
|
|
285
|
+
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
if (GIT_LOG_COMMIT_HEADER.test(line)) {
|
|
288
|
+
if (current) commits.push(current);
|
|
289
|
+
current = { sha: line.split(' ')[1], subject: '' };
|
|
290
|
+
} else if (current && !current.subject && line.trim() && !/^(Author|Date|Merge|commit):/i.test(line)) {
|
|
291
|
+
current.subject = line.trim();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (current) commits.push(current);
|
|
295
|
+
|
|
296
|
+
if (commits.length === 0) return text;
|
|
297
|
+
|
|
298
|
+
const shown = commits.slice(0, MAX_GIT_LOG_COMMITS);
|
|
299
|
+
const skipped = commits.length - shown.length;
|
|
300
|
+
const body = shown.map(({ sha, subject }) => `${sha.slice(0, 7)} ${subject}`).join('\n');
|
|
301
|
+
return skipped > 0
|
|
302
|
+
? `${body}\n# ${skipped} more commit(s) not shown — narrow with --since/--until or git log -n <N>`
|
|
303
|
+
: body;
|
|
304
|
+
};
|
|
305
|
+
|
|
204
306
|
const compressDiff = (text) => {
|
|
205
307
|
if (!DIFF_FILE_HEADER.test(text)) return text;
|
|
206
308
|
|
|
@@ -301,8 +403,20 @@ export const smartShell = async ({ command }) => {
|
|
|
301
403
|
'entity not found',
|
|
302
404
|
]);
|
|
303
405
|
const shouldPrioritizeRelevant = execution.code !== 0 || execution.timedOut;
|
|
304
|
-
const
|
|
305
|
-
const
|
|
406
|
+
const isTap = TAP_DETECT.test(rawText);
|
|
407
|
+
const isGitLog = !isTap && GIT_LOG_DETECT.test(rawText);
|
|
408
|
+
let stagedCompression;
|
|
409
|
+
|
|
410
|
+
if (isTap) {
|
|
411
|
+
stagedCompression = compressTapOutput(rawText);
|
|
412
|
+
} else if (isGitLog) {
|
|
413
|
+
stagedCompression = compressGitLog(rawText);
|
|
414
|
+
} else {
|
|
415
|
+
const compressedSource = shouldPrioritizeRelevant && relevant ? relevant : rawText;
|
|
416
|
+
stagedCompression = compressDiff(uniqueLines(compressedSource));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const compressedText = truncate(stagedCompression, 5000);
|
|
306
420
|
const metrics = buildMetrics({
|
|
307
421
|
tool: 'smart_shell',
|
|
308
422
|
target: command,
|
|
@@ -19,6 +19,16 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const MAX_SESSION_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
21
21
|
const DEFAULT_MAX_TOKENS = 500;
|
|
22
|
+
const ROLLING_LIMIT = 30;
|
|
23
|
+
const ROLLING_KEEP = 20;
|
|
24
|
+
|
|
25
|
+
const applyRollingWindow = (items) => {
|
|
26
|
+
if (!Array.isArray(items) || items.length <= ROLLING_LIMIT) {
|
|
27
|
+
return { items: Array.isArray(items) ? items : [], archived: 0 };
|
|
28
|
+
}
|
|
29
|
+
const archived = items.length - ROLLING_KEEP;
|
|
30
|
+
return { items: items.slice(-ROLLING_KEEP), archived };
|
|
31
|
+
};
|
|
22
32
|
const VALID_STATUSES = new Set(['planning', 'in_progress', 'blocked', 'completed']);
|
|
23
33
|
const ACTIVE_STATUSES = new Set(['planning', 'in_progress', 'blocked']);
|
|
24
34
|
const DEFAULT_STATUS = 'in_progress';
|
|
@@ -735,6 +745,65 @@ const getLatestTaskHandoffRow = (db, taskId) => db.prepare(`
|
|
|
735
745
|
LIMIT 1
|
|
736
746
|
`).get(taskId);
|
|
737
747
|
|
|
748
|
+
const HANDOFF_FALLBACK_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
749
|
+
|
|
750
|
+
const getMostRecentTaskHandoffRow = (db) => db.prepare(`
|
|
751
|
+
SELECT
|
|
752
|
+
handoff_id,
|
|
753
|
+
task_id,
|
|
754
|
+
session_id,
|
|
755
|
+
from_agent_id,
|
|
756
|
+
to_agent_id,
|
|
757
|
+
trigger,
|
|
758
|
+
summary_json,
|
|
759
|
+
created_at
|
|
760
|
+
FROM task_handoffs
|
|
761
|
+
ORDER BY datetime(created_at) DESC, handoff_id DESC
|
|
762
|
+
LIMIT 1
|
|
763
|
+
`).get();
|
|
764
|
+
|
|
765
|
+
const buildSummaryFromHandoff = (handoff, taskRecord = null) => {
|
|
766
|
+
const summary = handoff?.summary ?? {};
|
|
767
|
+
const touchedFiles = mergeUniqueStrings(summary.touchedFiles);
|
|
768
|
+
const pending = mergeUniqueStrings(summary.pending);
|
|
769
|
+
|
|
770
|
+
return pruneEmptyFields({
|
|
771
|
+
status: normalizeStatus(taskRecord?.status, 'in_progress'),
|
|
772
|
+
nextStep: isMeaningfulString(summary.nextStep) ? summary.nextStep : undefined,
|
|
773
|
+
currentFocus: isMeaningfulString(summary.currentFocus) ? summary.currentFocus : undefined,
|
|
774
|
+
goal: isMeaningfulString(taskRecord?.canonicalGoal) ? taskRecord.canonicalGoal : undefined,
|
|
775
|
+
pinnedContext: pending.slice(0, 3),
|
|
776
|
+
hotFiles: uniqueTail(touchedFiles.map(compactFilePath), 5),
|
|
777
|
+
touchedFilesCount: touchedFiles.length,
|
|
778
|
+
completedCount: 0,
|
|
779
|
+
decisionsCount: 0,
|
|
780
|
+
});
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
const buildHandoffFallback = (db) => {
|
|
784
|
+
const handoffRow = getMostRecentTaskHandoffRow(db);
|
|
785
|
+
if (!handoffRow) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const ageMs = Date.now() - getTimestamp(handoffRow.created_at, 0);
|
|
790
|
+
if (!Number.isFinite(ageMs) || ageMs > HANDOFF_FALLBACK_MAX_AGE_MS) {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const handoff = normalizeTaskHandoff(handoffRow);
|
|
795
|
+
const taskRecord = handoff?.taskId ? normalizeTaskRow(getTaskRow(db, handoff.taskId)) : null;
|
|
796
|
+
const summary = buildSummaryFromHandoff(handoff, taskRecord);
|
|
797
|
+
const tokens = countTokens(JSON.stringify(summary));
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
summary,
|
|
801
|
+
tokens,
|
|
802
|
+
handoff,
|
|
803
|
+
task: taskRecord,
|
|
804
|
+
};
|
|
805
|
+
};
|
|
806
|
+
|
|
738
807
|
const normalizeTaskHandoff = (row) => {
|
|
739
808
|
if (!row) {
|
|
740
809
|
return null;
|
|
@@ -761,6 +830,13 @@ const hydrateSession = (row) => {
|
|
|
761
830
|
const completed = mergeUniqueStrings(snapshot.completed);
|
|
762
831
|
const decisions = mergeUniqueStrings(snapshot.decisions);
|
|
763
832
|
const touchedFiles = mergeUniqueStrings(snapshot.touchedFiles);
|
|
833
|
+
const archivedCounts = snapshot.archivedCounts && typeof snapshot.archivedCounts === 'object'
|
|
834
|
+
? {
|
|
835
|
+
completed: Number(snapshot.archivedCounts.completed ?? 0) || 0,
|
|
836
|
+
decisions: Number(snapshot.archivedCounts.decisions ?? 0) || 0,
|
|
837
|
+
touchedFiles: Number(snapshot.archivedCounts.touchedFiles ?? 0) || 0,
|
|
838
|
+
}
|
|
839
|
+
: { completed: 0, decisions: 0, touchedFiles: 0 };
|
|
764
840
|
const pinnedContext = mergeUniqueStrings(parseJsonText(row.pinned_context_json, []), snapshot.pinnedContext);
|
|
765
841
|
const unresolvedQuestions = mergeUniqueStrings(parseJsonText(row.unresolved_questions_json, []), snapshot.unresolvedQuestions);
|
|
766
842
|
const blockers = mergeUniqueStrings(parseJsonText(row.blockers_json, []), snapshot.blockers);
|
|
@@ -784,9 +860,10 @@ const hydrateSession = (row) => {
|
|
|
784
860
|
completed,
|
|
785
861
|
decisions,
|
|
786
862
|
touchedFiles,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
863
|
+
archivedCounts,
|
|
864
|
+
completedCount: Number.isInteger(row.completed_count) ? row.completed_count : completed.length + archivedCounts.completed,
|
|
865
|
+
decisionsCount: Number.isInteger(row.decisions_count) ? row.decisions_count : decisions.length + archivedCounts.decisions,
|
|
866
|
+
touchedFilesCount: Number.isInteger(row.touched_files_count) ? row.touched_files_count : touchedFiles.length + archivedCounts.touchedFiles,
|
|
790
867
|
createdAt: row.created_at,
|
|
791
868
|
updatedAt: row.updated_at,
|
|
792
869
|
};
|
|
@@ -821,7 +898,7 @@ const buildSessionSummary = (session) => {
|
|
|
821
898
|
const compressSummary = (data, maxTokens) => {
|
|
822
899
|
const baseSummary = buildSessionSummary(data);
|
|
823
900
|
let compressed = baseSummary;
|
|
824
|
-
let summary = JSON.stringify(compressed
|
|
901
|
+
let summary = JSON.stringify(compressed);
|
|
825
902
|
let tokens = countTokens(summary);
|
|
826
903
|
|
|
827
904
|
if (tokens <= maxTokens) {
|
|
@@ -830,7 +907,7 @@ const compressSummary = (data, maxTokens) => {
|
|
|
830
907
|
|
|
831
908
|
const recomputeTokens = () => {
|
|
832
909
|
compressed = pruneEmptyFields(compressed);
|
|
833
|
-
summary = JSON.stringify(compressed
|
|
910
|
+
summary = JSON.stringify(compressed);
|
|
834
911
|
tokens = countTokens(summary);
|
|
835
912
|
};
|
|
836
913
|
|
|
@@ -1005,13 +1082,23 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
|
|
|
1005
1082
|
branchName: data.branchName ?? existing?.branchName ?? null,
|
|
1006
1083
|
worktreePath: data.worktreePath ?? existing?.worktreePath ?? null,
|
|
1007
1084
|
});
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
1010
|
-
const
|
|
1085
|
+
const completedRoll = applyRollingWindow(mergeUniqueStrings(data.completed));
|
|
1086
|
+
const decisionsRoll = applyRollingWindow(mergeUniqueStrings(data.decisions));
|
|
1087
|
+
const touchedRoll = applyRollingWindow(mergeUniqueStrings(data.touchedFiles));
|
|
1088
|
+
const completed = completedRoll.items;
|
|
1089
|
+
const decisions = decisionsRoll.items;
|
|
1090
|
+
const touchedFiles = touchedRoll.items;
|
|
1011
1091
|
const pinnedContext = mergeUniqueStrings(data.pinnedContext);
|
|
1012
1092
|
const unresolvedQuestions = mergeUniqueStrings(data.unresolvedQuestions);
|
|
1013
1093
|
const blockers = mergeUniqueStrings(data.blockers);
|
|
1014
1094
|
|
|
1095
|
+
const previousArchived = existing?.archivedCounts ?? {};
|
|
1096
|
+
const archivedCounts = {
|
|
1097
|
+
completed: (previousArchived.completed ?? 0) + completedRoll.archived,
|
|
1098
|
+
decisions: (previousArchived.decisions ?? 0) + decisionsRoll.archived,
|
|
1099
|
+
touchedFiles: (previousArchived.touchedFiles ?? 0) + touchedRoll.archived,
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1015
1102
|
const snapshot = {
|
|
1016
1103
|
taskId: task.taskId,
|
|
1017
1104
|
agentId: data.agentId ?? existing?.agentId ?? null,
|
|
@@ -1028,9 +1115,10 @@ const saveSession = (db, sessionId, data, { action, eventPayload } = {}) => {
|
|
|
1028
1115
|
blockers,
|
|
1029
1116
|
nextStep: data.nextStep ?? '',
|
|
1030
1117
|
touchedFiles,
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1118
|
+
archivedCounts,
|
|
1119
|
+
completedCount: completed.length + archivedCounts.completed,
|
|
1120
|
+
decisionsCount: decisions.length + archivedCounts.decisions,
|
|
1121
|
+
touchedFilesCount: touchedFiles.length + archivedCounts.touchedFiles,
|
|
1034
1122
|
schemaVersion: SQLITE_SCHEMA_VERSION,
|
|
1035
1123
|
sessionId,
|
|
1036
1124
|
createdAt,
|
|
@@ -1446,6 +1534,23 @@ export const smartSummary = async ({
|
|
|
1446
1534
|
cleanup: allowReadSideEffects,
|
|
1447
1535
|
});
|
|
1448
1536
|
if (!resolution.found) {
|
|
1537
|
+
const handoffFallback = buildHandoffFallback(db);
|
|
1538
|
+
if (handoffFallback) {
|
|
1539
|
+
return addRepoSafety({
|
|
1540
|
+
action: 'get',
|
|
1541
|
+
sessionId: null,
|
|
1542
|
+
found: true,
|
|
1543
|
+
recoveredFromHandoff: true,
|
|
1544
|
+
autoResumed: false,
|
|
1545
|
+
ambiguous: false,
|
|
1546
|
+
summary: handoffFallback.summary,
|
|
1547
|
+
tokens: handoffFallback.tokens,
|
|
1548
|
+
compressionLevel: 'handoff',
|
|
1549
|
+
...(handoffFallback.task ? { task: handoffFallback.task } : {}),
|
|
1550
|
+
handoff: handoffFallback.handoff,
|
|
1551
|
+
message: 'Recovered from latest task handoff (no live session).',
|
|
1552
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1553
|
+
}
|
|
1449
1554
|
return addRepoSafety({
|
|
1450
1555
|
action: 'get',
|
|
1451
1556
|
sessionId: null,
|
|
@@ -1463,6 +1568,21 @@ export const smartSummary = async ({
|
|
|
1463
1568
|
}
|
|
1464
1569
|
|
|
1465
1570
|
if (!targetSessionId) {
|
|
1571
|
+
const handoffFallback = buildHandoffFallback(db);
|
|
1572
|
+
if (handoffFallback) {
|
|
1573
|
+
return addRepoSafety({
|
|
1574
|
+
action: 'get',
|
|
1575
|
+
sessionId: null,
|
|
1576
|
+
found: true,
|
|
1577
|
+
recoveredFromHandoff: true,
|
|
1578
|
+
summary: handoffFallback.summary,
|
|
1579
|
+
tokens: handoffFallback.tokens,
|
|
1580
|
+
compressionLevel: 'handoff',
|
|
1581
|
+
...(handoffFallback.task ? { task: handoffFallback.task } : {}),
|
|
1582
|
+
handoff: handoffFallback.handoff,
|
|
1583
|
+
message: 'Recovered from latest task handoff (no live session).',
|
|
1584
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1585
|
+
}
|
|
1466
1586
|
return addRepoSafety({
|
|
1467
1587
|
action: 'get',
|
|
1468
1588
|
sessionId: null,
|
|
@@ -1473,6 +1593,21 @@ export const smartSummary = async ({
|
|
|
1473
1593
|
|
|
1474
1594
|
const session = hydrateSession(getSessionRow(db, targetSessionId));
|
|
1475
1595
|
if (!session) {
|
|
1596
|
+
const handoffFallback = buildHandoffFallback(db);
|
|
1597
|
+
if (handoffFallback) {
|
|
1598
|
+
return addRepoSafety({
|
|
1599
|
+
action: 'get',
|
|
1600
|
+
sessionId: targetSessionId,
|
|
1601
|
+
found: true,
|
|
1602
|
+
recoveredFromHandoff: true,
|
|
1603
|
+
summary: handoffFallback.summary,
|
|
1604
|
+
tokens: handoffFallback.tokens,
|
|
1605
|
+
compressionLevel: 'handoff',
|
|
1606
|
+
...(handoffFallback.task ? { task: handoffFallback.task } : {}),
|
|
1607
|
+
handoff: handoffFallback.handoff,
|
|
1608
|
+
message: 'Session not found; recovered context from latest task handoff.',
|
|
1609
|
+
}, mutationSafety.repoSafety, suppressReadSideEffects);
|
|
1610
|
+
}
|
|
1476
1611
|
return addRepoSafety({
|
|
1477
1612
|
action: 'get',
|
|
1478
1613
|
sessionId: targetSessionId,
|
package/src/tools/smart-turn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildIndexIncremental, persistIndex } from '../index.js';
|
|
2
|
+
import { triggerBackgroundIndexBuild } from '../index-manager.js';
|
|
2
3
|
import { projectRoot } from '../utils/runtime-config.js';
|
|
3
4
|
import {
|
|
4
5
|
autoTrackWorkflow,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
isWorkflowTrackingEnabled,
|
|
8
9
|
} from '../workflow-tracker.js';
|
|
9
10
|
import { persistMetrics } from '../metrics.js';
|
|
11
|
+
import { runStorageMaintenance } from '../storage/sqlite.js';
|
|
10
12
|
import { PRODUCT_QUALITY_ANALYTICS_KIND } from '../analytics/product-quality.js';
|
|
11
13
|
import { attachSafetyMetadata, buildMutationSafety } from '../utils/mutation-safety.js';
|
|
12
14
|
import { smartContext } from './smart-context.js';
|
|
@@ -16,6 +18,12 @@ import { smartSummary } from './smart-summary.js';
|
|
|
16
18
|
const isStorageUnhealthy = (health) =>
|
|
17
19
|
health && health.status !== 'ok' && health.status !== null && health.status !== undefined;
|
|
18
20
|
|
|
21
|
+
const VALID_VERBOSITIES = new Set(['minimal', 'standard', 'full']);
|
|
22
|
+
const DEFAULT_VERBOSITY = 'minimal';
|
|
23
|
+
|
|
24
|
+
const normalizeVerbosity = (value) =>
|
|
25
|
+
typeof value === 'string' && VALID_VERBOSITIES.has(value) ? value : DEFAULT_VERBOSITY;
|
|
26
|
+
|
|
19
27
|
const DEFAULT_START_MAX_TOKENS = 400;
|
|
20
28
|
const DEFAULT_END_MAX_TOKENS = 500;
|
|
21
29
|
const DEFAULT_END_EVENT = 'milestone';
|
|
@@ -253,6 +261,7 @@ const buildStartRecommendedPath = ({
|
|
|
253
261
|
mutationSafety,
|
|
254
262
|
autoCreated,
|
|
255
263
|
isolatedSession,
|
|
264
|
+
verbosity = DEFAULT_VERBOSITY,
|
|
256
265
|
}) => {
|
|
257
266
|
const nextTools = [];
|
|
258
267
|
const steps = [];
|
|
@@ -310,15 +319,23 @@ const buildStartRecommendedPath = ({
|
|
|
310
319
|
));
|
|
311
320
|
}
|
|
312
321
|
|
|
322
|
+
const mode = mutationSafety?.blocked
|
|
323
|
+
? 'blocked_guided'
|
|
324
|
+
: refreshedContext
|
|
325
|
+
? 'guided_refresh'
|
|
326
|
+
: hasMeaningfulPrompt(prompt)
|
|
327
|
+
? 'guided_context'
|
|
328
|
+
: 'lightweight';
|
|
329
|
+
const dedupedTools = [...new Set(nextTools)];
|
|
330
|
+
const next = steps[0] ? `${steps[0].tool}: ${steps[0].instruction}` : (dedupedTools[0] ?? '');
|
|
331
|
+
|
|
332
|
+
if (verbosity === 'minimal') {
|
|
333
|
+
return { phase: 'start', mode, nextTools: dedupedTools, next };
|
|
334
|
+
}
|
|
335
|
+
|
|
313
336
|
return {
|
|
314
337
|
phase: 'start',
|
|
315
|
-
mode
|
|
316
|
-
? 'blocked_guided'
|
|
317
|
-
: refreshedContext
|
|
318
|
-
? 'guided_refresh'
|
|
319
|
-
: hasMeaningfulPrompt(prompt)
|
|
320
|
-
? 'guided_context'
|
|
321
|
-
: 'lightweight',
|
|
338
|
+
mode,
|
|
322
339
|
contextSource: refreshedContext
|
|
323
340
|
? 'refreshed_context'
|
|
324
341
|
: continuity?.shouldReuseContext
|
|
@@ -328,12 +345,13 @@ const buildStartRecommendedPath = ({
|
|
|
328
345
|
ensureSessionRecommended: Boolean(hasMeaningfulPrompt(prompt) && (ensureSession || !summaryResult?.found)),
|
|
329
346
|
autoCreated,
|
|
330
347
|
isolatedSession,
|
|
331
|
-
nextTools:
|
|
348
|
+
nextTools: dedupedTools,
|
|
349
|
+
next,
|
|
332
350
|
instructions: steps.map((s) => `${s.tool}: ${s.instruction}`).join(' | '),
|
|
333
351
|
};
|
|
334
352
|
};
|
|
335
353
|
|
|
336
|
-
const buildEndRecommendedPath = ({ event, checkpoint, mutationSafety, workflow }) => {
|
|
354
|
+
const buildEndRecommendedPath = ({ event, checkpoint, mutationSafety, workflow, verbosity = DEFAULT_VERBOSITY }) => {
|
|
337
355
|
const nextTools = [];
|
|
338
356
|
const steps = [];
|
|
339
357
|
|
|
@@ -367,15 +385,24 @@ const buildEndRecommendedPath = ({ event, checkpoint, mutationSafety, workflow }
|
|
|
367
385
|
));
|
|
368
386
|
}
|
|
369
387
|
|
|
388
|
+
const mode = mutationSafety?.blocked
|
|
389
|
+
? 'blocked_guided'
|
|
390
|
+
: checkpoint?.skipped
|
|
391
|
+
? 'continue_until_milestone'
|
|
392
|
+
: 'checkpointed';
|
|
393
|
+
const dedupedTools = [...new Set(nextTools)];
|
|
394
|
+
const next = steps[0] ? `${steps[0].tool}: ${steps[0].instruction}` : (dedupedTools[0] ?? '');
|
|
395
|
+
|
|
396
|
+
if (verbosity === 'minimal') {
|
|
397
|
+
return { phase: 'end', mode, nextTools: dedupedTools, next };
|
|
398
|
+
}
|
|
399
|
+
|
|
370
400
|
return {
|
|
371
401
|
phase: 'end',
|
|
372
|
-
mode
|
|
373
|
-
? 'blocked_guided'
|
|
374
|
-
: checkpoint?.skipped
|
|
375
|
-
? 'continue_until_milestone'
|
|
376
|
-
: 'checkpointed',
|
|
402
|
+
mode,
|
|
377
403
|
checkpointEvent: event,
|
|
378
|
-
nextTools:
|
|
404
|
+
nextTools: dedupedTools,
|
|
405
|
+
next,
|
|
379
406
|
instructions: steps.map((s) => `${s.tool}: ${s.instruction}`).join(' | '),
|
|
380
407
|
};
|
|
381
408
|
};
|
|
@@ -416,8 +443,15 @@ const startTurn = async ({
|
|
|
416
443
|
includeMetrics = false,
|
|
417
444
|
metricsWindow = '7d',
|
|
418
445
|
latestMetrics = 5,
|
|
446
|
+
verbosity = DEFAULT_VERBOSITY,
|
|
419
447
|
} = {}) => {
|
|
420
448
|
const startTime = Date.now();
|
|
449
|
+
|
|
450
|
+
if (process.env.DEVCTX_DISABLE_BACKGROUND_TASKS !== 'true') {
|
|
451
|
+
triggerBackgroundIndexBuild({ root: projectRoot }).catch(() => {});
|
|
452
|
+
runStorageMaintenance().catch(() => {});
|
|
453
|
+
}
|
|
454
|
+
|
|
421
455
|
let summaryResult = await smartSummary({
|
|
422
456
|
action: 'get',
|
|
423
457
|
sessionId,
|
|
@@ -526,6 +560,7 @@ const startTurn = async ({
|
|
|
526
560
|
mutationSafety,
|
|
527
561
|
autoCreated,
|
|
528
562
|
isolatedSession,
|
|
563
|
+
verbosity,
|
|
529
564
|
});
|
|
530
565
|
|
|
531
566
|
await persistSmartTurnQualityMetrics({
|
|
@@ -555,32 +590,43 @@ const startTurn = async ({
|
|
|
555
590
|
},
|
|
556
591
|
});
|
|
557
592
|
|
|
593
|
+
const minimal = verbosity === 'minimal';
|
|
594
|
+
const compactContinuity = minimal
|
|
595
|
+
? { state: continuity.state, shouldReuseContext: continuity.shouldReuseContext }
|
|
596
|
+
: continuity;
|
|
597
|
+
const compactTask = summaryResult.task && minimal
|
|
598
|
+
? { taskId: summaryResult.task.taskId, status: summaryResult.task.status }
|
|
599
|
+
: summaryResult.task ?? null;
|
|
600
|
+
const includeMessage = !minimal || mutationSafety?.blocked;
|
|
601
|
+
|
|
558
602
|
return attachSafetyMetadata({
|
|
559
603
|
phase: 'start',
|
|
560
|
-
promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW),
|
|
561
604
|
sessionId: effectiveSessionId,
|
|
562
605
|
found: summaryResult.found ?? false,
|
|
563
606
|
autoCreated,
|
|
564
607
|
isolatedSession,
|
|
608
|
+
...(minimal ? {} : { promptPreview: truncate(prompt, MAX_PROMPT_PREVIEW) }),
|
|
565
609
|
...(previousSessionId ? { previousSessionId } : {}),
|
|
566
|
-
continuity,
|
|
567
|
-
summary: summaryResult.summary
|
|
610
|
+
continuity: compactContinuity,
|
|
611
|
+
...(summaryResult.summary ? { summary: summaryResult.summary } : {}),
|
|
568
612
|
...(refreshedContext ? { refreshedContext } : {}),
|
|
569
613
|
...(workflow ? { workflow } : {}),
|
|
570
|
-
...(summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
|
|
571
|
-
...(summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
|
|
572
|
-
...(
|
|
614
|
+
...(!minimal && summaryResult.candidates ? { candidates: summaryResult.candidates } : {}),
|
|
615
|
+
...(summaryResult.ambiguous && summaryResult.recommendedSessionId ? { recommendedSessionId: summaryResult.recommendedSessionId } : {}),
|
|
616
|
+
...(compactTask ? { task: compactTask } : {}),
|
|
573
617
|
...(summaryResult.handoff ? { handoff: summaryResult.handoff } : {}),
|
|
574
618
|
...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
|
|
575
619
|
...(isStorageUnhealthy(summaryResult.storageHealth ?? metrics?.storageHealth) ? { storageHealth: summaryResult.storageHealth ?? metrics?.storageHealth } : {}),
|
|
576
620
|
recommendedPath,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
621
|
+
...(includeMessage ? {
|
|
622
|
+
message: mutationSafety?.blocked
|
|
623
|
+
? mutationSafety.message
|
|
624
|
+
: summaryResult.found
|
|
625
|
+
? continuity.reason
|
|
626
|
+
: autoCreated
|
|
627
|
+
? 'Created a new persisted session for this task prompt.'
|
|
628
|
+
: continuity.reason,
|
|
629
|
+
} : {}),
|
|
584
630
|
}, {
|
|
585
631
|
repoSafety: summaryResult.repoSafety ?? metrics?.repoSafety ?? null,
|
|
586
632
|
sideEffectsSuppressed: Boolean(summaryResult.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
|
|
@@ -601,6 +647,7 @@ const endTurn = async ({
|
|
|
601
647
|
includeMetrics = false,
|
|
602
648
|
metricsWindow = '7d',
|
|
603
649
|
latestMetrics = 5,
|
|
650
|
+
verbosity = DEFAULT_VERBOSITY,
|
|
604
651
|
} = {}) => {
|
|
605
652
|
const startTime = Date.now();
|
|
606
653
|
const checkpoint = await smartSummary({
|
|
@@ -660,6 +707,7 @@ const endTurn = async ({
|
|
|
660
707
|
checkpoint,
|
|
661
708
|
mutationSafety,
|
|
662
709
|
workflow,
|
|
710
|
+
verbosity,
|
|
663
711
|
});
|
|
664
712
|
|
|
665
713
|
await persistSmartTurnQualityMetrics({
|
|
@@ -683,15 +731,34 @@ const endTurn = async ({
|
|
|
683
731
|
},
|
|
684
732
|
});
|
|
685
733
|
|
|
734
|
+
const minimal = verbosity === 'minimal';
|
|
735
|
+
const compactCheckpoint = minimal
|
|
736
|
+
? {
|
|
737
|
+
skipped: Boolean(checkpoint.skipped),
|
|
738
|
+
...(checkpoint.blocked !== undefined ? { blocked: checkpoint.blocked } : {}),
|
|
739
|
+
...(checkpoint.summary ? { summary: checkpoint.summary } : {}),
|
|
740
|
+
...(checkpoint.tokens !== undefined ? { tokens: checkpoint.tokens } : {}),
|
|
741
|
+
...(checkpoint.checkpoint ? {
|
|
742
|
+
checkpoint: {
|
|
743
|
+
event: checkpoint.checkpoint.event,
|
|
744
|
+
shouldPersist: checkpoint.checkpoint.shouldPersist,
|
|
745
|
+
score: checkpoint.checkpoint.score,
|
|
746
|
+
threshold: checkpoint.checkpoint.threshold,
|
|
747
|
+
},
|
|
748
|
+
} : {}),
|
|
749
|
+
}
|
|
750
|
+
: checkpoint;
|
|
751
|
+
const includeMessage = !minimal || mutationSafety?.blocked;
|
|
752
|
+
|
|
686
753
|
return attachSafetyMetadata({
|
|
687
754
|
phase: 'end',
|
|
688
755
|
sessionId: checkpoint.sessionId ?? sessionId ?? null,
|
|
689
|
-
checkpoint,
|
|
756
|
+
checkpoint: compactCheckpoint,
|
|
690
757
|
...(workflow ? { workflow } : {}),
|
|
691
758
|
...(metrics ? { metrics: summarizeMetrics(metrics) } : {}),
|
|
692
759
|
...(isStorageUnhealthy(checkpoint.storageHealth ?? metrics?.storageHealth) ? { storageHealth: checkpoint.storageHealth ?? metrics?.storageHealth } : {}),
|
|
693
760
|
recommendedPath,
|
|
694
|
-
message: mutationSafety?.blocked ? mutationSafety.message : checkpoint.message,
|
|
761
|
+
...(includeMessage ? { message: mutationSafety?.blocked ? mutationSafety.message : checkpoint.message } : {}),
|
|
695
762
|
}, {
|
|
696
763
|
repoSafety: checkpoint.repoSafety ?? metrics?.repoSafety ?? null,
|
|
697
764
|
sideEffectsSuppressed: Boolean(checkpoint.sideEffectsSuppressed ?? metrics?.sideEffectsSuppressed),
|
|
@@ -715,7 +782,10 @@ export const smartTurn = async ({
|
|
|
715
782
|
includeMetrics = false,
|
|
716
783
|
metricsWindow = '7d',
|
|
717
784
|
latestMetrics = 5,
|
|
785
|
+
verbosity,
|
|
718
786
|
} = {}) => {
|
|
787
|
+
const resolvedVerbosity = normalizeVerbosity(verbosity);
|
|
788
|
+
|
|
719
789
|
if (phase === 'start') {
|
|
720
790
|
return startTurn({
|
|
721
791
|
sessionId,
|
|
@@ -726,6 +796,7 @@ export const smartTurn = async ({
|
|
|
726
796
|
includeMetrics,
|
|
727
797
|
metricsWindow,
|
|
728
798
|
latestMetrics,
|
|
799
|
+
verbosity: resolvedVerbosity,
|
|
729
800
|
});
|
|
730
801
|
}
|
|
731
802
|
|
|
@@ -740,6 +811,7 @@ export const smartTurn = async ({
|
|
|
740
811
|
includeMetrics,
|
|
741
812
|
metricsWindow,
|
|
742
813
|
latestMetrics,
|
|
814
|
+
verbosity: resolvedVerbosity,
|
|
743
815
|
});
|
|
744
816
|
}
|
|
745
817
|
|