sweet-search 2.6.16 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/core/graph/structural-context-format.js +5 -0
- package/core/graph/structural-context.js +13 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/language-patterns/registry-core.js +14 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/infrastructure/structural-context-repository.js +73 -7
- package/core/infrastructure/structural-context-utils.js +11 -0
- package/core/infrastructure/structural-qualified-resolution.js +29 -0
- package/core/infrastructure/tree-sitter-provider.js +77 -3
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
- package/core/search/agent-pack-completion.js +493 -0
- package/core/search/agent-span-client.js +87 -0
- package/core/search/agent-span-ledger.js +447 -0
- package/core/search/context-expander.js +29 -0
- package/core/search/grep-output-shaping.js +28 -0
- package/core/search/regex-dialect.js +328 -0
- package/core/search/search-format.js +96 -0
- package/core/search/search-pattern.js +62 -10
- package/core/search/search-read.js +57 -10
- package/core/search/search-server.js +375 -7
- package/core/search/session-daemon-prewarm.mjs +75 -0
- package/core/search/sweet-search.js +7 -1
- package/core/search/unread-symbol-ranking.js +90 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +249 -45
- package/mcp/read-tool.js +34 -3
- package/mcp/server.js +55 -9
- package/mcp/tool-handlers.js +60 -7
- package/package.json +9 -9
- package/scripts/init.js +30 -5
- package/scripts/inject-agent-instructions.js +4 -2
- package/scripts/uninstall.js +21 -19
package/mcp/server.js
CHANGED
|
@@ -12,6 +12,12 @@ import { existsSync, statSync, readFileSync } from 'node:fs';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { launchMaintainer } from '../core/indexing/maintainer-launcher.mjs';
|
|
15
|
+
import {
|
|
16
|
+
collectSemanticShownSpans,
|
|
17
|
+
exactRereadOmissionEnabled,
|
|
18
|
+
validAgentSessionId,
|
|
19
|
+
} from '../core/search/agent-span-ledger.js';
|
|
20
|
+
import { sendAgentSpanOperation } from '../core/search/agent-span-client.js';
|
|
15
21
|
|
|
16
22
|
import {
|
|
17
23
|
SearchOutputSchema,
|
|
@@ -104,13 +110,26 @@ async function getConfig() {
|
|
|
104
110
|
|
|
105
111
|
const coreDir = path.join(__dirname, '..', 'core');
|
|
106
112
|
|
|
107
|
-
const searchDeps = { getSearcher };
|
|
113
|
+
const searchDeps = { getSearcher, PROJECT_ROOT };
|
|
108
114
|
const traceDeps = { PROJECT_ROOT };
|
|
109
115
|
const indexDeps = { PROJECT_ROOT, coreDir };
|
|
110
116
|
const healthDeps = { getConfig, PROJECT_ROOT };
|
|
111
117
|
const repoMapDeps = { coreDir, PROJECT_ROOT };
|
|
112
118
|
const vocabDeps = { coreDir };
|
|
113
119
|
|
|
120
|
+
function requestAgentSessionId(extra) {
|
|
121
|
+
// A long-lived MCP process does not receive later shell-environment rotation
|
|
122
|
+
// after clear/compact. Never fall back to inherited process session ids.
|
|
123
|
+
return validAgentSessionId(extra?.sessionId) ? extra.sessionId : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function touchAgentCall(extra, spans = [], { query, regex } = {}) {
|
|
127
|
+
if (!exactRereadOmissionEnabled()) return;
|
|
128
|
+
const sessionId = requestAgentSessionId(extra);
|
|
129
|
+
if (!sessionId) return;
|
|
130
|
+
await sendAgentSpanOperation({ operation: 'observe', sessionId, spans, query, regex });
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
// ---------------------------------------------------------------------------
|
|
115
134
|
// MCP Server
|
|
116
135
|
// ---------------------------------------------------------------------------
|
|
@@ -171,7 +190,10 @@ server.registerTool('search', {
|
|
|
171
190
|
idempotentHint: true,
|
|
172
191
|
openWorldHint: false,
|
|
173
192
|
},
|
|
174
|
-
}, async (args) => handleSearch(args,
|
|
193
|
+
}, async (args, extra) => handleSearch(args, {
|
|
194
|
+
...searchDeps,
|
|
195
|
+
agentSessionId: requestAgentSessionId(extra),
|
|
196
|
+
}));
|
|
175
197
|
|
|
176
198
|
server.registerTool('trace', {
|
|
177
199
|
description: 'Trace callers, callees, and transitive impact paths for one specific symbol — returns a single structural-context package adapted to the token budget. USE WHEN the question is "who calls X", "what does X depend on", or "what would break if I changed X". For general code discovery use `search` instead; for navigation around an unfamiliar repo use `repo-map` first.',
|
|
@@ -194,7 +216,12 @@ server.registerTool('trace', {
|
|
|
194
216
|
idempotentHint: true,
|
|
195
217
|
openWorldHint: false,
|
|
196
218
|
},
|
|
197
|
-
}, async (args) =>
|
|
219
|
+
}, async (args, extra) => {
|
|
220
|
+
await touchAgentCall(extra, [], {
|
|
221
|
+
query: `${args.symbol} ${args.query || ''}`.trim(),
|
|
222
|
+
});
|
|
223
|
+
return handleTrace(args, traceDeps);
|
|
224
|
+
});
|
|
198
225
|
|
|
199
226
|
server.registerTool('index', {
|
|
200
227
|
description: 'Index or re-index the codebase. USE BEFORE first search if the project has not been indexed yet, or after large source changes (`mode="full"`). The Claude Code SessionStart hook installed by `sweet-search init` keeps the incremental index fresh during normal sessions, so manual re-indexing is rarely needed.',
|
|
@@ -209,7 +236,10 @@ server.registerTool('index', {
|
|
|
209
236
|
idempotentHint: false,
|
|
210
237
|
openWorldHint: false,
|
|
211
238
|
},
|
|
212
|
-
}, async (args) =>
|
|
239
|
+
}, async (args, extra) => {
|
|
240
|
+
await touchAgentCall(extra);
|
|
241
|
+
return handleIndex(args, indexDeps);
|
|
242
|
+
});
|
|
213
243
|
|
|
214
244
|
server.registerTool('health', {
|
|
215
245
|
description: 'Check health of every sweet-search subsystem (index, embedding model, late-interaction reranker, structural graph, daemon). USE WHEN searches return empty unexpectedly, results look stale, or latency is unusual — diagnoses missing index, model load failures, daemon issues. Read-only, fast.',
|
|
@@ -220,7 +250,8 @@ server.registerTool('health', {
|
|
|
220
250
|
idempotentHint: true,
|
|
221
251
|
openWorldHint: false,
|
|
222
252
|
},
|
|
223
|
-
}, async () => {
|
|
253
|
+
}, async (_args, extra) => {
|
|
254
|
+
await touchAgentCall(extra);
|
|
224
255
|
const structured = await checkHealth(healthDeps);
|
|
225
256
|
|
|
226
257
|
const statusLines = Object.entries(structured.subsystems).map(
|
|
@@ -251,7 +282,10 @@ server.registerTool('repo-map', {
|
|
|
251
282
|
idempotentHint: true,
|
|
252
283
|
openWorldHint: false,
|
|
253
284
|
},
|
|
254
|
-
}, async (args) =>
|
|
285
|
+
}, async (args, extra) => {
|
|
286
|
+
await touchAgentCall(extra);
|
|
287
|
+
return handleRepoMap(args, repoMapDeps);
|
|
288
|
+
});
|
|
255
289
|
|
|
256
290
|
server.registerTool('vocab-prewarm', {
|
|
257
291
|
description: 'Pre-warm sweet-search caches by mining the codebase for project-specific vocabulary across lexical / semantic / hybrid modes. USE ONCE after a fresh index to make the first batch of searches faster; generally not needed for one-off queries because the daemon-prewarm hook handles cold-start warmup automatically.',
|
|
@@ -272,7 +306,10 @@ server.registerTool('vocab-prewarm', {
|
|
|
272
306
|
idempotentHint: true,
|
|
273
307
|
openWorldHint: true,
|
|
274
308
|
},
|
|
275
|
-
}, async (args) =>
|
|
309
|
+
}, async (args, extra) => {
|
|
310
|
+
await touchAgentCall(extra);
|
|
311
|
+
return handleVocabPrewarm(args, vocabDeps);
|
|
312
|
+
});
|
|
276
313
|
|
|
277
314
|
server.registerTool('read', {
|
|
278
315
|
description: 'Read 1-20 files (with optional line ranges) for exact code understanding. USE INSTEAD OF the native Read tool for code-discovery reads — batches multiple files in one call, attaches symbol-aware chunk metadata when the file is indexed, and returns the exact bytes from disk. Native Read remains fine for files you are about to Edit (Edit needs a prior Read of that exact file).',
|
|
@@ -284,10 +321,14 @@ server.registerTool('read', {
|
|
|
284
321
|
})).min(1).max(20).describe('Files to read (1-20)'),
|
|
285
322
|
includeMetadata: z.boolean().default(true).optional()
|
|
286
323
|
.describe('Attach symbol-aware chunk metadata when the file is indexed'),
|
|
324
|
+
force: z.boolean().default(false).optional(),
|
|
287
325
|
},
|
|
288
326
|
outputSchema: ReadOutputSchema,
|
|
289
327
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
290
|
-
}, async (args) => handleRead(args, {
|
|
328
|
+
}, async (args, extra) => handleRead(args, {
|
|
329
|
+
PROJECT_ROOT,
|
|
330
|
+
agentSessionId: requestAgentSessionId(extra),
|
|
331
|
+
}));
|
|
291
332
|
|
|
292
333
|
server.registerTool('read-semantic', {
|
|
293
334
|
description: 'Read only the spans of a file relevant to a question. USE WHEN you know the file but the relevant span is unclear — selects spans via hybrid retrieval (lexical + symbol + ColBERT MaxSim, RRF-fused and LI-reranked), then re-reads exact lines from disk. Returns 1-N small spans instead of the full file. Avoid running this on multiple files unless the task is explicitly multi-file — call `search` with the question instead. Falls back to a plain read when the file is not indexed.',
|
|
@@ -309,7 +350,12 @@ server.registerTool('read-semantic', {
|
|
|
309
350
|
},
|
|
310
351
|
outputSchema: ReadSemanticOutputSchema,
|
|
311
352
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
312
|
-
}, async (args) =>
|
|
353
|
+
}, async (args, extra) => {
|
|
354
|
+
const result = await handleReadSemantic(args, { PROJECT_ROOT });
|
|
355
|
+
const spans = collectSemanticShownSpans(result.structuredContent, { projectRoot: PROJECT_ROOT });
|
|
356
|
+
await touchAgentCall(extra, spans, { query: args.query });
|
|
357
|
+
return result;
|
|
358
|
+
});
|
|
313
359
|
|
|
314
360
|
// ---------------------------------------------------------------------------
|
|
315
361
|
// Resources
|
package/mcp/tool-handlers.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// mcp/tool-handlers.js — Extracted tool handler functions (SOLID refactor)
|
|
2
2
|
// Each handler receives its dependencies via a `deps` parameter.
|
|
3
|
-
|
|
4
3
|
import { z } from 'zod';
|
|
5
4
|
import { existsSync } from 'node:fs';
|
|
6
5
|
import { spawn } from 'node:child_process';
|
|
7
6
|
import path from 'node:path';
|
|
8
|
-
|
|
7
|
+
import { collectAgentShownSpans, exactRereadOmissionEnabled, renderShownFullTrailer, shownSpanTrailerEnabled } from '../core/search/agent-span-ledger.js';
|
|
8
|
+
import { sendAgentSpanOperation } from '../core/search/agent-span-client.js';
|
|
9
|
+
import { renderRegexDialectHint } from '../core/search/regex-dialect.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Output schemas (Zod — SDK converts to JSON Schema for clients)
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
13
|
const SearchResultSchema = z.object({
|
|
14
14
|
file: z.string(),
|
|
15
15
|
score: z.number(),
|
|
@@ -25,6 +25,22 @@ const SearchResultSchema = z.object({
|
|
|
25
25
|
presentation: z.enum(['full', 'preview', 'summary']).optional(),
|
|
26
26
|
code: z.string().nullable().optional(),
|
|
27
27
|
codeTokens: z.number().int().optional(),
|
|
28
|
+
continuation: z.object({
|
|
29
|
+
kind: z.enum(['symbol', 'trailer']),
|
|
30
|
+
file: z.string(),
|
|
31
|
+
startLine: z.number().int(),
|
|
32
|
+
endLine: z.number().int(),
|
|
33
|
+
symbol: z.string(),
|
|
34
|
+
symbolType: z.string().nullable().optional(),
|
|
35
|
+
code: z.string().nullable().optional(),
|
|
36
|
+
rendered: z.string(),
|
|
37
|
+
tokens: z.number().int(),
|
|
38
|
+
}).optional(),
|
|
39
|
+
familyManifest: z.object({
|
|
40
|
+
rendered: z.string(),
|
|
41
|
+
tokens: z.number().int(),
|
|
42
|
+
groups: z.array(z.string()),
|
|
43
|
+
}).optional(),
|
|
28
44
|
});
|
|
29
45
|
|
|
30
46
|
export const SearchOutputSchema = z.object({
|
|
@@ -102,10 +118,11 @@ let _healthDb = null;
|
|
|
102
118
|
|
|
103
119
|
/**
|
|
104
120
|
* @param {{ query: string, k: number, mode: string, structural?: boolean, regex?: string, format?: string, tokenBudget?: number }} args
|
|
105
|
-
* @param {{ getSearcher: () => Promise<object
|
|
121
|
+
* @param {{ getSearcher: () => Promise<object>, PROJECT_ROOT?: string, agentSessionId?: string }} deps
|
|
106
122
|
*/
|
|
107
|
-
export async function handleSearch({ query, k, mode, structural, regex, format, tokenBudget },
|
|
123
|
+
export async function handleSearch({ query, k, mode, structural, regex, format, tokenBudget }, deps) {
|
|
108
124
|
try {
|
|
125
|
+
const { getSearcher } = deps;
|
|
109
126
|
const searcher = await getSearcher();
|
|
110
127
|
const searchMode = structural ? 'structural' : mode;
|
|
111
128
|
const searchResult = await searcher.search(query, {
|
|
@@ -116,11 +133,25 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
116
133
|
...(regex && { regex }),
|
|
117
134
|
...(format && { format }),
|
|
118
135
|
...(tokenBudget && { tokenBudget }),
|
|
136
|
+
_isAgentFormat: format === 'agent',
|
|
119
137
|
});
|
|
120
138
|
|
|
121
139
|
// Agent mode: return the fully packaged response directly
|
|
122
140
|
if (searchResult.format === 'agent') {
|
|
123
141
|
const agentResults = searchResult.results || [];
|
|
142
|
+
const exactReread = exactRereadOmissionEnabled() && deps.agentSessionId;
|
|
143
|
+
const shownTrailerEnabled = shownSpanTrailerEnabled();
|
|
144
|
+
const shownSpans = (exactReread || shownTrailerEnabled)
|
|
145
|
+
? collectAgentShownSpans(agentResults, { projectRoot: deps.PROJECT_ROOT }) : [];
|
|
146
|
+
if (exactReread) {
|
|
147
|
+
await sendAgentSpanOperation({
|
|
148
|
+
operation: 'observe',
|
|
149
|
+
sessionId: deps.agentSessionId,
|
|
150
|
+
spans: shownSpans,
|
|
151
|
+
query,
|
|
152
|
+
regex,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
124
155
|
const lines = agentResults
|
|
125
156
|
.filter(r => r.presentation !== 'summary')
|
|
126
157
|
.map((r) => {
|
|
@@ -134,7 +165,14 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
134
165
|
.map(n => `${n.name} (${n.type === 'function' ? 'fn' : (n.type || 'sym')} ${n.startLine}-${n.endLine} ${n.position})`)
|
|
135
166
|
.join(' · ')} — sweep: read-semantic ${r.file}`
|
|
136
167
|
: '';
|
|
137
|
-
|
|
168
|
+
const continuation = r.continuation?.rendered
|
|
169
|
+
? `\n ${r.continuation.rendered.replace(/^#\s*/, '')}`
|
|
170
|
+
+ (r.continuation.kind === 'symbol' && r.continuation.code
|
|
171
|
+
? `\n\`\`\`\n${r.continuation.code}\n\`\`\`` : '')
|
|
172
|
+
: '';
|
|
173
|
+
const family = r.familyManifest?.rendered
|
|
174
|
+
? `\n ${r.familyManifest.rendered.replace(/^#\s*/, '')}` : '';
|
|
175
|
+
return header + symbolInfo + code + sameFile + continuation + family;
|
|
138
176
|
});
|
|
139
177
|
|
|
140
178
|
const summaries = agentResults
|
|
@@ -144,9 +182,13 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
144
182
|
const confidence = `Confidence: ${searchResult.confidence} (${searchResult.confidenceReason})`;
|
|
145
183
|
const budget = `Tokens: ${searchResult.tokensUsed}/${searchResult.tokenBudget}`;
|
|
146
184
|
|
|
147
|
-
|
|
185
|
+
let text = lines.length > 0
|
|
148
186
|
? `${confidence} | ${budget}\n\n${lines.join('\n\n')}${summaries.length ? '\n\nAlso found:\n' + summaries.join('\n') : ''}`
|
|
149
187
|
: `No results found for "${query}"`;
|
|
188
|
+
const regexDialectNote = renderRegexDialectHint(searchResult.stats?.regexDialectHint);
|
|
189
|
+
if (regexDialectNote) text += `\n\n${regexDialectNote}`;
|
|
190
|
+
const shownTrailer = shownTrailerEnabled ? renderShownFullTrailer(shownSpans) : '';
|
|
191
|
+
if (shownTrailer) text += `\n\n${shownTrailer}`;
|
|
150
192
|
|
|
151
193
|
// Shape structuredContent to conform to SearchOutputSchema
|
|
152
194
|
const structured = {
|
|
@@ -161,6 +203,14 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
161
203
|
presentation: r.presentation,
|
|
162
204
|
code: r.code ?? null,
|
|
163
205
|
codeTokens: r.codeTokens,
|
|
206
|
+
...(r.continuation ? { continuation: r.continuation } : {}),
|
|
207
|
+
...(r.familyManifest ? {
|
|
208
|
+
familyManifest: {
|
|
209
|
+
rendered: r.familyManifest.rendered,
|
|
210
|
+
tokens: r.familyManifest.tokens,
|
|
211
|
+
groups: r.familyManifest.groups,
|
|
212
|
+
},
|
|
213
|
+
} : {}),
|
|
164
214
|
})),
|
|
165
215
|
totalFound: searchResult.totalResults,
|
|
166
216
|
mode: searchResult.mode,
|
|
@@ -189,6 +239,9 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
|
|
|
189
239
|
}
|
|
190
240
|
|
|
191
241
|
// Standard benchmark mode
|
|
242
|
+
if (exactRereadOmissionEnabled() && deps.agentSessionId) {
|
|
243
|
+
await sendAgentSpanOperation({ operation: 'observe', sessionId: deps.agentSessionId, spans: [] });
|
|
244
|
+
}
|
|
192
245
|
const { results, stats } = searchResult;
|
|
193
246
|
const structured = {
|
|
194
247
|
format: 'benchmark',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.7.0",
|
|
4
|
+
"description": "Local code search for AI agents: six fast, purpose-built tools that return ranked answers, not raw grep. Because maybe grep isn't all you need... 🍬",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
7
7
|
"exports": {
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"vitest": "^4.0.16"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@sweet-search/native-darwin-arm64": "2.
|
|
171
|
-
"@sweet-search/native-darwin-x64": "2.
|
|
172
|
-
"@sweet-search/native-linux-arm64-gnu": "2.
|
|
173
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.
|
|
174
|
-
"@sweet-search/native-linux-x64-gnu": "2.
|
|
175
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.
|
|
176
|
-
"@sweet-search/bg-priority": "2.
|
|
170
|
+
"@sweet-search/native-darwin-arm64": "2.7.0",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.7.0",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.7.0",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.7.0",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.7.0",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.7.0",
|
|
176
|
+
"@sweet-search/bg-priority": "2.7.0"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|
package/scripts/init.js
CHANGED
|
@@ -994,7 +994,8 @@ export function registerPrewarmSessionStartHook({
|
|
|
994
994
|
};
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
-
const command = `node ${hookPath}`;
|
|
997
|
+
const command = `node ${hookPath} --agent-session-hook`;
|
|
998
|
+
const dropCommand = `node ${hookPath} --agent-session-drop`;
|
|
998
999
|
|
|
999
1000
|
const settingsDir = join(projectRoot, '.claude');
|
|
1000
1001
|
const settingsPath = join(settingsDir, 'settings.json');
|
|
@@ -1012,6 +1013,7 @@ export function registerPrewarmSessionStartHook({
|
|
|
1012
1013
|
const sessionStart = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : [];
|
|
1013
1014
|
|
|
1014
1015
|
const entry = {
|
|
1016
|
+
matcher: 'startup|resume|clear|compact',
|
|
1015
1017
|
hooks: [
|
|
1016
1018
|
{
|
|
1017
1019
|
type: 'command',
|
|
@@ -1035,6 +1037,29 @@ export function registerPrewarmSessionStartHook({
|
|
|
1035
1037
|
}
|
|
1036
1038
|
settings.hooks.SessionStart = sessionStart;
|
|
1037
1039
|
|
|
1040
|
+
// Claude exposes a real SessionEnd event. Drop the bounded RAM receipt
|
|
1041
|
+
// bucket immediately instead of waiting for daemon/session LRU eviction.
|
|
1042
|
+
const sessionEnd = Array.isArray(settings.hooks.SessionEnd) ? settings.hooks.SessionEnd : [];
|
|
1043
|
+
const endEntry = {
|
|
1044
|
+
hooks: [
|
|
1045
|
+
{
|
|
1046
|
+
type: 'command',
|
|
1047
|
+
command: dropCommand,
|
|
1048
|
+
timeout: 2000,
|
|
1049
|
+
continueOnError: true,
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
};
|
|
1053
|
+
const ownedEndIdx = sessionEnd.findIndex((group) =>
|
|
1054
|
+
Array.isArray(group?.hooks) &&
|
|
1055
|
+
group.hooks.some((h) => typeof h?.command === 'string'
|
|
1056
|
+
&& h.command.includes(PREWARM_HOOK_FILENAME)
|
|
1057
|
+
&& h.command.includes('--agent-session-drop'))
|
|
1058
|
+
);
|
|
1059
|
+
if (ownedEndIdx >= 0) sessionEnd[ownedEndIdx] = endEntry;
|
|
1060
|
+
else sessionEnd.push(endEntry);
|
|
1061
|
+
settings.hooks.SessionEnd = sessionEnd;
|
|
1062
|
+
|
|
1038
1063
|
try {
|
|
1039
1064
|
mkdirSync(settingsDir, { recursive: true });
|
|
1040
1065
|
const tmpPath = settingsPath + '.tmp';
|
|
@@ -1103,7 +1128,7 @@ export function registerCodexSessionStartHook({ projectRoot, packageRoot, skippe
|
|
|
1103
1128
|
// a repo), which fixes both path resolution AND the launcher's own
|
|
1104
1129
|
// `process.cwd()` project-root detection, while staying machine-portable
|
|
1105
1130
|
// (no absolute path is written into the often-committed hooks.json).
|
|
1106
|
-
const command = `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && node ${JSON.stringify(hookPath)}`;
|
|
1131
|
+
const command = `cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)" && node ${JSON.stringify(hookPath)} --agent-session-hook`;
|
|
1107
1132
|
const codexDir = join(projectRoot, '.codex');
|
|
1108
1133
|
const hooksPath = join(codexDir, CODEX_HOOKS_FILENAME);
|
|
1109
1134
|
|
|
@@ -1119,12 +1144,12 @@ export function registerCodexSessionStartHook({ projectRoot, packageRoot, skippe
|
|
|
1119
1144
|
doc.hooks = doc.hooks || {};
|
|
1120
1145
|
const sessionStart = Array.isArray(doc.hooks.SessionStart) ? doc.hooks.SessionStart : [];
|
|
1121
1146
|
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1147
|
+
// Start/resume prewarm the daemon; clear/compact also reset shown-span
|
|
1148
|
+
// receipts for the new context epoch.
|
|
1124
1149
|
// `timeout` is in seconds (per Codex's hooks docs); the launcher detaches and
|
|
1125
1150
|
// returns in well under a second, so a small budget is plenty.
|
|
1126
1151
|
const entry = {
|
|
1127
|
-
matcher: 'startup|resume',
|
|
1152
|
+
matcher: 'startup|resume|clear|compact',
|
|
1128
1153
|
hooks: [
|
|
1129
1154
|
{
|
|
1130
1155
|
type: 'command',
|
|
@@ -54,8 +54,10 @@ function escapeRegex(s) {
|
|
|
54
54
|
// champion (PHASE7 M++ — held-out 0.988 Maximin, OOD 0.952, HOMP/SCS/counter
|
|
55
55
|
// all pass, 5-cell cross-harness validated — plus the stop-discipline scoping
|
|
56
56
|
// edits that fix M++'s over-stopping on FIX tasks, plus the verdict-gated
|
|
57
|
-
// trust line that scans already-delivered lower ranks before any new search
|
|
58
|
-
//
|
|
57
|
+
// trust line that scans already-delivered lower ranks before any new search,
|
|
58
|
+
// plus the P2 fix-surface paragraph that maps a multi-site edit surface in
|
|
59
|
+
// ONE call before the first edit; tool routing unchanged), injected VERBATIM
|
|
60
|
+
// into the harness files.
|
|
59
61
|
// Per-harness shims (Cursor frontmatter, @imports) are applied at write-time,
|
|
60
62
|
// not embedded here.
|
|
61
63
|
//
|
package/scripts/uninstall.js
CHANGED
|
@@ -406,7 +406,7 @@ function pruneEmptyAncestors(start, stopAt) {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
409
|
-
* Remove the sweet-search-owned SessionStart
|
|
409
|
+
* Remove the sweet-search-owned SessionStart and SessionEnd entries from `.claude/settings.json`,
|
|
410
410
|
* preserving every other hook, permission, and top-level key. Detection is
|
|
411
411
|
* filename-based (see PREWARM_HOOK_FILENAME) — only entries whose command
|
|
412
412
|
* references the sweet-search preheat script are removed.
|
|
@@ -437,31 +437,33 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
|
|
|
437
437
|
return { status: 'error', detail: `settings.json is not valid JSON: ${err.message}` };
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
const changes = [];
|
|
441
|
+
for (const event of ['SessionStart', 'SessionEnd']) {
|
|
442
|
+
const groups = settings?.hooks?.[event];
|
|
443
|
+
if (!Array.isArray(groups)) continue;
|
|
444
|
+
const filtered = groups.filter((group) =>
|
|
445
|
+
!(Array.isArray(group?.hooks) &&
|
|
446
|
+
group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME)))
|
|
447
|
+
);
|
|
448
|
+
if (filtered.length !== groups.length) changes.push({ event, groups, filtered });
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
|
|
446
|
-
!(Array.isArray(group?.hooks) &&
|
|
447
|
-
group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME)))
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
if (filtered.length === sessionStart.length) {
|
|
451
|
+
if (changes.length === 0) {
|
|
451
452
|
return { status: 'not-found', detail: 'no matching entry' };
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
const removedCount = changes.reduce((sum, change) => sum + change.groups.length - change.filtered.length, 0);
|
|
456
|
+
|
|
454
457
|
if (dryRun) {
|
|
455
|
-
return { status: 'dry-run', detail: `would remove ${
|
|
458
|
+
return { status: 'dry-run', detail: `would remove ${removedCount} entries` };
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
|
|
459
|
-
delete settings.hooks
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
settings.hooks.SessionStart = filtered;
|
|
461
|
+
for (const { event, filtered } of changes) {
|
|
462
|
+
if (filtered.length === 0) delete settings.hooks[event];
|
|
463
|
+
else settings.hooks[event] = filtered;
|
|
464
|
+
}
|
|
465
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) {
|
|
466
|
+
delete settings.hooks;
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
try {
|
|
@@ -472,7 +474,7 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
|
|
|
472
474
|
return { status: 'error', detail: `write failed: ${err.message}` };
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
return { status: 'removed', detail: `spliced out ${
|
|
477
|
+
return { status: 'removed', detail: `spliced out ${removedCount} entries` };
|
|
476
478
|
}
|
|
477
479
|
|
|
478
480
|
/**
|