pi-maestro-teammate 2.8.1 → 2.9.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/package.json +2 -2
- package/src/extension/teammate-helpers.ts +2 -1
- package/src/runs/execution-infra.ts +30 -7
- package/src/runs/pi-subprocess-attempt.ts +41 -14
- package/src/runs/rpc-stdout-projection.mjs +51 -0
- package/src/transcript/session-history.ts +96 -18
- package/types/runs/execution-infra.d.ts +5 -1
- package/types/transcript/session-history.d.ts +11 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
},
|
|
178
178
|
"files": [
|
|
179
179
|
"bin/*.mjs",
|
|
180
|
-
"src/**/*.{ts,json}",
|
|
180
|
+
"src/**/*.{ts,mjs,json}",
|
|
181
181
|
"types/**/*.d.ts",
|
|
182
182
|
"agents/",
|
|
183
183
|
"README.md"
|
|
@@ -2148,7 +2148,8 @@ export async function handleChildInteractionRequest(
|
|
|
2148
2148
|
const details = isRecord(response.details) ? response.details : {};
|
|
2149
2149
|
result = response.isError || details.cancelled || !Array.isArray(details.answers)
|
|
2150
2150
|
? { action: "cancel", error: response.isError ? "Root question broker failed" : undefined }
|
|
2151
|
-
: { action: "answer", answers: details.answers, ...(Array.isArray(details.decisions) ? { decisions: details.decisions } : {})
|
|
2151
|
+
: { action: "answer", answers: details.answers, ...(Array.isArray(details.decisions) ? { decisions: details.decisions } : {}),
|
|
2152
|
+
...(Array.isArray(details.recommendations) ? { recommendations: details.recommendations } : {}) };
|
|
2152
2153
|
} else if (!ctx?.hasUI) {
|
|
2153
2154
|
decisionSource = "headless";
|
|
2154
2155
|
result = interaction === "permission" ? { action: "deny" } : { action: "cancel" };
|
|
@@ -868,27 +868,50 @@ export function appendBoundedTranscriptMessage(
|
|
|
868
868
|
|
|
869
869
|
export function createUtf8LineDecoder(
|
|
870
870
|
maxBufferedBytes = EXECUTION_BUFFER_LIMITS.lineBytes,
|
|
871
|
+
onOverflow?: (info: { receivedBytes: number; maxBytes: number; prefix: string }) => void,
|
|
872
|
+
onLine?: (line: string) => void,
|
|
871
873
|
): Utf8LineDecoder {
|
|
872
874
|
const decoder = new StringDecoder("utf8");
|
|
873
875
|
let buffered = "";
|
|
876
|
+
let receivedBytes = 0;
|
|
877
|
+
let prefix = "";
|
|
878
|
+
let overflowReported = false;
|
|
879
|
+
const append = (text: string): void => {
|
|
880
|
+
receivedBytes += Buffer.byteLength(text);
|
|
881
|
+
if (Buffer.byteLength(prefix) < 256) prefix = truncateUtf8Head(prefix + text.slice(0, 256), 256);
|
|
882
|
+
buffered = appendUtf8Tail(buffered, text, maxBufferedBytes);
|
|
883
|
+
if (!overflowReported && receivedBytes > maxBufferedBytes) {
|
|
884
|
+
overflowReported = true;
|
|
885
|
+
onOverflow?.({ receivedBytes, maxBytes: maxBufferedBytes, prefix });
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
const emit = (lines: string[]): void => {
|
|
889
|
+
const line = buffered.endsWith("\r") ? buffered.slice(0, -1) : buffered;
|
|
890
|
+
if (onLine) onLine(line);
|
|
891
|
+
else lines.push(line);
|
|
892
|
+
buffered = "";
|
|
893
|
+
receivedBytes = 0;
|
|
894
|
+
prefix = "";
|
|
895
|
+
overflowReported = false;
|
|
896
|
+
};
|
|
874
897
|
return {
|
|
875
898
|
write(chunk: Buffer): string[] {
|
|
876
899
|
const text = decoder.write(chunk);
|
|
877
900
|
const lines: string[] = [];
|
|
878
901
|
let start = 0;
|
|
879
902
|
for (let newline = text.indexOf("\n"); newline !== -1; newline = text.indexOf("\n", start)) {
|
|
880
|
-
|
|
881
|
-
lines
|
|
882
|
-
buffered = "";
|
|
903
|
+
append(text.slice(start, newline));
|
|
904
|
+
emit(lines);
|
|
883
905
|
start = newline + 1;
|
|
884
906
|
}
|
|
885
|
-
|
|
907
|
+
append(text.slice(start));
|
|
886
908
|
return lines;
|
|
887
909
|
},
|
|
888
910
|
end(): string[] {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
911
|
+
append(decoder.end());
|
|
912
|
+
const lines: string[] = [];
|
|
913
|
+
if (buffered) emit(lines);
|
|
914
|
+
return lines;
|
|
892
915
|
},
|
|
893
916
|
};
|
|
894
917
|
}
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
releasePublishedTurnHistory,
|
|
107
107
|
resetUsage,
|
|
108
108
|
setUsageSnapshot,
|
|
109
|
+
truncateUtf8Head,
|
|
109
110
|
truncateUtf8Tail,
|
|
110
111
|
writeSchemaFile,
|
|
111
112
|
writeSystemPromptFile,
|
|
@@ -875,6 +876,10 @@ export async function runSingleAttempt(
|
|
|
875
876
|
forkSessionFile,
|
|
876
877
|
params.fast ?? agentConfig.fast,
|
|
877
878
|
);
|
|
879
|
+
const projectionImport = `--import=${new URL("./rpc-stdout-projection.mjs", import.meta.url).href}`;
|
|
880
|
+
if (!spawnEnv.NODE_OPTIONS?.split(/\s+/).includes(projectionImport)) {
|
|
881
|
+
spawnEnv.NODE_OPTIONS = [spawnEnv.NODE_OPTIONS, projectionImport].filter(Boolean).join(" ");
|
|
882
|
+
}
|
|
878
883
|
const spawnSpec = getPiSpawnCommand(piArgs);
|
|
879
884
|
const probedChildVersion = options.spawnChildProcess
|
|
880
885
|
? options.childPiVersion
|
|
@@ -1141,8 +1146,17 @@ export async function runSingleAttempt(
|
|
|
1141
1146
|
// Parse JSON lines from stdout. Pi's agent_end event can repeat the full
|
|
1142
1147
|
// message history even without structured output, so framing must allow the
|
|
1143
1148
|
// bounded transcript plus JSON escaping and event metadata in every lane.
|
|
1144
|
-
const
|
|
1149
|
+
const failStdoutProtocol = (reason: string): void => {
|
|
1150
|
+
if (state.terminal) return;
|
|
1151
|
+
state.receivedFirstActivity = true;
|
|
1152
|
+
state.stdoutProtocolViolation = true;
|
|
1153
|
+
addReplayEvidence("stdout-protocol", reason);
|
|
1154
|
+
state.runtimeFailure = `Teammate stdout JSONL protocol violation (agent=${params.agent}, correlationId=${correlationId}): ${reason}`;
|
|
1155
|
+
appendBoundedTranscriptMessage(messages, { role: "system", content: state.runtimeFailure });
|
|
1156
|
+
completeTurn(undefined, true, 1, "failed");
|
|
1157
|
+
};
|
|
1145
1158
|
const processStdoutLine = (line: string) => {
|
|
1159
|
+
if (state.terminal) return;
|
|
1146
1160
|
const trimmed = line.trim();
|
|
1147
1161
|
if (!trimmed) {
|
|
1148
1162
|
state.receivedFirstActivity = true;
|
|
@@ -1150,24 +1164,34 @@ export async function runSingleAttempt(
|
|
|
1150
1164
|
if (timers.firstActivity) clearTimeout(timers.firstActivity);
|
|
1151
1165
|
return;
|
|
1152
1166
|
}
|
|
1167
|
+
let event: JsonLineEvent;
|
|
1168
|
+
try {
|
|
1169
|
+
event = JSON.parse(trimmed) as JsonLineEvent;
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
failStdoutProtocol(`malformed JSON, receivedBytes=${Buffer.byteLength(line)}: ${error instanceof Error ? truncateUtf8Head(error.message, 256) : String(error)}`);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") {
|
|
1175
|
+
failStdoutProtocol(`invalid event envelope, receivedBytes=${Buffer.byteLength(line)}`);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1153
1178
|
try {
|
|
1154
|
-
const event = JSON.parse(trimmed) as JsonLineEvent;
|
|
1155
1179
|
processEvent(event);
|
|
1156
|
-
} catch {
|
|
1157
|
-
|
|
1158
|
-
addReplayEvidence("stdout-protocol", "malformed-json-line");
|
|
1159
|
-
state.stdoutProtocolViolation = true;
|
|
1160
|
-
if (timers.firstActivity) clearTimeout(timers.firstActivity);
|
|
1161
|
-
state.lastContent = appendUtf8Tail(
|
|
1162
|
-
state.lastContent,
|
|
1163
|
-
trimmed + "\n",
|
|
1164
|
-
EXECUTION_BUFFER_LIMITS.streamBytes,
|
|
1165
|
-
);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
failStdoutProtocol(`event processing failed, event=${truncateUtf8Head(event.type, 64)}: ${error instanceof Error ? truncateUtf8Head(error.message, 256) : String(error)}`);
|
|
1166
1182
|
}
|
|
1167
1183
|
};
|
|
1184
|
+
const stdoutLines = createUtf8LineDecoder(
|
|
1185
|
+
EXECUTION_BUFFER_LIMITS.transcriptBytes * 4,
|
|
1186
|
+
({ receivedBytes, maxBytes, prefix }) => {
|
|
1187
|
+
const eventType = /"type"\s*:\s*"([\w-]{1,64})"/.exec(prefix)?.[1] ?? "unknown";
|
|
1188
|
+
failStdoutProtocol(`oversized event=${eventType}, receivedBytes=${receivedBytes}, limitBytes=${maxBytes}`);
|
|
1189
|
+
},
|
|
1190
|
+
processStdoutLine,
|
|
1191
|
+
);
|
|
1168
1192
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
1169
1193
|
pokeLifecycleDeadline();
|
|
1170
|
-
|
|
1194
|
+
stdoutLines.write(chunk);
|
|
1171
1195
|
});
|
|
1172
1196
|
|
|
1173
1197
|
const clearInterrupt = (): PendingInterrupt | undefined => {
|
|
@@ -1358,7 +1382,10 @@ export async function runSingleAttempt(
|
|
|
1358
1382
|
exitCode = 0,
|
|
1359
1383
|
terminalStatus: AgentTerminalStatus = exitCode === 0 ? "completed" : "failed",
|
|
1360
1384
|
): void {
|
|
1361
|
-
if (state.terminal
|
|
1385
|
+
if (state.terminal) return;
|
|
1386
|
+
// A settled result stays published, but a later corrupt RPC line must
|
|
1387
|
+
// retire the still-live runtime and report a failed lifecycle.
|
|
1388
|
+
if (state.turnLifecycleSettled && !(state.stdoutProtocolViolation && terminalStatus === "failed")) return;
|
|
1362
1389
|
if (state.stdoutProtocolViolation && exitCode === 0) {
|
|
1363
1390
|
appendBoundedTranscriptMessage(messages, {
|
|
1364
1391
|
role: "system",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The SDK serializes whole RPC events, including repeated tool-result images.
|
|
2
|
+
// Project only telemetry; the model and persisted session retain the originals.
|
|
3
|
+
function projectContent(value) {
|
|
4
|
+
if (!value || !Array.isArray(value.content)) return value;
|
|
5
|
+
let changed = false;
|
|
6
|
+
const content = value.content.map((part) => {
|
|
7
|
+
if (part?.type !== "image" || typeof part.data !== "string") return part;
|
|
8
|
+
changed = true;
|
|
9
|
+
return { type: "text", text: `[image omitted from teammate telemetry: ${Buffer.byteLength(part.data)} encoded bytes]` };
|
|
10
|
+
});
|
|
11
|
+
return changed ? { ...value, content } : value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function projectRpcEvent(event) {
|
|
15
|
+
const projected = { ...event };
|
|
16
|
+
for (const key of ["message", "result", "partialResult"]) {
|
|
17
|
+
if (event[key]) projected[key] = projectContent(event[key]);
|
|
18
|
+
}
|
|
19
|
+
for (const key of ["messages", "toolResults"]) {
|
|
20
|
+
if (Array.isArray(event[key])) projected[key] = event[key].map(projectContent);
|
|
21
|
+
}
|
|
22
|
+
return projected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function projectRpcWrite(chunk) {
|
|
26
|
+
if (typeof chunk !== "string" || !chunk.endsWith("\n") || !chunk.includes('"image"')) return chunk;
|
|
27
|
+
// RPC writes one complete JSONL event per write. Preserve malformed writes
|
|
28
|
+
// verbatim so the parent remains the protocol-failure authority.
|
|
29
|
+
let event;
|
|
30
|
+
try {
|
|
31
|
+
event = JSON.parse(chunk);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof SyntaxError) return chunk;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
if (!event || typeof event !== "object" || typeof event.type !== "string") return chunk;
|
|
37
|
+
return `${JSON.stringify(projectRpcEvent(event))}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function installRpcStdoutProjection(stream) {
|
|
41
|
+
const originalWrite = stream.write;
|
|
42
|
+
stream.write = function (chunk, encodingOrCallback, callback) {
|
|
43
|
+
return originalWrite.call(this, projectRpcWrite(chunk), encodingOrCallback, callback);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A child inherits NODE_OPTIONS into tools too; never intercept their stdout.
|
|
48
|
+
if (process.env.PI_TEAMMATE_CHILD === "1"
|
|
49
|
+
&& process.argv.some((arg, index, args) => arg === "--mode" && args[index + 1] === "rpc")) {
|
|
50
|
+
installRpcStdoutProjection(process.stdout);
|
|
51
|
+
}
|
|
@@ -98,7 +98,14 @@ export interface SessionHistoryOptions {
|
|
|
98
98
|
signal?: AbortSignal;
|
|
99
99
|
}
|
|
100
100
|
export type SessionHistoryListOptions = SessionHistoryOptions;
|
|
101
|
-
export
|
|
101
|
+
export interface SessionHistorySearchOptions extends SessionHistoryOptions {
|
|
102
|
+
/** Literal matching is the compatibility default; terms require every unique normalized term in one projected entry. */
|
|
103
|
+
mode?: "literal" | "terms";
|
|
104
|
+
/** Relevance ranks exact text, continuous phrase, then the tightest term span; ties retain source order. */
|
|
105
|
+
sort?: "source" | "relevance";
|
|
106
|
+
/** Zero-based match offset within this fresh scan (not a snapshot cursor). */
|
|
107
|
+
offset?: number;
|
|
108
|
+
}
|
|
102
109
|
export type SessionHistoryReadOptions = SessionHistoryOptions;
|
|
103
110
|
export type SessionHistoryCompactionOptions = Omit<SessionHistoryOptions, "include">;
|
|
104
111
|
|
|
@@ -161,7 +168,10 @@ export interface SessionHistorySearchResult extends ScanMetrics {
|
|
|
161
168
|
generation?: number;
|
|
162
169
|
query: string;
|
|
163
170
|
matches: readonly SessionHistorySearchMatch[];
|
|
171
|
+
/** Total matches in this scan, independent of pagination. */
|
|
164
172
|
matchCount: number;
|
|
173
|
+
hasMore: boolean;
|
|
174
|
+
nextOffset?: number;
|
|
165
175
|
}
|
|
166
176
|
export interface SessionHistoryCompactionResult extends ScanMetrics {
|
|
167
177
|
version: typeof SESSION_HISTORY_VERSION;
|
|
@@ -251,6 +261,7 @@ export class SessionHistoryService {
|
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
async list(options: SessionHistoryListOptions = {}): Promise<SessionHistoryListResult> {
|
|
264
|
+
rejectSearchOptions(options);
|
|
254
265
|
const include = normalizedInclude(options.include);
|
|
255
266
|
const limit = boundedLimit(options.limit);
|
|
256
267
|
const scan = await this.#scan(include, options.signal);
|
|
@@ -268,36 +279,57 @@ export class SessionHistoryService {
|
|
|
268
279
|
|
|
269
280
|
async search(query: string, options: SessionHistorySearchOptions = {}): Promise<SessionHistorySearchResult> {
|
|
270
281
|
validateQuery(query);
|
|
282
|
+
const mode = options.mode ?? "literal";
|
|
283
|
+
const sort = options.sort ?? "source";
|
|
284
|
+
if (mode !== "literal" && mode !== "terms") throw new TypeError("mode must be literal or terms.");
|
|
285
|
+
if (sort !== "source" && sort !== "relevance") throw new TypeError("sort must be source or relevance.");
|
|
286
|
+
const offset = options.offset ?? 0;
|
|
287
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new TypeError("offset must be a non-negative safe integer.");
|
|
288
|
+
const folded = mode === "terms" ? normalizedSearchText(query) : query.toLocaleLowerCase("en-US");
|
|
289
|
+
if (mode === "terms" && folded.length === 0) throw new TypeError("terms query must contain non-whitespace text.");
|
|
290
|
+
const terms = mode === "terms" ? [...new Set(folded.split(" "))] : [folded];
|
|
271
291
|
const include = normalizedInclude(options.include);
|
|
272
292
|
const limit = boundedLimit(options.limit);
|
|
273
293
|
const scan = await this.#scan(include, options.signal);
|
|
274
|
-
const
|
|
275
|
-
const matches: SessionHistorySearchMatch[] = [];
|
|
276
|
-
let matchCount = 0;
|
|
294
|
+
const ranked: Array<{ entry: SessionHistoryEntry; index: number; rank: number; span: number; sourceOrder: number }> = [];
|
|
277
295
|
for (const session of scan.sessions) {
|
|
278
296
|
for (const entry of session.entries) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
timestamp: entry.timestamp,
|
|
290
|
-
snippet: boundedSnippet(entry.text, index),
|
|
297
|
+
const text = mode === "terms" ? normalizedSearchText(entry.text) : entry.text.toLocaleLowerCase("en-US");
|
|
298
|
+
const indices = terms.map((term) => text.indexOf(term));
|
|
299
|
+
if (indices.some((index) => index < 0)) continue;
|
|
300
|
+
const phraseIndex = text.indexOf(folded);
|
|
301
|
+
ranked.push({
|
|
302
|
+
entry,
|
|
303
|
+
index: phraseIndex >= 0 ? phraseIndex : Math.min(...indices),
|
|
304
|
+
rank: text === folded ? 0 : phraseIndex >= 0 ? 1 : 2,
|
|
305
|
+
span: sort === "relevance" ? tightestTermSpan(text, terms, indices) : 0,
|
|
306
|
+
sourceOrder: ranked.length,
|
|
291
307
|
});
|
|
292
308
|
}
|
|
293
309
|
}
|
|
310
|
+
if (sort === "relevance") ranked.sort((left, right) =>
|
|
311
|
+
left.rank - right.rank || left.span - right.span || left.sourceOrder - right.sourceOrder);
|
|
312
|
+
const page = ranked.slice(offset, offset + limit);
|
|
313
|
+
const matches: SessionHistorySearchMatch[] = page.map(({ entry, index }) => ({
|
|
314
|
+
sessionId: entry.sessionId,
|
|
315
|
+
entryId: entry.entryId,
|
|
316
|
+
turn: entry.turn,
|
|
317
|
+
resourceUri: entry.resourceUri,
|
|
318
|
+
kind: entry.kind,
|
|
319
|
+
timestamp: entry.timestamp,
|
|
320
|
+
snippet: boundedSnippet(entry.text, mode === "terms" && entry.text.length > MAX_SESSION_HISTORY_SNIPPET_CHARS
|
|
321
|
+
? originalSearchIndex(entry.text, index) : index),
|
|
322
|
+
}));
|
|
323
|
+
const remaining = Math.max(0, ranked.length - offset - matches.length);
|
|
294
324
|
return resultWithMetrics(scan, {
|
|
295
325
|
version: SESSION_HISTORY_VERSION,
|
|
296
326
|
...(scan.generation === undefined ? {} : { generation: scan.generation }),
|
|
297
327
|
query,
|
|
298
328
|
matches: Object.freeze(matches),
|
|
299
|
-
matchCount,
|
|
300
|
-
|
|
329
|
+
matchCount: ranked.length,
|
|
330
|
+
hasMore: remaining > 0,
|
|
331
|
+
...(remaining > 0 ? { nextOffset: offset + matches.length } : {}),
|
|
332
|
+
}, remaining);
|
|
301
333
|
}
|
|
302
334
|
|
|
303
335
|
async searchSessions(query: string, options: SessionHistorySearchOptions = {}): Promise<SessionHistorySearchResult> {
|
|
@@ -305,6 +337,7 @@ export class SessionHistoryService {
|
|
|
305
337
|
}
|
|
306
338
|
|
|
307
339
|
async compactions(options: SessionHistoryCompactionOptions = {}): Promise<SessionHistoryCompactionResult> {
|
|
340
|
+
rejectSearchOptions(options);
|
|
308
341
|
const limit = boundedLimit(options.limit);
|
|
309
342
|
const scan = await this.#scan(new Set<SessionHistoryInclude>(["compaction"]), options.signal);
|
|
310
343
|
const all = scan.sessions
|
|
@@ -331,6 +364,7 @@ export class SessionHistoryService {
|
|
|
331
364
|
options: SessionHistoryReadOptions = {},
|
|
332
365
|
): Promise<SessionHistoryReadResult> {
|
|
333
366
|
const request = typeof sessionOrRequest === "string" ? { sessionId: sessionOrRequest, ...options } : sessionOrRequest;
|
|
367
|
+
rejectSearchOptions(request);
|
|
334
368
|
validateIdentifierInput(request.sessionId, "sessionId");
|
|
335
369
|
if (request.entryId !== undefined) validateIdentifierInput(request.entryId, "entryId");
|
|
336
370
|
if (request.turn !== undefined && (!Number.isSafeInteger(request.turn) || request.turn < 0)) {
|
|
@@ -775,6 +809,50 @@ function boundedLimit(value: number | undefined): number {
|
|
|
775
809
|
}
|
|
776
810
|
return value;
|
|
777
811
|
}
|
|
812
|
+
function rejectSearchOptions(options: SessionHistoryOptions): void {
|
|
813
|
+
for (const key of ["mode", "sort", "offset"] as const) {
|
|
814
|
+
if (key in options) throw new TypeError(`${key} is available only for search.`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function normalizedSearchText(text: string): string {
|
|
818
|
+
return text.normalize("NFKC").toLocaleLowerCase("en-US").replace(/\s+/gu, " ").trim();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** Map a non-whitespace match back via whole-prefix normalization, only for
|
|
822
|
+
* returned long snippets. Per-character/grapheme normalization is insufficient:
|
|
823
|
+
* compatibility Hangul jamo can compose across original grapheme boundaries.
|
|
824
|
+
* Prefix lengths account for composition, expansion and collapsed whitespace. */
|
|
825
|
+
function originalSearchIndex(text: string, target: number): number {
|
|
826
|
+
let low = 0;
|
|
827
|
+
let high = text.length;
|
|
828
|
+
while (low < high) {
|
|
829
|
+
const middle = Math.floor((low + high) / 2);
|
|
830
|
+
if (normalizedSearchText(text.slice(0, middle)).length > target) high = middle;
|
|
831
|
+
else low = middle + 1;
|
|
832
|
+
}
|
|
833
|
+
let index = Math.max(0, low - 1);
|
|
834
|
+
// A normalized character may expand to several UTF-16 units; anchor an
|
|
835
|
+
// astral source character at its high surrogate, not in its middle.
|
|
836
|
+
if (index > 0 && /[\uDC00-\uDFFF]/.test(text[index]!)) index -= 1;
|
|
837
|
+
return index;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Smallest covering window over all unique substring terms; no tokenization
|
|
841
|
+
* beyond query whitespace and no matching across projected entries. */
|
|
842
|
+
function tightestTermSpan(text: string, terms: readonly string[], initial: readonly number[]): number {
|
|
843
|
+
if (terms.length === 1) return terms[0]!.length;
|
|
844
|
+
const indices = [...initial];
|
|
845
|
+
let best = Infinity;
|
|
846
|
+
while (true) {
|
|
847
|
+
const start = Math.min(...indices);
|
|
848
|
+
const end = Math.max(...indices.map((index, term) => index + terms[term]!.length));
|
|
849
|
+
best = Math.min(best, end - start);
|
|
850
|
+
const earliest = indices.indexOf(start);
|
|
851
|
+
const next = text.indexOf(terms[earliest]!, start + 1);
|
|
852
|
+
if (next < 0) return best;
|
|
853
|
+
indices[earliest] = next;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
778
856
|
function validateQuery(query: string): void {
|
|
779
857
|
if (typeof query !== "string" || query.length === 0 || query.length > MAX_SESSION_HISTORY_QUERY_CHARS || query.includes("\0")) {
|
|
780
858
|
throw new TypeError(`query must be a non-empty string of at most ${MAX_SESSION_HISTORY_QUERY_CHARS} characters.`);
|
|
@@ -522,7 +522,11 @@ export declare function appendBoundedTranscriptMessage(messages: Array<{
|
|
|
522
522
|
role: string;
|
|
523
523
|
content: string;
|
|
524
524
|
}): void;
|
|
525
|
-
export declare function createUtf8LineDecoder(maxBufferedBytes?: number
|
|
525
|
+
export declare function createUtf8LineDecoder(maxBufferedBytes?: number, onOverflow?: (info: {
|
|
526
|
+
receivedBytes: number;
|
|
527
|
+
maxBytes: number;
|
|
528
|
+
prefix: string;
|
|
529
|
+
}) => void, onLine?: (line: string) => void): Utf8LineDecoder;
|
|
526
530
|
export declare function appendDistinctAssistantMessage(messages: Array<{
|
|
527
531
|
role: string;
|
|
528
532
|
content: string;
|
|
@@ -59,7 +59,14 @@ export interface SessionHistoryOptions {
|
|
|
59
59
|
signal?: AbortSignal;
|
|
60
60
|
}
|
|
61
61
|
export type SessionHistoryListOptions = SessionHistoryOptions;
|
|
62
|
-
export
|
|
62
|
+
export interface SessionHistorySearchOptions extends SessionHistoryOptions {
|
|
63
|
+
/** Literal matching is the compatibility default; terms require every unique normalized term in one projected entry. */
|
|
64
|
+
mode?: "literal" | "terms";
|
|
65
|
+
/** Relevance ranks exact text, continuous phrase, then the tightest term span; ties retain source order. */
|
|
66
|
+
sort?: "source" | "relevance";
|
|
67
|
+
/** Zero-based match offset within this fresh scan (not a snapshot cursor). */
|
|
68
|
+
offset?: number;
|
|
69
|
+
}
|
|
63
70
|
export type SessionHistoryReadOptions = SessionHistoryOptions;
|
|
64
71
|
export type SessionHistoryCompactionOptions = Omit<SessionHistoryOptions, "include">;
|
|
65
72
|
export interface SessionHistorySession {
|
|
@@ -117,7 +124,10 @@ export interface SessionHistorySearchResult extends ScanMetrics {
|
|
|
117
124
|
generation?: number;
|
|
118
125
|
query: string;
|
|
119
126
|
matches: readonly SessionHistorySearchMatch[];
|
|
127
|
+
/** Total matches in this scan, independent of pagination. */
|
|
120
128
|
matchCount: number;
|
|
129
|
+
hasMore: boolean;
|
|
130
|
+
nextOffset?: number;
|
|
121
131
|
}
|
|
122
132
|
export interface SessionHistoryCompactionResult extends ScanMetrics {
|
|
123
133
|
version: typeof SESSION_HISTORY_VERSION;
|