@phnx-labs/agents-cli 1.21.3 → 1.22.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/CHANGELOG.md +67 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +22 -2
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { StringDecoder } from 'string_decoder';
|
|
3
|
+
import { getDB } from './db.js';
|
|
4
|
+
import { parseSession } from './parse.js';
|
|
5
|
+
import { TOOL_INDEX_VERSION, TOOL_INDEX_LIMIT_ORDINAL, ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, toolCallEvidenceBytes, toolCallsFromEvents, } from './tool-calls.js';
|
|
6
|
+
import { persistToolCalls, purgeToolCalls, toolEvidenceSourcePath, } from './tool-store.js';
|
|
7
|
+
const BACKFILL_MAX_FILES = 25;
|
|
8
|
+
const BACKFILL_MAX_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const BACKFILL_MAX_IN_MEMORY_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const BACKFILL_MAX_JSONL_RECORD_BYTES = 1024 * 1024;
|
|
11
|
+
export const BACKFILL_MAX_STREAM_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
export const TOOL_SEARCH_SCHEMA_VERSION = 1;
|
|
13
|
+
export const TOOL_PROGRAM_COUNT_SCHEMA_VERSION = 1;
|
|
14
|
+
export const TOOL_QUERY_MAX_CLAUSES = 32;
|
|
15
|
+
export const TOOL_QUERY_MAX_CLAUSE_BYTES = 4096;
|
|
16
|
+
export const TOOL_QUERY_MAX_CALL_ROWS = 50_000;
|
|
17
|
+
export const TOOL_QUERY_MAX_RESULT_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
export const TOOL_QUERY_MAX_RESULT_SESSIONS = 1_000;
|
|
19
|
+
export const TOOL_QUERY_MAX_SERIALIZED_BYTES = 15 * 1024 * 1024;
|
|
20
|
+
export const TOOL_QUERY_MERGE_OVERHEAD_BYTES = 64 * 1024;
|
|
21
|
+
/** Serialize once and enforce headroom below the fleet transport's 16 MiB cap. */
|
|
22
|
+
export function serializeToolSearchEnvelope(envelope) {
|
|
23
|
+
const rendered = JSON.stringify(envelope, null, 2) + '\n';
|
|
24
|
+
if (Buffer.byteLength(rendered) > TOOL_QUERY_MAX_SERIALIZED_BYTES) {
|
|
25
|
+
throw new Error(`Tool result exceeds ${TOOL_QUERY_MAX_SERIALIZED_BYTES / 1024 / 1024} MiB after JSON encoding; reduce --limit or add a more specific term.`);
|
|
26
|
+
}
|
|
27
|
+
return rendered;
|
|
28
|
+
}
|
|
29
|
+
/** Exact UTF-8 size of the public pretty-JSON envelope. */
|
|
30
|
+
export function serializedToolSearchEnvelopeBytes(envelope) {
|
|
31
|
+
return Buffer.byteLength(JSON.stringify(envelope, null, 2) + '\n');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reserve the exact local JSON plus coordinator headroom before retaining peer
|
|
35
|
+
* stdout. Peer envelopes repeat metadata that disappears during merge, so
|
|
36
|
+
* charging their full wire bytes against the remainder is conservative.
|
|
37
|
+
*/
|
|
38
|
+
export function toolSearchRemoteReceiveBudget(envelope) {
|
|
39
|
+
const localBytes = serializedToolSearchEnvelopeBytes(envelope);
|
|
40
|
+
return Math.max(0, TOOL_QUERY_MAX_SERIALIZED_BYTES - TOOL_QUERY_MERGE_OVERHEAD_BYTES - localBytes);
|
|
41
|
+
}
|
|
42
|
+
function needsIndex(db, sessionId, stamp) {
|
|
43
|
+
const row = db.prepare(`
|
|
44
|
+
SELECT file_mtime_ms, file_size, extractor_version
|
|
45
|
+
FROM tool_scan_ledger WHERE session_id = ?
|
|
46
|
+
`).get(sessionId);
|
|
47
|
+
return !row
|
|
48
|
+
|| row.file_mtime_ms !== stamp.fileMtimeMs
|
|
49
|
+
|| row.file_size !== stamp.fileSize
|
|
50
|
+
|| row.extractor_version !== TOOL_INDEX_VERSION;
|
|
51
|
+
}
|
|
52
|
+
/** Read index completeness from SQLite only; never stat or parse transcripts. */
|
|
53
|
+
export function readToolIndexCoverage(sessions) {
|
|
54
|
+
const db = getDB();
|
|
55
|
+
const sessionIds = [...new Set(sessions
|
|
56
|
+
.filter((session) => session.filePath)
|
|
57
|
+
.map((session) => session.id))];
|
|
58
|
+
const ledgerRows = sessionIds.length === 0 ? [] : db.prepare(`
|
|
59
|
+
SELECT session_id, extractor_version, call_count
|
|
60
|
+
FROM tool_scan_ledger
|
|
61
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
62
|
+
`).all(JSON.stringify(sessionIds));
|
|
63
|
+
const currentRows = ledgerRows.filter((row) => row.extractor_version === TOOL_INDEX_VERSION);
|
|
64
|
+
const limited = sessionIds.length === 0 ? { count: 0 } : db.prepare(`
|
|
65
|
+
SELECT count(DISTINCT session_id) AS count
|
|
66
|
+
FROM tool_calls
|
|
67
|
+
WHERE tool = 'index_limit'
|
|
68
|
+
AND session_id IN (SELECT value FROM json_each(?))
|
|
69
|
+
`).get(JSON.stringify(sessionIds));
|
|
70
|
+
const remainingFiles = Math.max(0, sessionIds.length - currentRows.length);
|
|
71
|
+
return {
|
|
72
|
+
indexedFiles: currentRows.length,
|
|
73
|
+
indexedCalls: currentRows.reduce((sum, row) => sum + row.call_count, 0),
|
|
74
|
+
skippedFiles: 0,
|
|
75
|
+
limitedFiles: limited.count,
|
|
76
|
+
remainingFiles,
|
|
77
|
+
complete: remainingFiles === 0 && limited.count === 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function backfillLimitCall(session, reason) {
|
|
81
|
+
return {
|
|
82
|
+
ordinal: TOOL_INDEX_LIMIT_ORDINAL,
|
|
83
|
+
timestamp: session.timestamp,
|
|
84
|
+
tool: 'index_limit',
|
|
85
|
+
programs: [],
|
|
86
|
+
programOccurrences: [],
|
|
87
|
+
input: reason,
|
|
88
|
+
outcome: 'unknown',
|
|
89
|
+
parseError: 'Additional tool calls were not indexed for this session.',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Stream Claude/Codex JSONL without ever retaining an oversized record. */
|
|
93
|
+
async function streamJsonlToolCalls(session) {
|
|
94
|
+
const collector = new ToolCallCollector();
|
|
95
|
+
const stream = fs.createReadStream(session.filePath, { highWaterMark: 64 * 1024 });
|
|
96
|
+
const decoder = new StringDecoder('utf8');
|
|
97
|
+
let pending = '';
|
|
98
|
+
let pendingBytes = 0;
|
|
99
|
+
let droppingOversizedLine = false;
|
|
100
|
+
let skippedOversizedLine = false;
|
|
101
|
+
const applyLine = (line) => {
|
|
102
|
+
if (!line.trim())
|
|
103
|
+
return;
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (session.agent === 'claude')
|
|
112
|
+
collectClaudeToolCalls(collector, parsed);
|
|
113
|
+
else
|
|
114
|
+
collectCodexToolCalls(collector, parsed);
|
|
115
|
+
};
|
|
116
|
+
const consume = (text) => {
|
|
117
|
+
let start = 0;
|
|
118
|
+
while (start < text.length) {
|
|
119
|
+
const newline = text.indexOf('\n', start);
|
|
120
|
+
const end = newline >= 0 ? newline : text.length;
|
|
121
|
+
const segment = text.slice(start, end);
|
|
122
|
+
if (!droppingOversizedLine) {
|
|
123
|
+
const segmentBytes = Buffer.byteLength(segment);
|
|
124
|
+
if (pendingBytes + segmentBytes <= BACKFILL_MAX_JSONL_RECORD_BYTES) {
|
|
125
|
+
pending += segment;
|
|
126
|
+
pendingBytes += segmentBytes;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
pending = '';
|
|
130
|
+
pendingBytes = 0;
|
|
131
|
+
droppingOversizedLine = true;
|
|
132
|
+
skippedOversizedLine = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (newline < 0)
|
|
136
|
+
break;
|
|
137
|
+
if (!droppingOversizedLine)
|
|
138
|
+
applyLine(pending);
|
|
139
|
+
pending = '';
|
|
140
|
+
pendingBytes = 0;
|
|
141
|
+
droppingOversizedLine = false;
|
|
142
|
+
start = newline + 1;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
for await (const chunk of stream)
|
|
146
|
+
consume(decoder.write(chunk));
|
|
147
|
+
consume(decoder.end());
|
|
148
|
+
if (!droppingOversizedLine && pending.length > 0)
|
|
149
|
+
applyLine(pending);
|
|
150
|
+
const calls = collector.drainChanged();
|
|
151
|
+
if (skippedOversizedLine && !calls.some((call) => call.ordinal === TOOL_INDEX_LIMIT_ORDINAL)) {
|
|
152
|
+
calls.push(backfillLimitCall(session, 'At least one JSONL record exceeded the 1 MiB tool-backfill parser limit.'));
|
|
153
|
+
}
|
|
154
|
+
return calls;
|
|
155
|
+
}
|
|
156
|
+
async function toolCallsForBackfill(session, sourceBytes) {
|
|
157
|
+
if (session.agent === 'claude' || session.agent === 'codex') {
|
|
158
|
+
if (sourceBytes > BACKFILL_MAX_STREAM_SOURCE_BYTES) {
|
|
159
|
+
return [backfillLimitCall(session, 'Transcript exceeds the 64 MiB safe streaming tool-backfill limit.')];
|
|
160
|
+
}
|
|
161
|
+
return streamJsonlToolCalls(session);
|
|
162
|
+
}
|
|
163
|
+
if (sourceBytes > BACKFILL_MAX_IN_MEMORY_SOURCE_BYTES) {
|
|
164
|
+
return [backfillLimitCall(session, 'Transcript exceeds the 16 MiB safe in-memory tool-backfill parser limit.')];
|
|
165
|
+
}
|
|
166
|
+
return toolCallsFromEvents(parseSession(session.filePath, session.agent));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Fill one bounded chunk of the independent tool index. A warm call performs
|
|
170
|
+
* only stat + ledger checks; it never opens or parses unchanged transcripts.
|
|
171
|
+
*/
|
|
172
|
+
export async function ensureToolIndex(sessions, limits = {}) {
|
|
173
|
+
const maxFiles = limits.maxFiles ?? BACKFILL_MAX_FILES;
|
|
174
|
+
const maxBytes = limits.maxBytes ?? BACKFILL_MAX_BYTES;
|
|
175
|
+
const db = getDB();
|
|
176
|
+
const pending = [];
|
|
177
|
+
let skippedFiles = 0;
|
|
178
|
+
for (const session of sessions) {
|
|
179
|
+
if (!session.filePath)
|
|
180
|
+
continue;
|
|
181
|
+
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
182
|
+
const mustStatSource = limits.verifySourceStamps || sourcePath !== session.filePath;
|
|
183
|
+
const indexed = !mustStatSource
|
|
184
|
+
? db.prepare(`
|
|
185
|
+
SELECT file_mtime_ms, file_size FROM sessions WHERE id = ?
|
|
186
|
+
`).get(session.id)
|
|
187
|
+
: undefined;
|
|
188
|
+
let stamp = indexed?.file_mtime_ms != null && indexed.file_size != null
|
|
189
|
+
? { fileMtimeMs: indexed.file_mtime_ms, fileSize: indexed.file_size }
|
|
190
|
+
: undefined;
|
|
191
|
+
if (mustStatSource || !stamp) {
|
|
192
|
+
try {
|
|
193
|
+
const stat = fs.statSync(sourcePath);
|
|
194
|
+
stamp = { fileMtimeMs: stat.mtimeMs, fileSize: stat.size };
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
purgeToolCalls(db, session.id);
|
|
198
|
+
skippedFiles++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (needsIndex(db, session.id, stamp))
|
|
203
|
+
pending.push({ session, stamp });
|
|
204
|
+
}
|
|
205
|
+
let indexedFiles = 0;
|
|
206
|
+
let indexedCalls = 0;
|
|
207
|
+
let consumedBytes = 0;
|
|
208
|
+
let attemptedFiles = 0;
|
|
209
|
+
for (const item of pending) {
|
|
210
|
+
if (attemptedFiles >= maxFiles)
|
|
211
|
+
break;
|
|
212
|
+
// The byte budget is a batch boundary, not a correctness boundary. Admit
|
|
213
|
+
// one oversized transcript by itself so it can never wedge the ledger or
|
|
214
|
+
// silently disappear from results; the next invocation resumes afterward.
|
|
215
|
+
if (attemptedFiles > 0 && consumedBytes + item.stamp.fileSize > maxBytes)
|
|
216
|
+
break;
|
|
217
|
+
attemptedFiles++;
|
|
218
|
+
consumedBytes += item.stamp.fileSize;
|
|
219
|
+
try {
|
|
220
|
+
const calls = await toolCallsForBackfill(item.session, item.stamp.fileSize);
|
|
221
|
+
persistToolCalls(db, item.session, calls, item.stamp);
|
|
222
|
+
indexedFiles++;
|
|
223
|
+
indexedCalls += calls.length;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
skippedFiles++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const remainingFiles = Math.max(0, pending.length - attemptedFiles);
|
|
230
|
+
const limitedSessionIds = new Set();
|
|
231
|
+
const sessionIds = sessions.map((session) => session.id);
|
|
232
|
+
for (let offset = 0; offset < sessionIds.length; offset += 500) {
|
|
233
|
+
const ids = sessionIds.slice(offset, offset + 500);
|
|
234
|
+
if (ids.length === 0)
|
|
235
|
+
continue;
|
|
236
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
237
|
+
const rows = db.prepare(`
|
|
238
|
+
SELECT DISTINCT session_id FROM tool_calls
|
|
239
|
+
WHERE tool = 'index_limit' AND session_id IN (${placeholders})
|
|
240
|
+
`).all(...ids);
|
|
241
|
+
for (const row of rows)
|
|
242
|
+
limitedSessionIds.add(row.session_id);
|
|
243
|
+
}
|
|
244
|
+
const limitedFiles = limitedSessionIds.size;
|
|
245
|
+
return {
|
|
246
|
+
indexedFiles,
|
|
247
|
+
indexedCalls,
|
|
248
|
+
skippedFiles,
|
|
249
|
+
limitedFiles,
|
|
250
|
+
remainingFiles,
|
|
251
|
+
complete: remainingFiles === 0 && skippedFiles === 0 && limitedFiles === 0,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function tokenizeClause(source) {
|
|
255
|
+
const tokens = [];
|
|
256
|
+
let token = '';
|
|
257
|
+
let quote;
|
|
258
|
+
let escaped = false;
|
|
259
|
+
for (const char of source.trim()) {
|
|
260
|
+
if (escaped) {
|
|
261
|
+
token += char;
|
|
262
|
+
escaped = false;
|
|
263
|
+
}
|
|
264
|
+
else if (char === '\\') {
|
|
265
|
+
escaped = true;
|
|
266
|
+
}
|
|
267
|
+
else if (quote) {
|
|
268
|
+
if (char === quote)
|
|
269
|
+
quote = undefined;
|
|
270
|
+
else
|
|
271
|
+
token += char;
|
|
272
|
+
}
|
|
273
|
+
else if (char === '"' || char === "'") {
|
|
274
|
+
quote = char;
|
|
275
|
+
}
|
|
276
|
+
else if (/\s/.test(char)) {
|
|
277
|
+
if (token)
|
|
278
|
+
tokens.push(token);
|
|
279
|
+
token = '';
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
token += char;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (escaped)
|
|
286
|
+
token += '\\';
|
|
287
|
+
if (token)
|
|
288
|
+
tokens.push(token);
|
|
289
|
+
return tokens;
|
|
290
|
+
}
|
|
291
|
+
/** Parse one same-call clause. Unprefixed terms search every evidence field. */
|
|
292
|
+
export function parseToolQueryClause(source) {
|
|
293
|
+
const fields = new Set(['tool', 'program', 'input', 'output', 'status', 'exit', 'error']);
|
|
294
|
+
return tokenizeClause(source).map((token) => {
|
|
295
|
+
const colon = token.indexOf(':');
|
|
296
|
+
if (colon > 0) {
|
|
297
|
+
const candidate = token.slice(0, colon).toLowerCase();
|
|
298
|
+
if (fields.has(candidate)) {
|
|
299
|
+
return { field: candidate, value: token.slice(colon + 1) };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { value: token };
|
|
303
|
+
}).filter((term) => term.value.length > 0);
|
|
304
|
+
}
|
|
305
|
+
/** A count has one unambiguous subject: one exact `program:<name>` term. */
|
|
306
|
+
export function parseToolProgramCountClause(source) {
|
|
307
|
+
const terms = parseToolQueryClause(source);
|
|
308
|
+
if (terms.length !== 1 || terms[0].field !== 'program') {
|
|
309
|
+
throw new Error('--count requires exactly one --query program:<name> clause.');
|
|
310
|
+
}
|
|
311
|
+
return terms[0].value;
|
|
312
|
+
}
|
|
313
|
+
function includes(haystack, needle) {
|
|
314
|
+
return (haystack ?? '').toLocaleLowerCase().includes(needle.toLocaleLowerCase());
|
|
315
|
+
}
|
|
316
|
+
function matchesTerm(call, term) {
|
|
317
|
+
const value = term.value.toLocaleLowerCase();
|
|
318
|
+
switch (term.field) {
|
|
319
|
+
case 'tool': return includes(call.tool, value);
|
|
320
|
+
case 'program': return call.programs.some((program) => program.toLocaleLowerCase() === value);
|
|
321
|
+
case 'input': return includes(call.input, value);
|
|
322
|
+
// A harness can mark the returned bytes as an error while they are still
|
|
323
|
+
// the command's output. Keep `error:` available for explicit diagnostics,
|
|
324
|
+
// but let `output:` match either bounded result channel.
|
|
325
|
+
case 'output': return includes(call.output, value) || includes(call.error, value);
|
|
326
|
+
case 'status': return call.outcome === value || String(call.statusCode ?? '') === value;
|
|
327
|
+
case 'exit': return String(call.exitCode ?? '') === value;
|
|
328
|
+
case 'error': return includes(call.error, value) || includes(call.errorCode, value);
|
|
329
|
+
default:
|
|
330
|
+
return [call.tool, call.input, call.output, call.error, call.errorCode, call.outcome]
|
|
331
|
+
.some((field) => includes(field, value))
|
|
332
|
+
|| call.programs.some((program) => includes(program, value))
|
|
333
|
+
|| String(call.exitCode ?? '') === value
|
|
334
|
+
|| String(call.statusCode ?? '') === value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function distinctClauseAssignment(calls, clauses) {
|
|
338
|
+
if (clauses.length === 0)
|
|
339
|
+
return calls;
|
|
340
|
+
const candidates = clauses.map((clause) => calls.filter((call) => clause.every((term) => matchesTerm(call, term))));
|
|
341
|
+
if (candidates.some((matches) => matches.length === 0))
|
|
342
|
+
return null;
|
|
343
|
+
const order = candidates.map((_, index) => index).sort((a, b) => candidates[a].length - candidates[b].length);
|
|
344
|
+
const selected = new Map();
|
|
345
|
+
const ownerByCall = new Map();
|
|
346
|
+
const assign = (clauseIndex, seenCalls) => {
|
|
347
|
+
for (const call of candidates[clauseIndex]) {
|
|
348
|
+
if (seenCalls.has(call.id))
|
|
349
|
+
continue;
|
|
350
|
+
seenCalls.add(call.id);
|
|
351
|
+
const owner = ownerByCall.get(call.id);
|
|
352
|
+
if (owner === undefined || assign(owner, seenCalls)) {
|
|
353
|
+
ownerByCall.set(call.id, clauseIndex);
|
|
354
|
+
selected.set(clauseIndex, call);
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
};
|
|
360
|
+
for (const clauseIndex of order) {
|
|
361
|
+
if (!assign(clauseIndex, new Set()))
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return clauses.map((_, index) => selected.get(index));
|
|
365
|
+
}
|
|
366
|
+
function intersect(left, right) {
|
|
367
|
+
if (!left)
|
|
368
|
+
return right;
|
|
369
|
+
return new Set([...left].filter((key) => right.has(key)));
|
|
370
|
+
}
|
|
371
|
+
function ftsPhrase(value) {
|
|
372
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
373
|
+
}
|
|
374
|
+
function canUseTrigram(value) {
|
|
375
|
+
return Array.from(value).length >= 3;
|
|
376
|
+
}
|
|
377
|
+
function ftsCandidateRows(db, sessionIds, expression) {
|
|
378
|
+
return db.prepare(`
|
|
379
|
+
SELECT t.call_key FROM tool_call_text t
|
|
380
|
+
JOIN tool_calls c ON c.call_key = t.call_key
|
|
381
|
+
WHERE c.session_id IN (SELECT value FROM json_each(?)) AND tool_call_text MATCH ?
|
|
382
|
+
LIMIT ?
|
|
383
|
+
`).all(JSON.stringify(sessionIds), expression, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
384
|
+
}
|
|
385
|
+
/** Use indexed typed columns and trigram FTS; final matching remains exact in JS. */
|
|
386
|
+
function candidateKeysForClause(db, sessionIds, clause) {
|
|
387
|
+
let candidates;
|
|
388
|
+
const scope = JSON.stringify(sessionIds);
|
|
389
|
+
for (const term of clause) {
|
|
390
|
+
const matches = new Set();
|
|
391
|
+
let rows = [];
|
|
392
|
+
switch (term.field) {
|
|
393
|
+
case 'program':
|
|
394
|
+
rows = db.prepare(`
|
|
395
|
+
SELECT p.call_key FROM tool_call_programs p
|
|
396
|
+
JOIN tool_calls c ON c.call_key = p.call_key
|
|
397
|
+
WHERE c.session_id IN (SELECT value FROM json_each(?)) AND p.program = ? COLLATE NOCASE
|
|
398
|
+
LIMIT ?
|
|
399
|
+
`).all(scope, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
400
|
+
break;
|
|
401
|
+
case 'status':
|
|
402
|
+
rows = db.prepare(`
|
|
403
|
+
SELECT call_key FROM tool_calls
|
|
404
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
405
|
+
AND (outcome = ? COLLATE NOCASE OR CAST(status_code AS TEXT) = ?)
|
|
406
|
+
LIMIT ?
|
|
407
|
+
`).all(scope, term.value, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
408
|
+
break;
|
|
409
|
+
case 'exit':
|
|
410
|
+
rows = db.prepare(`
|
|
411
|
+
SELECT call_key FROM tool_calls
|
|
412
|
+
WHERE session_id IN (SELECT value FROM json_each(?)) AND CAST(exit_code AS TEXT) = ?
|
|
413
|
+
LIMIT ?
|
|
414
|
+
`).all(scope, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
415
|
+
break;
|
|
416
|
+
case 'tool':
|
|
417
|
+
case 'input': {
|
|
418
|
+
const field = term.field;
|
|
419
|
+
rows = canUseTrigram(term.value)
|
|
420
|
+
? ftsCandidateRows(db, sessionIds, `${field}:${ftsPhrase(term.value)}`)
|
|
421
|
+
: db.prepare(`
|
|
422
|
+
SELECT call_key FROM tool_calls
|
|
423
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
424
|
+
AND instr(lower(${field}), lower(?)) > 0
|
|
425
|
+
LIMIT ?
|
|
426
|
+
`).all(scope, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case 'output':
|
|
430
|
+
rows = canUseTrigram(term.value)
|
|
431
|
+
? ftsCandidateRows(db, sessionIds, `(output:${ftsPhrase(term.value)} OR error:${ftsPhrase(term.value)})`)
|
|
432
|
+
: db.prepare(`
|
|
433
|
+
SELECT call_key FROM tool_calls
|
|
434
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
435
|
+
AND (instr(lower(coalesce(output, '')), lower(?)) > 0
|
|
436
|
+
OR instr(lower(coalesce(error, '')), lower(?)) > 0)
|
|
437
|
+
LIMIT ?
|
|
438
|
+
`).all(scope, term.value, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
439
|
+
break;
|
|
440
|
+
case 'error':
|
|
441
|
+
if (canUseTrigram(term.value)) {
|
|
442
|
+
rows = ftsCandidateRows(db, sessionIds, `error:${ftsPhrase(term.value)}`);
|
|
443
|
+
rows.push(...db.prepare(`
|
|
444
|
+
SELECT call_key FROM tool_calls
|
|
445
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
446
|
+
AND instr(lower(coalesce(error_code, '')), lower(?)) > 0
|
|
447
|
+
LIMIT ?
|
|
448
|
+
`).all(scope, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1));
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
rows = db.prepare(`
|
|
452
|
+
SELECT call_key FROM tool_calls
|
|
453
|
+
WHERE session_id IN (SELECT value FROM json_each(?))
|
|
454
|
+
AND (instr(lower(coalesce(error, '')), lower(?)) > 0
|
|
455
|
+
OR instr(lower(coalesce(error_code, '')), lower(?)) > 0)
|
|
456
|
+
LIMIT ?
|
|
457
|
+
`).all(scope, term.value, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
default:
|
|
461
|
+
if (canUseTrigram(term.value)) {
|
|
462
|
+
rows = ftsCandidateRows(db, sessionIds, ftsPhrase(term.value));
|
|
463
|
+
rows.push(...db.prepare(`
|
|
464
|
+
SELECT c.call_key FROM tool_calls c
|
|
465
|
+
WHERE c.session_id IN (SELECT value FROM json_each(?)) AND (
|
|
466
|
+
instr(lower(coalesce(c.error_code, '')), lower(?)) > 0
|
|
467
|
+
OR instr(lower(c.outcome), lower(?)) > 0
|
|
468
|
+
OR CAST(c.exit_code AS TEXT) = ?
|
|
469
|
+
OR CAST(c.status_code AS TEXT) = ?
|
|
470
|
+
OR EXISTS (
|
|
471
|
+
SELECT 1 FROM tool_call_programs p
|
|
472
|
+
WHERE p.call_key = c.call_key AND instr(lower(p.program), lower(?)) > 0
|
|
473
|
+
)
|
|
474
|
+
) LIMIT ?
|
|
475
|
+
`).all(scope, term.value, term.value, term.value, term.value, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
rows = db.prepare(`
|
|
479
|
+
SELECT c.call_key FROM tool_calls c
|
|
480
|
+
WHERE c.session_id IN (SELECT value FROM json_each(?)) AND (
|
|
481
|
+
instr(lower(c.tool), lower(?)) > 0
|
|
482
|
+
OR instr(lower(c.input), lower(?)) > 0
|
|
483
|
+
OR instr(lower(coalesce(c.output, '')), lower(?)) > 0
|
|
484
|
+
OR instr(lower(coalesce(c.error, '')), lower(?)) > 0
|
|
485
|
+
OR instr(lower(coalesce(c.error_code, '')), lower(?)) > 0
|
|
486
|
+
OR instr(lower(c.outcome), lower(?)) > 0
|
|
487
|
+
OR CAST(c.exit_code AS TEXT) = ?
|
|
488
|
+
OR CAST(c.status_code AS TEXT) = ?
|
|
489
|
+
OR EXISTS (
|
|
490
|
+
SELECT 1 FROM tool_call_programs p
|
|
491
|
+
WHERE p.call_key = c.call_key AND instr(lower(p.program), lower(?)) > 0
|
|
492
|
+
)
|
|
493
|
+
) LIMIT ?
|
|
494
|
+
`).all(scope, term.value, term.value, term.value, term.value, term.value, term.value, term.value, term.value, term.value, TOOL_QUERY_MAX_CALL_ROWS + 1);
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
for (const row of rows)
|
|
499
|
+
matches.add(row.call_key);
|
|
500
|
+
if (matches.size > TOOL_QUERY_MAX_CALL_ROWS) {
|
|
501
|
+
throw new Error(`Tool query matched more than ${TOOL_QUERY_MAX_CALL_ROWS.toLocaleString()} call rows; add a more specific term.`);
|
|
502
|
+
}
|
|
503
|
+
candidates = intersect(candidates, matches);
|
|
504
|
+
if (candidates.size === 0)
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
return candidates ?? new Set();
|
|
508
|
+
}
|
|
509
|
+
function programsForRows(db, rows) {
|
|
510
|
+
const programs = new Map();
|
|
511
|
+
const keys = rows.map((row) => row.call_key);
|
|
512
|
+
if (keys.length === 0)
|
|
513
|
+
return programs;
|
|
514
|
+
const programRows = db.prepare(`
|
|
515
|
+
SELECT call_key, program FROM tool_call_programs
|
|
516
|
+
WHERE call_key IN (SELECT value FROM json_each(?)) ORDER BY program
|
|
517
|
+
`).all(JSON.stringify(keys));
|
|
518
|
+
for (const row of programRows) {
|
|
519
|
+
const list = programs.get(row.call_key) ?? [];
|
|
520
|
+
list.push(row.program);
|
|
521
|
+
programs.set(row.call_key, list);
|
|
522
|
+
}
|
|
523
|
+
return programs;
|
|
524
|
+
}
|
|
525
|
+
function occurrencesForRows(db, rows) {
|
|
526
|
+
const occurrences = new Map();
|
|
527
|
+
const keys = rows.map((row) => row.call_key);
|
|
528
|
+
if (keys.length === 0)
|
|
529
|
+
return occurrences;
|
|
530
|
+
const occurrenceRows = db.prepare(`
|
|
531
|
+
SELECT call_key, program, role FROM tool_program_occurrences
|
|
532
|
+
WHERE call_key IN (SELECT value FROM json_each(?))
|
|
533
|
+
ORDER BY call_key, occurrence_ordinal
|
|
534
|
+
`).all(JSON.stringify(keys));
|
|
535
|
+
for (const row of occurrenceRows) {
|
|
536
|
+
const list = occurrences.get(row.call_key) ?? [];
|
|
537
|
+
list.push({ program: row.program, role: row.role });
|
|
538
|
+
occurrences.set(row.call_key, list);
|
|
539
|
+
}
|
|
540
|
+
return occurrences;
|
|
541
|
+
}
|
|
542
|
+
function appendStoredRows(out, rows, programs, occurrences, loadedBytes) {
|
|
543
|
+
for (const row of rows) {
|
|
544
|
+
const call = {
|
|
545
|
+
id: row.call_key,
|
|
546
|
+
ordinal: row.ordinal,
|
|
547
|
+
sourceCallId: row.source_call_id ?? undefined,
|
|
548
|
+
timestamp: row.timestamp,
|
|
549
|
+
tool: row.tool,
|
|
550
|
+
programs: programs.get(row.call_key) ?? [],
|
|
551
|
+
programOccurrences: occurrences.get(row.call_key) ?? [],
|
|
552
|
+
input: row.input,
|
|
553
|
+
outcome: row.outcome,
|
|
554
|
+
exitCode: row.exit_code ?? undefined,
|
|
555
|
+
statusCode: row.status_code ?? undefined,
|
|
556
|
+
errorCode: row.error_code ?? undefined,
|
|
557
|
+
output: row.output ?? undefined,
|
|
558
|
+
error: row.error ?? undefined,
|
|
559
|
+
parseError: row.parse_error ?? undefined,
|
|
560
|
+
};
|
|
561
|
+
loadedBytes += toolCallEvidenceBytes(call);
|
|
562
|
+
if (loadedBytes > TOOL_QUERY_MAX_RESULT_BYTES) {
|
|
563
|
+
throw new Error(`Tool result exceeds ${TOOL_QUERY_MAX_RESULT_BYTES / 1024 / 1024} MiB; reduce --limit or add a more specific term.`);
|
|
564
|
+
}
|
|
565
|
+
const list = out.get(row.session_id) ?? [];
|
|
566
|
+
list.push(call);
|
|
567
|
+
out.set(row.session_id, list);
|
|
568
|
+
}
|
|
569
|
+
return loadedBytes;
|
|
570
|
+
}
|
|
571
|
+
function readCalls(db, sessionIds) {
|
|
572
|
+
const out = new Map();
|
|
573
|
+
const PAGE = 250;
|
|
574
|
+
let totalRows = 0;
|
|
575
|
+
let loadedBytes = 0;
|
|
576
|
+
for (const sessionId of sessionIds) {
|
|
577
|
+
let afterOrdinal = -1;
|
|
578
|
+
for (;;) {
|
|
579
|
+
const rows = db.prepare(`
|
|
580
|
+
SELECT * FROM tool_calls
|
|
581
|
+
WHERE session_id = ? AND ordinal > ?
|
|
582
|
+
ORDER BY ordinal LIMIT ?
|
|
583
|
+
`).all(sessionId, afterOrdinal, PAGE);
|
|
584
|
+
if (rows.length === 0)
|
|
585
|
+
break;
|
|
586
|
+
totalRows += rows.length;
|
|
587
|
+
if (totalRows > TOOL_QUERY_MAX_CALL_ROWS) {
|
|
588
|
+
throw new Error(`Tool listing exceeds ${TOOL_QUERY_MAX_CALL_ROWS.toLocaleString()} call rows; reduce --limit or add --query.`);
|
|
589
|
+
}
|
|
590
|
+
loadedBytes = appendStoredRows(out, rows, programsForRows(db, rows), occurrencesForRows(db, rows), loadedBytes);
|
|
591
|
+
afterOrdinal = rows.at(-1).ordinal;
|
|
592
|
+
if (rows.length < PAGE)
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
function readCallsByKeys(db, keys) {
|
|
599
|
+
if (keys.size === 0)
|
|
600
|
+
return new Map();
|
|
601
|
+
const out = new Map();
|
|
602
|
+
const allKeys = [...keys];
|
|
603
|
+
let loadedBytes = 0;
|
|
604
|
+
const rows = db.prepare(`
|
|
605
|
+
SELECT * FROM tool_calls
|
|
606
|
+
WHERE call_key IN (SELECT value FROM json_each(?))
|
|
607
|
+
ORDER BY session_id, ordinal
|
|
608
|
+
`).all(JSON.stringify(allKeys));
|
|
609
|
+
loadedBytes = appendStoredRows(out, rows, programsForRows(db, rows), occurrencesForRows(db, rows), loadedBytes);
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
/** Count exact, pre-indexed static program sites without reading transcripts. */
|
|
613
|
+
export function countToolProgramOccurrences(sessions, program, coverage, machine) {
|
|
614
|
+
const normalized = program.trim();
|
|
615
|
+
if (!normalized || Buffer.byteLength(normalized) > 512) {
|
|
616
|
+
throw new Error('Tool program count requires one program name from 1 to 512 bytes.');
|
|
617
|
+
}
|
|
618
|
+
const db = getDB();
|
|
619
|
+
const ids = sessions.map((session) => session.id);
|
|
620
|
+
const rows = ids.length === 0 ? [] : db.prepare(`
|
|
621
|
+
SELECT coalesce(nullif(s.machine, ''), ?) AS machine,
|
|
622
|
+
count(*) AS occurrences,
|
|
623
|
+
count(DISTINCT o.call_key) AS toolCalls,
|
|
624
|
+
count(DISTINCT c.session_id) AS sessions
|
|
625
|
+
FROM tool_program_occurrences o
|
|
626
|
+
JOIN tool_calls c ON c.call_key = o.call_key
|
|
627
|
+
JOIN sessions s ON s.id = c.session_id
|
|
628
|
+
WHERE c.session_id IN (SELECT value FROM json_each(?))
|
|
629
|
+
AND o.program = ? COLLATE NOCASE
|
|
630
|
+
GROUP BY coalesce(nullif(s.machine, ''), ?)
|
|
631
|
+
`).all(machine, JSON.stringify(ids), normalized, machine);
|
|
632
|
+
const totals = rows.reduce((sum, row) => ({
|
|
633
|
+
occurrences: sum.occurrences + row.occurrences,
|
|
634
|
+
toolCalls: sum.toolCalls + row.toolCalls,
|
|
635
|
+
sessions: sum.sessions + row.sessions,
|
|
636
|
+
}), { occurrences: 0, toolCalls: 0, sessions: 0 });
|
|
637
|
+
const sessionsByMachine = new Map();
|
|
638
|
+
for (const session of sessions) {
|
|
639
|
+
const origin = session.machine ?? machine;
|
|
640
|
+
const group = sessionsByMachine.get(origin) ?? [];
|
|
641
|
+
group.push(session);
|
|
642
|
+
sessionsByMachine.set(origin, group);
|
|
643
|
+
}
|
|
644
|
+
const coverageRows = ids.length === 0 ? [] : db.prepare(`
|
|
645
|
+
WITH scoped AS (
|
|
646
|
+
SELECT id, coalesce(nullif(machine, ''), ?) AS machine
|
|
647
|
+
FROM sessions
|
|
648
|
+
WHERE file_path <> ''
|
|
649
|
+
AND id IN (SELECT value FROM json_each(?))
|
|
650
|
+
)
|
|
651
|
+
SELECT
|
|
652
|
+
scoped.machine,
|
|
653
|
+
count(*) AS scope_files,
|
|
654
|
+
count(ledger.session_id) AS indexed_files,
|
|
655
|
+
coalesce(sum(ledger.call_count), 0) AS indexed_calls,
|
|
656
|
+
count(limited.call_key) AS limited_files
|
|
657
|
+
FROM scoped
|
|
658
|
+
LEFT JOIN tool_scan_ledger ledger
|
|
659
|
+
ON ledger.session_id = scoped.id
|
|
660
|
+
AND ledger.extractor_version = ?
|
|
661
|
+
LEFT JOIN tool_calls limited
|
|
662
|
+
ON limited.session_id = scoped.id
|
|
663
|
+
AND limited.tool = 'index_limit'
|
|
664
|
+
GROUP BY scoped.machine
|
|
665
|
+
`).all(machine, JSON.stringify(ids), TOOL_INDEX_VERSION);
|
|
666
|
+
const coverageByMachine = new Map(coverageRows.map((row) => {
|
|
667
|
+
const remainingFiles = Math.max(0, row.scope_files - row.indexed_files);
|
|
668
|
+
return [row.machine, {
|
|
669
|
+
indexedFiles: row.indexed_files,
|
|
670
|
+
indexedCalls: row.indexed_calls,
|
|
671
|
+
skippedFiles: 0,
|
|
672
|
+
limitedFiles: row.limited_files,
|
|
673
|
+
remainingFiles,
|
|
674
|
+
complete: remainingFiles === 0 && row.limited_files === 0,
|
|
675
|
+
}];
|
|
676
|
+
}));
|
|
677
|
+
const machineTotals = new Map(rows.map((row) => [row.machine, {
|
|
678
|
+
occurrences: row.occurrences,
|
|
679
|
+
toolCalls: row.toolCalls,
|
|
680
|
+
sessions: row.sessions,
|
|
681
|
+
}]));
|
|
682
|
+
const machines = [...sessionsByMachine.keys()].map((origin) => ({
|
|
683
|
+
machine: origin,
|
|
684
|
+
coverage: coverageByMachine.get(origin) ?? {
|
|
685
|
+
indexedFiles: 0,
|
|
686
|
+
indexedCalls: 0,
|
|
687
|
+
skippedFiles: 0,
|
|
688
|
+
limitedFiles: 0,
|
|
689
|
+
remainingFiles: 0,
|
|
690
|
+
complete: true,
|
|
691
|
+
},
|
|
692
|
+
totals: machineTotals.get(origin) ?? { occurrences: 0, toolCalls: 0, sessions: 0 },
|
|
693
|
+
}));
|
|
694
|
+
if (machines.length === 0)
|
|
695
|
+
machines.push({ machine, coverage, totals });
|
|
696
|
+
return {
|
|
697
|
+
schemaVersion: TOOL_PROGRAM_COUNT_SCHEMA_VERSION,
|
|
698
|
+
kind: 'tool-program-count',
|
|
699
|
+
generatedAt: new Date().toISOString(),
|
|
700
|
+
query: { program: normalized, semantics: 'static-program-occurrences-v1' },
|
|
701
|
+
coverage,
|
|
702
|
+
totals,
|
|
703
|
+
machines,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
/** Query cached evidence; every repeated clause must select a distinct call. */
|
|
707
|
+
export function searchToolCalls(sessions, clauseSources, coverage, resultLimit = sessions.length) {
|
|
708
|
+
if (!Number.isSafeInteger(resultLimit) || resultLimit < 1 || resultLimit > TOOL_QUERY_MAX_RESULT_SESSIONS) {
|
|
709
|
+
throw new Error(`Tool search --limit must be from 1 to ${TOOL_QUERY_MAX_RESULT_SESSIONS}.`);
|
|
710
|
+
}
|
|
711
|
+
if (clauseSources.length > TOOL_QUERY_MAX_CLAUSES) {
|
|
712
|
+
throw new Error(`Tool search accepts at most ${TOOL_QUERY_MAX_CLAUSES} --query clauses.`);
|
|
713
|
+
}
|
|
714
|
+
if (clauseSources.some((clause) => Buffer.byteLength(clause) > TOOL_QUERY_MAX_CLAUSE_BYTES)) {
|
|
715
|
+
throw new Error(`Each tool --query clause is limited to ${TOOL_QUERY_MAX_CLAUSE_BYTES} bytes.`);
|
|
716
|
+
}
|
|
717
|
+
const db = getDB();
|
|
718
|
+
const clauses = clauseSources.map(parseToolQueryClause);
|
|
719
|
+
const searchedSessions = clauses.length === 0 ? sessions.slice(0, resultLimit) : sessions;
|
|
720
|
+
const sessionIds = searchedSessions.map((session) => session.id);
|
|
721
|
+
let candidateKeys;
|
|
722
|
+
if (clauses.length > 0) {
|
|
723
|
+
candidateKeys = new Set();
|
|
724
|
+
for (const clause of clauses) {
|
|
725
|
+
for (const key of candidateKeysForClause(db, sessionIds, clause)) {
|
|
726
|
+
candidateKeys.add(key);
|
|
727
|
+
if (candidateKeys.size > TOOL_QUERY_MAX_CALL_ROWS) {
|
|
728
|
+
throw new Error(`Tool query requires more than ${TOOL_QUERY_MAX_CALL_ROWS.toLocaleString()} candidate calls; add a more specific term.`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const callsBySession = clauses.length === 0
|
|
734
|
+
? readCalls(db, sessionIds)
|
|
735
|
+
: readCallsByKeys(db, candidateKeys);
|
|
736
|
+
const matched = [];
|
|
737
|
+
let resultBytes = 0;
|
|
738
|
+
for (const session of searchedSessions) {
|
|
739
|
+
const calls = callsBySession.get(session.id) ?? [];
|
|
740
|
+
const selected = distinctClauseAssignment(calls, clauses);
|
|
741
|
+
if (!selected || selected.length === 0)
|
|
742
|
+
continue;
|
|
743
|
+
resultBytes += Buffer.byteLength([
|
|
744
|
+
session.id, session.shortId, session.agent, session.machine, session.timestamp,
|
|
745
|
+
session.project, session.cwd, session.topic, session.label,
|
|
746
|
+
].filter((value) => typeof value === 'string').join('\0'));
|
|
747
|
+
resultBytes += selected.reduce((total, call) => total + toolCallEvidenceBytes(call), 0);
|
|
748
|
+
if (resultBytes > TOOL_QUERY_MAX_RESULT_BYTES) {
|
|
749
|
+
throw new Error(`Tool result exceeds ${TOOL_QUERY_MAX_RESULT_BYTES / 1024 / 1024} MiB; reduce --limit or add a more specific term.`);
|
|
750
|
+
}
|
|
751
|
+
matched.push({
|
|
752
|
+
id: session.id,
|
|
753
|
+
shortId: session.shortId,
|
|
754
|
+
agent: session.agent,
|
|
755
|
+
machine: session.machine,
|
|
756
|
+
timestamp: session.timestamp,
|
|
757
|
+
project: session.project,
|
|
758
|
+
cwd: session.cwd,
|
|
759
|
+
topic: session.topic,
|
|
760
|
+
label: session.label,
|
|
761
|
+
calls: selected,
|
|
762
|
+
});
|
|
763
|
+
if (matched.length >= resultLimit)
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
schemaVersion: TOOL_SEARCH_SCHEMA_VERSION,
|
|
768
|
+
generatedAt: new Date().toISOString(),
|
|
769
|
+
query: { clauses: clauseSources },
|
|
770
|
+
coverage,
|
|
771
|
+
sessions: matched,
|
|
772
|
+
};
|
|
773
|
+
}
|