@hyperdrive.bot/paseo-server 0.2.8 → 0.3.1
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/dist/server/server/file-explorer/service.js +9 -0
- package/dist/server/server/search/extract.d.ts +53 -0
- package/dist/server/server/search/extract.js +222 -0
- package/dist/server/server/search/index-store.d.ts +83 -0
- package/dist/server/server/search/index-store.js +241 -0
- package/dist/server/server/search/indexer.d.ts +69 -0
- package/dist/server/server/search/indexer.js +362 -0
- package/dist/server/server/search/resolve-cwd.d.ts +40 -0
- package/dist/server/server/search/resolve-cwd.js +109 -0
- package/dist/server/server/search/session-search-service.d.ts +59 -0
- package/dist/server/server/search/session-search-service.js +171 -0
- package/dist/server/server/session.js +10 -7
- package/dist/server/server/workspace/recall-search-feature.js +25 -16
- package/dist/server/server/workspace/sessions-search-route.d.ts +18 -46
- package/dist/server/server/workspace/sessions-search-route.js +27 -163
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js → desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-dd96011e23063ea32a8d93c8e75c11b7.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js → desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-1526526b7e379374c85e4d816bbb7742.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-e2ffd4d4419cad7f5153525e95f645b8.js → index-fad30dd59837afbdf33b7d9190c754e9.js} +246 -245
- package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-fad30dd59837afbdf33b7d9190c754e9.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-e2ffd4d4419cad7f5153525e95f645b8.js.map.gz → index-fad30dd59837afbdf33b7d9190c754e9.js.map.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js → indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js} +3 -3
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-d12fb34cd3595fa34df2d42d9121d0d7.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js → native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js} +5 -5
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-c04241cce5bea0c64d4e31fea6e45288.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{profiler-8ca15c1659e234673816bc7bad0f88b6.js → profiler-24af32fdd64b094ad2dc78cf1625bd07.js} +11 -11
- package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-24af32fdd64b094ad2dc78cf1625bd07.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{profiler-8ca15c1659e234673816bc7bad0f88b6.js.map.gz → profiler-24af32fdd64b094ad2dc78cf1625bd07.js.map.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-1a240d0212111d3eb294a9407e136e6a.js → startRecording-d113d3bf7d809773ff5c95198366c905.js} +60 -60
- package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-d113d3bf7d809773ff5c95198366c905.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{startRecording-1a240d0212111d3eb294a9407e136e6a.js.map.gz → startRecording-d113d3bf7d809773ff5c95198366c905.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +2 -2
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-bridge-998e1be07f321f26e398086e261fbee3.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/desktop-attachment-store-7e067a0e3c8d6639d94a1ce72c9ec0d2.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-e2ffd4d4419cad7f5153525e95f645b8.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/indexeddb-attachment-store-7fead1a370b17c2a180153dbfdd444f3.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/native-file-attachment-store-8a937b8ecc2b5546131709e2bdda97cb.js.map.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/profiler-8ca15c1659e234673816bc7bad0f88b6.js.map.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/startRecording-1a240d0212111d3eb294a9407e136e6a.js.map.br +0 -0
|
@@ -5,6 +5,15 @@ import { expandUserPath, resolvePathFromBase } from "../path-utils.js";
|
|
|
5
5
|
import { IMAGE_MIME_TYPES, isImageMimeType, resolveBinaryMimeType } from "./mime-types.js";
|
|
6
6
|
const TEXT_MIME_TYPES = {
|
|
7
7
|
".json": "application/json",
|
|
8
|
+
// HTML is textual, so it classifies as `kind: "text"` and its bytes arrive as UTF-8.
|
|
9
|
+
// Naming the mime honestly is what lets the client render it instead of showing source.
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".htm": "text/html",
|
|
12
|
+
".xhtml": "application/xhtml+xml",
|
|
13
|
+
".css": "text/css",
|
|
14
|
+
".csv": "text/csv",
|
|
15
|
+
".xml": "application/xml",
|
|
16
|
+
".md": "text/markdown",
|
|
8
17
|
};
|
|
9
18
|
const DEFAULT_TEXT_MIME_TYPE = "text/plain";
|
|
10
19
|
const FILE_TYPE_SAMPLE_BYTES = 8192;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Max characters indexed from a single tool input/result block. */
|
|
2
|
+
export declare const TOOL_BLOCK_CAP: number;
|
|
3
|
+
/** Which FTS column a block feeds. */
|
|
4
|
+
export type Lane = "text" | "tool";
|
|
5
|
+
export interface ExtractedBlock {
|
|
6
|
+
lane: Lane;
|
|
7
|
+
/** "user" | "assistant" | "system" | "tool" — informational, not indexed. */
|
|
8
|
+
role: string;
|
|
9
|
+
text: string;
|
|
10
|
+
/** True when the source payload exceeded TOOL_BLOCK_CAP and was cut. */
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ExtractedRecord {
|
|
14
|
+
blocks: ExtractedBlock[];
|
|
15
|
+
/** ISO timestamp for the record, when the provider supplies one. */
|
|
16
|
+
timestamp: string | null;
|
|
17
|
+
/** Session id carried in the record, when the provider supplies one. */
|
|
18
|
+
sid: string | null;
|
|
19
|
+
/**
|
|
20
|
+
* The TRUE working directory, read from the record itself.
|
|
21
|
+
*
|
|
22
|
+
* This must never be reverse-engineered from Claude's project-dir slug.
|
|
23
|
+
* The slug replaces every non-alphanumeric character with "-", so it is a
|
|
24
|
+
* lossy, irreversible encoding: `/home/claudiao/super-repo` and
|
|
25
|
+
* `/home/claudiao/super/repo` produce the SAME slug. Decoding it back
|
|
26
|
+
* yields `super/repo`, a directory that does not exist — and since the
|
|
27
|
+
* command palette resumes a session INTO its cwd, a wrong cwd makes the
|
|
28
|
+
* hit unresumable. That is the defect that made the old search results
|
|
29
|
+
* unusable even when they were correct.
|
|
30
|
+
*
|
|
31
|
+
* Slug-derived cwd is kept only as a last-resort display fallback for
|
|
32
|
+
* transcripts that carry no cwd at all.
|
|
33
|
+
*/
|
|
34
|
+
cwd: string | null;
|
|
35
|
+
}
|
|
36
|
+
export declare function extractClaudeRecord(record: unknown): ExtractedRecord;
|
|
37
|
+
export declare function extractCodexRecord(record: unknown): ExtractedRecord;
|
|
38
|
+
export interface PaseoAgentMeta {
|
|
39
|
+
agentId: string;
|
|
40
|
+
provider: string | null;
|
|
41
|
+
cwd: string | null;
|
|
42
|
+
title: string | null;
|
|
43
|
+
/** runtimeInfo.sessionId — the join key into the provider's transcript. */
|
|
44
|
+
linkedSessionId: string | null;
|
|
45
|
+
workspaceId: string | null;
|
|
46
|
+
labels: Record<string, string>;
|
|
47
|
+
updatedAt: string | null;
|
|
48
|
+
}
|
|
49
|
+
export declare function extractPaseoAgent(record: unknown): {
|
|
50
|
+
meta: PaseoAgentMeta | null;
|
|
51
|
+
blocks: ExtractedBlock[];
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=extract.d.ts.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// MARRA CUSTOMIZATION — universal transcript text extraction for session FTS.
|
|
2
|
+
//
|
|
3
|
+
// One job: turn a raw transcript record (whatever the provider's shape) into
|
|
4
|
+
// zero or more `ExtractedBlock`s of searchable text. Nothing here touches the
|
|
5
|
+
// filesystem or SQLite, so it is trivially unit-testable per provider.
|
|
6
|
+
//
|
|
7
|
+
// Two text lanes, deliberately kept apart:
|
|
8
|
+
// • "text" — prose a human or the assistant actually wrote.
|
|
9
|
+
// • "tool" — tool names, tool inputs, tool results.
|
|
10
|
+
// They land in separate FTS5 columns so the ranker can weight typed prose
|
|
11
|
+
// above tool spew. Searching a phrase you typed should surface the message
|
|
12
|
+
// where you typed it, not the 400 grep hits that happen to contain it.
|
|
13
|
+
//
|
|
14
|
+
// Tool payloads are capped per block (see TOOL_BLOCK_CAP). Measured on the
|
|
15
|
+
// real 4.6 GB corpus: tool traffic is 92% of all indexable text, and capping
|
|
16
|
+
// at 8 KB cuts it 64% (1.12 GB → 0.40 GB) while keeping the identifying head
|
|
17
|
+
// of every result — the command, the path, the first ~100 lines of output.
|
|
18
|
+
// Nobody finds anything by searching the 700th KB of a file dump. Blocks that
|
|
19
|
+
// were cut set `truncated`, so coverage claims stay honest.
|
|
20
|
+
/** Max characters indexed from a single tool input/result block. */
|
|
21
|
+
export const TOOL_BLOCK_CAP = 8 * 1024;
|
|
22
|
+
const EMPTY = { blocks: [], timestamp: null, sid: null, cwd: null };
|
|
23
|
+
function cap(raw) {
|
|
24
|
+
if (raw.length <= TOOL_BLOCK_CAP)
|
|
25
|
+
return { text: raw, truncated: false };
|
|
26
|
+
return { text: raw.slice(0, TOOL_BLOCK_CAP), truncated: true };
|
|
27
|
+
}
|
|
28
|
+
/** JSON.stringify that never throws (cycles, BigInt, exotic values). */
|
|
29
|
+
function safeStringify(value) {
|
|
30
|
+
if (typeof value === "string")
|
|
31
|
+
return value;
|
|
32
|
+
if (value === null || value === undefined)
|
|
33
|
+
return "";
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(value) ?? "";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function pushProse(out, role, text) {
|
|
42
|
+
if (typeof text !== "string")
|
|
43
|
+
return;
|
|
44
|
+
const trimmed = text.trim();
|
|
45
|
+
if (trimmed.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
out.push({ lane: "text", role, text: trimmed, truncated: false });
|
|
48
|
+
}
|
|
49
|
+
function pushTool(out, role, raw) {
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (trimmed.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const { text, truncated } = cap(trimmed);
|
|
54
|
+
out.push({ lane: "tool", role, text, truncated });
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Claude Code — ~/.claude/projects/<cwd-slug>/<sid>.jsonl
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
//
|
|
60
|
+
// One JSON object per line. Conversation records carry `message: { role,
|
|
61
|
+
// content }` where content is a string or an array of Anthropic content
|
|
62
|
+
// blocks. Non-conversation lines (hook attachments, queue-operation,
|
|
63
|
+
// custom-title) have no `message` and are skipped — they are 74% of the raw
|
|
64
|
+
// bytes and contain nothing a human typed.
|
|
65
|
+
export function extractClaudeRecord(record) {
|
|
66
|
+
if (typeof record !== "object" || record === null)
|
|
67
|
+
return EMPTY;
|
|
68
|
+
const rec = record;
|
|
69
|
+
const message = rec.message;
|
|
70
|
+
if (typeof message !== "object" || message === null)
|
|
71
|
+
return EMPTY;
|
|
72
|
+
const msg = message;
|
|
73
|
+
const role = typeof msg.role === "string" ? msg.role : "unknown";
|
|
74
|
+
const timestamp = typeof rec.timestamp === "string" ? rec.timestamp : null;
|
|
75
|
+
const sid = typeof rec.sessionId === "string" ? rec.sessionId : null;
|
|
76
|
+
// The authoritative cwd. Present on ~74% of records; the indexer keeps the
|
|
77
|
+
// first non-null it sees per file.
|
|
78
|
+
const cwd = typeof rec.cwd === "string" && rec.cwd.length > 0 ? rec.cwd : null;
|
|
79
|
+
const blocks = [];
|
|
80
|
+
const content = msg.content;
|
|
81
|
+
if (typeof content === "string") {
|
|
82
|
+
pushProse(blocks, role, content);
|
|
83
|
+
return { blocks, timestamp, sid, cwd };
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(content))
|
|
86
|
+
return { blocks, timestamp, sid, cwd };
|
|
87
|
+
for (const block of content) {
|
|
88
|
+
pushClaudeContentBlock(blocks, role, block);
|
|
89
|
+
}
|
|
90
|
+
return { blocks, timestamp, sid, cwd };
|
|
91
|
+
}
|
|
92
|
+
/** Route a single Anthropic content block into the right lane. */
|
|
93
|
+
function pushClaudeContentBlock(out, role, block) {
|
|
94
|
+
// Some records carry bare strings inside the content array.
|
|
95
|
+
if (typeof block === "string") {
|
|
96
|
+
pushProse(out, role, block);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof block !== "object" || block === null)
|
|
100
|
+
return;
|
|
101
|
+
const b = block;
|
|
102
|
+
switch (b.type) {
|
|
103
|
+
case "text":
|
|
104
|
+
pushProse(out, role, b.text);
|
|
105
|
+
return;
|
|
106
|
+
case "thinking":
|
|
107
|
+
pushProse(out, "thinking", b.thinking);
|
|
108
|
+
return;
|
|
109
|
+
case "tool_use": {
|
|
110
|
+
const name = typeof b.name === "string" ? b.name : "tool";
|
|
111
|
+
pushTool(out, "tool_use", `${name} ${safeStringify(b.input)}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "tool_result":
|
|
115
|
+
pushTool(out, "tool_result", safeStringify(b.content));
|
|
116
|
+
return;
|
|
117
|
+
default:
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Codex CLI — ~/.codex/sessions/**/rollout-*.jsonl
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
//
|
|
125
|
+
// UNVERIFIED AGAINST REAL DATA. `~/.codex/` on the dev box holds only
|
|
126
|
+
// `prompts/` — zero session rollouts — so this adapter is unit-tested against
|
|
127
|
+
// a fixture and MUST be re-validated the first time a real Codex session
|
|
128
|
+
// exists. It is written defensively: unknown shapes yield no blocks rather
|
|
129
|
+
// than garbage.
|
|
130
|
+
//
|
|
131
|
+
// Codex rollout lines wrap a payload: `{ type, payload: { type, role,
|
|
132
|
+
// content: [{ type: "input_text" | "output_text", text }] } }`, with function
|
|
133
|
+
// calls as `function_call` / `function_call_output`.
|
|
134
|
+
export function extractCodexRecord(record) {
|
|
135
|
+
if (typeof record !== "object" || record === null)
|
|
136
|
+
return EMPTY;
|
|
137
|
+
const rec = record;
|
|
138
|
+
const payloadRaw = typeof rec.payload === "object" && rec.payload !== null ? rec.payload : rec;
|
|
139
|
+
const p = payloadRaw;
|
|
140
|
+
const timestamp = typeof rec.timestamp === "string" ? rec.timestamp : null;
|
|
141
|
+
const sid = typeof rec.session_id === "string" ? rec.session_id : null;
|
|
142
|
+
const cwd = typeof rec.cwd === "string" && rec.cwd.length > 0 ? rec.cwd : null;
|
|
143
|
+
const role = typeof p.role === "string" ? p.role : "unknown";
|
|
144
|
+
const blocks = [];
|
|
145
|
+
pushCodexPayload(blocks, role, p);
|
|
146
|
+
return { blocks, timestamp, sid, cwd };
|
|
147
|
+
}
|
|
148
|
+
/** Route one Codex rollout payload into the right lane. */
|
|
149
|
+
function pushCodexPayload(out, role, p) {
|
|
150
|
+
switch (typeof p.type === "string" ? p.type : "") {
|
|
151
|
+
case "function_call": {
|
|
152
|
+
const name = typeof p.name === "string" ? p.name : "tool";
|
|
153
|
+
pushTool(out, "tool_use", `${name} ${safeStringify(p.arguments)}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "function_call_output":
|
|
157
|
+
pushTool(out, "tool_result", safeStringify(p.output));
|
|
158
|
+
return;
|
|
159
|
+
case "reasoning":
|
|
160
|
+
for (const s of Array.isArray(p.summary) ? p.summary : []) {
|
|
161
|
+
if (typeof s === "object" && s !== null) {
|
|
162
|
+
pushProse(out, "thinking", s.text);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
default:
|
|
167
|
+
pushCodexContent(out, role, p.content);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function pushCodexContent(out, role, content) {
|
|
171
|
+
if (typeof content === "string") {
|
|
172
|
+
pushProse(out, role, content);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(content))
|
|
176
|
+
return;
|
|
177
|
+
for (const block of content) {
|
|
178
|
+
if (typeof block === "string") {
|
|
179
|
+
pushProse(out, role, block);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (typeof block !== "object" || block === null)
|
|
183
|
+
continue;
|
|
184
|
+
// input_text / output_text / text all carry `.text`.
|
|
185
|
+
pushProse(out, role, block.text);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function extractPaseoAgent(record) {
|
|
189
|
+
if (typeof record !== "object" || record === null)
|
|
190
|
+
return { meta: null, blocks: [] };
|
|
191
|
+
const r = record;
|
|
192
|
+
const agentId = typeof r.id === "string" ? r.id : null;
|
|
193
|
+
if (!agentId)
|
|
194
|
+
return { meta: null, blocks: [] };
|
|
195
|
+
const runtime = typeof r.runtimeInfo === "object" && r.runtimeInfo !== null
|
|
196
|
+
? r.runtimeInfo
|
|
197
|
+
: {};
|
|
198
|
+
const rawLabels = typeof r.labels === "object" && r.labels !== null ? r.labels : {};
|
|
199
|
+
const labels = {};
|
|
200
|
+
for (const [k, v] of Object.entries(rawLabels)) {
|
|
201
|
+
if (typeof v === "string")
|
|
202
|
+
labels[k] = v;
|
|
203
|
+
}
|
|
204
|
+
const meta = {
|
|
205
|
+
agentId,
|
|
206
|
+
provider: typeof r.provider === "string" ? r.provider : null,
|
|
207
|
+
cwd: typeof r.cwd === "string" ? r.cwd : null,
|
|
208
|
+
title: typeof r.title === "string" ? r.title : null,
|
|
209
|
+
linkedSessionId: typeof runtime.sessionId === "string" ? runtime.sessionId : null,
|
|
210
|
+
workspaceId: typeof r.workspaceId === "string" ? r.workspaceId : null,
|
|
211
|
+
labels,
|
|
212
|
+
updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : null,
|
|
213
|
+
};
|
|
214
|
+
const blocks = [];
|
|
215
|
+
pushProse(blocks, "title", meta.title);
|
|
216
|
+
const labelText = Object.entries(labels)
|
|
217
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
218
|
+
.join(" ");
|
|
219
|
+
pushProse(blocks, "labels", labelText);
|
|
220
|
+
return { meta, blocks };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Bump when the schema or extraction changes in a way that invalidates rows. */
|
|
2
|
+
export declare const INDEX_SCHEMA_VERSION = 1;
|
|
3
|
+
export interface FileState {
|
|
4
|
+
path: string;
|
|
5
|
+
size: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
/** Bytes already consumed. Next pass reads from here. */
|
|
8
|
+
offset: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ChunkInput {
|
|
11
|
+
path: string;
|
|
12
|
+
sid: string;
|
|
13
|
+
source: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
timestamp: string | null;
|
|
16
|
+
/** Typed prose. Empty string when this chunk is tool-only. */
|
|
17
|
+
text: string;
|
|
18
|
+
/** Tool names, inputs, results. Empty string when prose-only. */
|
|
19
|
+
tool: string;
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface SearchHit {
|
|
23
|
+
sid: string;
|
|
24
|
+
source: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
snippet: string;
|
|
28
|
+
path: string;
|
|
29
|
+
/** True when the winning snippet came from tool traffic, not typed prose. */
|
|
30
|
+
fromTool: boolean;
|
|
31
|
+
score: number;
|
|
32
|
+
}
|
|
33
|
+
export interface IndexStats {
|
|
34
|
+
files: number;
|
|
35
|
+
chunks: number;
|
|
36
|
+
truncatedChunks: number;
|
|
37
|
+
bytesConsumed: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class SessionIndexStore {
|
|
40
|
+
private readonly db;
|
|
41
|
+
private insertChunkStmt;
|
|
42
|
+
private upsertFileStmt;
|
|
43
|
+
constructor(dbPath: string, options?: {
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
});
|
|
46
|
+
private migrate;
|
|
47
|
+
/** Byte offsets already consumed, keyed by path. */
|
|
48
|
+
getFileStates(): Map<string, FileState>;
|
|
49
|
+
/**
|
|
50
|
+
* Drop every chunk for a file. Used when a transcript is rewritten or
|
|
51
|
+
* truncated (size shrank), where the append-only assumption no longer holds
|
|
52
|
+
* and resuming from the old offset would index garbage.
|
|
53
|
+
*/
|
|
54
|
+
forgetFile(path: string): void;
|
|
55
|
+
/** Drop files that no longer exist on disk, so search never offers a dead session. */
|
|
56
|
+
pruneMissing(existingPaths: Set<string>): number;
|
|
57
|
+
/** Append chunks and record the new offset for a file, atomically. */
|
|
58
|
+
commitFile(state: FileState, source: string, sid: string, cwd: string, chunks: ChunkInput[]): void;
|
|
59
|
+
stats(): IndexStats;
|
|
60
|
+
/**
|
|
61
|
+
* FTS5 MATCH over both lanes, ranked with typed prose weighted above tool
|
|
62
|
+
* traffic. Returns at most one hit per session (the best-scoring chunk), so
|
|
63
|
+
* the palette shows 20 distinct sessions rather than 20 hits in one file.
|
|
64
|
+
*/
|
|
65
|
+
search(params: {
|
|
66
|
+
query: string;
|
|
67
|
+
limit?: number;
|
|
68
|
+
sources?: string[];
|
|
69
|
+
}): SearchHit[];
|
|
70
|
+
close(): void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Turn user input into a safe FTS5 MATCH expression.
|
|
74
|
+
*
|
|
75
|
+
* Users type prose, not query DSL. Raw input reaches FTS5's parser and a bare
|
|
76
|
+
* `-` or an unbalanced `"` throws mid-keystroke, which reads as "search is
|
|
77
|
+
* broken". Every token is quoted (making it a literal) and multi-token input
|
|
78
|
+
* becomes an implicit AND, which is what people expect from a search box.
|
|
79
|
+
* A trailing `*` on the last token gives prefix matching so results appear
|
|
80
|
+
* while still typing.
|
|
81
|
+
*/
|
|
82
|
+
export declare function toMatchExpression(raw: string): string | null;
|
|
83
|
+
//# sourceMappingURL=index-store.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// MARRA CUSTOMIZATION — SQLite FTS5 store for universal session search.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the `recall` CLI, which failed structurally rather than by bad
|
|
4
|
+
// tuning: it spawned a subprocess per keystroke, re-indexed its whole Tantivy
|
|
5
|
+
// store on every spawn, and took a global writer lock while doing it. On a
|
|
6
|
+
// loaded host (39-agent fleet, load ~100) every cmd+K keystroke timed out, so
|
|
7
|
+
// the feature was disabled by default in 73ae98f3.
|
|
8
|
+
//
|
|
9
|
+
// All three of those go away here:
|
|
10
|
+
// • no subprocess — node:sqlite is in-process (verified FTS5 on v24)
|
|
11
|
+
// • no reindex-per-query — writes happen only in the background indexer
|
|
12
|
+
// • no global lock — WAL, and the query path is read-only
|
|
13
|
+
//
|
|
14
|
+
// Schema notes:
|
|
15
|
+
// `files` — one row per transcript file, carrying the byte offset we have
|
|
16
|
+
// consumed. Transcripts are append-only, so a pass reads only
|
|
17
|
+
// [offset, EOF). The 214 MB monster is read once, ever.
|
|
18
|
+
// `chunks` — FTS5, two indexed columns so typed prose can outrank tool spew.
|
|
19
|
+
// `unicode61 remove_diacritics 2` is required, not cosmetic:
|
|
20
|
+
// without it "periodo" does not match "Período" and PT-BR search
|
|
21
|
+
// silently half-works.
|
|
22
|
+
import { DatabaseSync } from "node:sqlite";
|
|
23
|
+
/** Bump when the schema or extraction changes in a way that invalidates rows. */
|
|
24
|
+
export const INDEX_SCHEMA_VERSION = 1;
|
|
25
|
+
/** bm25 weights: typed prose counts 10x a tool payload hit. */
|
|
26
|
+
const WEIGHT_TEXT = 10.0;
|
|
27
|
+
const WEIGHT_TOOL = 1.0;
|
|
28
|
+
const MIN_QUERY_LEN = 2;
|
|
29
|
+
const DEFAULT_LIMIT = 20;
|
|
30
|
+
const MAX_LIMIT = 100;
|
|
31
|
+
export class SessionIndexStore {
|
|
32
|
+
constructor(dbPath, options = {}) {
|
|
33
|
+
this.insertChunkStmt = null;
|
|
34
|
+
this.upsertFileStmt = null;
|
|
35
|
+
this.db = new DatabaseSync(dbPath, options.readOnly ? { readOnly: true } : {});
|
|
36
|
+
if (!options.readOnly) {
|
|
37
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
38
|
+
// Durability is not worth the fsync cost here: the index is a derived
|
|
39
|
+
// cache. A torn write costs a re-index of that file, nothing more.
|
|
40
|
+
this.db.exec("PRAGMA synchronous = NORMAL");
|
|
41
|
+
this.migrate();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
migrate() {
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
47
|
+
key TEXT PRIMARY KEY,
|
|
48
|
+
value TEXT NOT NULL
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
51
|
+
path TEXT PRIMARY KEY,
|
|
52
|
+
source TEXT NOT NULL,
|
|
53
|
+
sid TEXT NOT NULL,
|
|
54
|
+
cwd TEXT NOT NULL,
|
|
55
|
+
size INTEGER NOT NULL,
|
|
56
|
+
mtime INTEGER NOT NULL,
|
|
57
|
+
offset INTEGER NOT NULL
|
|
58
|
+
);
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS chunks USING fts5(
|
|
60
|
+
text,
|
|
61
|
+
tool,
|
|
62
|
+
path UNINDEXED,
|
|
63
|
+
sid UNINDEXED,
|
|
64
|
+
source UNINDEXED,
|
|
65
|
+
cwd UNINDEXED,
|
|
66
|
+
ts UNINDEXED,
|
|
67
|
+
truncated UNINDEXED,
|
|
68
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
69
|
+
);
|
|
70
|
+
`);
|
|
71
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
72
|
+
const current = row ? Number.parseInt(row.value, 10) : 0;
|
|
73
|
+
if (current !== INDEX_SCHEMA_VERSION) {
|
|
74
|
+
// Derived data — a schema change just rebuilds rather than migrating.
|
|
75
|
+
this.db.exec("DELETE FROM chunks; DELETE FROM files;");
|
|
76
|
+
this.db
|
|
77
|
+
.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)")
|
|
78
|
+
.run(String(INDEX_SCHEMA_VERSION));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Byte offsets already consumed, keyed by path. */
|
|
82
|
+
getFileStates() {
|
|
83
|
+
const rows = this.db.prepare("SELECT path, size, mtime, offset FROM files").all();
|
|
84
|
+
const map = new Map();
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
map.set(r.path, { path: r.path, size: r.size, mtimeMs: r.mtime, offset: r.offset });
|
|
87
|
+
}
|
|
88
|
+
return map;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop every chunk for a file. Used when a transcript is rewritten or
|
|
92
|
+
* truncated (size shrank), where the append-only assumption no longer holds
|
|
93
|
+
* and resuming from the old offset would index garbage.
|
|
94
|
+
*/
|
|
95
|
+
forgetFile(path) {
|
|
96
|
+
this.db.prepare("DELETE FROM chunks WHERE path = ?").run(path);
|
|
97
|
+
this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
|
|
98
|
+
}
|
|
99
|
+
/** Drop files that no longer exist on disk, so search never offers a dead session. */
|
|
100
|
+
pruneMissing(existingPaths) {
|
|
101
|
+
const rows = this.db.prepare("SELECT path FROM files").all();
|
|
102
|
+
let dropped = 0;
|
|
103
|
+
for (const r of rows) {
|
|
104
|
+
if (!existingPaths.has(r.path)) {
|
|
105
|
+
this.forgetFile(r.path);
|
|
106
|
+
dropped += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return dropped;
|
|
110
|
+
}
|
|
111
|
+
/** Append chunks and record the new offset for a file, atomically. */
|
|
112
|
+
commitFile(state, source, sid, cwd, chunks) {
|
|
113
|
+
this.insertChunkStmt ?? (this.insertChunkStmt = this.db.prepare(`INSERT INTO chunks (text, tool, path, sid, source, cwd, ts, truncated)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`));
|
|
115
|
+
this.upsertFileStmt ?? (this.upsertFileStmt = this.db.prepare(`INSERT INTO files (path, source, sid, cwd, size, mtime, offset)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
117
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
118
|
+
size = excluded.size, mtime = excluded.mtime, offset = excluded.offset`));
|
|
119
|
+
this.db.exec("BEGIN");
|
|
120
|
+
try {
|
|
121
|
+
for (const c of chunks) {
|
|
122
|
+
this.insertChunkStmt.run(c.text, c.tool, c.path, c.sid, c.source, c.cwd, c.timestamp ?? "", c.truncated ? 1 : 0);
|
|
123
|
+
}
|
|
124
|
+
this.upsertFileStmt.run(state.path, source, sid, cwd, state.size, Math.floor(state.mtimeMs), state.offset);
|
|
125
|
+
this.db.exec("COMMIT");
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
this.db.exec("ROLLBACK");
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
stats() {
|
|
133
|
+
const f = this.db.prepare("SELECT COUNT(*) c, COALESCE(SUM(offset),0) b FROM files").get();
|
|
134
|
+
const ch = this.db.prepare("SELECT COUNT(*) c FROM chunks").get();
|
|
135
|
+
// FTS5 stores every column value as text, so the integer 1 written by
|
|
136
|
+
// commitFile comes back as '1' or 1 depending on the binding path. CAST
|
|
137
|
+
// makes the comparison independent of that — without it this counter
|
|
138
|
+
// silently reads 0 and we would under-report truncation.
|
|
139
|
+
const tr = this.db
|
|
140
|
+
.prepare("SELECT COUNT(*) c FROM chunks WHERE CAST(truncated AS INTEGER) = 1")
|
|
141
|
+
.get();
|
|
142
|
+
return { files: f.c, chunks: ch.c, truncatedChunks: tr.c, bytesConsumed: f.b };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* FTS5 MATCH over both lanes, ranked with typed prose weighted above tool
|
|
146
|
+
* traffic. Returns at most one hit per session (the best-scoring chunk), so
|
|
147
|
+
* the palette shows 20 distinct sessions rather than 20 hits in one file.
|
|
148
|
+
*/
|
|
149
|
+
search(params) {
|
|
150
|
+
const query = params.query.trim();
|
|
151
|
+
if (query.length < MIN_QUERY_LEN)
|
|
152
|
+
return [];
|
|
153
|
+
const limit = Math.min(Math.max(params.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT);
|
|
154
|
+
const matchExpr = toMatchExpression(query);
|
|
155
|
+
if (!matchExpr)
|
|
156
|
+
return [];
|
|
157
|
+
const sourceFilter = params.sources && params.sources.length > 0
|
|
158
|
+
? ` AND source IN (${params.sources.map(() => "?").join(",")})`
|
|
159
|
+
: "";
|
|
160
|
+
// Rank first, then pick the best chunk per session. Over-fetch so that a
|
|
161
|
+
// single chatty session cannot crowd out the rest of the result set.
|
|
162
|
+
const sql = `
|
|
163
|
+
SELECT sid, source, cwd, ts,
|
|
164
|
+
snippet(chunks, 0, '‹', '›', '…', 14) AS snip_text,
|
|
165
|
+
snippet(chunks, 1, '‹', '›', '…', 14) AS snip_tool,
|
|
166
|
+
path,
|
|
167
|
+
bm25(chunks, ${WEIGHT_TEXT}, ${WEIGHT_TOOL}) AS score
|
|
168
|
+
FROM chunks
|
|
169
|
+
WHERE chunks MATCH ?${sourceFilter}
|
|
170
|
+
ORDER BY score
|
|
171
|
+
LIMIT ?
|
|
172
|
+
`;
|
|
173
|
+
const args = [matchExpr];
|
|
174
|
+
if (params.sources && params.sources.length > 0)
|
|
175
|
+
args.push(...params.sources);
|
|
176
|
+
args.push(limit * 20);
|
|
177
|
+
let rows;
|
|
178
|
+
try {
|
|
179
|
+
rows = this.db.prepare(sql).all(...args);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Malformed MATCH expression (unbalanced quote mid-typing, etc.) — an
|
|
183
|
+
// empty result is the honest answer, not a 500 in the palette.
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const bySession = new Map();
|
|
187
|
+
for (const r of rows) {
|
|
188
|
+
const sid = String(r.sid ?? "");
|
|
189
|
+
if (!sid || bySession.has(sid))
|
|
190
|
+
continue;
|
|
191
|
+
bySession.set(sid, toSearchHit(sid, r));
|
|
192
|
+
if (bySession.size >= limit)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
return [...bySession.values()];
|
|
196
|
+
}
|
|
197
|
+
close() {
|
|
198
|
+
this.db.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Map one ranked FTS5 row to a hit. The prose snippet wins when the term
|
|
203
|
+
* appeared in typed text; the tool snippet is the fallback, flagged so the UI
|
|
204
|
+
* can say where the match came from.
|
|
205
|
+
*/
|
|
206
|
+
function toSearchHit(sid, r) {
|
|
207
|
+
const snipText = String(r.snip_text ?? "").trim();
|
|
208
|
+
const snipTool = String(r.snip_tool ?? "").trim();
|
|
209
|
+
const fromTool = snipText.length === 0;
|
|
210
|
+
return {
|
|
211
|
+
sid,
|
|
212
|
+
source: String(r.source ?? ""),
|
|
213
|
+
cwd: String(r.cwd ?? ""),
|
|
214
|
+
timestamp: String(r.ts ?? ""),
|
|
215
|
+
snippet: (fromTool ? snipTool : snipText).replace(/\s+/g, " ").slice(0, 300),
|
|
216
|
+
path: String(r.path ?? ""),
|
|
217
|
+
fromTool,
|
|
218
|
+
score: Number(r.score ?? 0),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Turn user input into a safe FTS5 MATCH expression.
|
|
223
|
+
*
|
|
224
|
+
* Users type prose, not query DSL. Raw input reaches FTS5's parser and a bare
|
|
225
|
+
* `-` or an unbalanced `"` throws mid-keystroke, which reads as "search is
|
|
226
|
+
* broken". Every token is quoted (making it a literal) and multi-token input
|
|
227
|
+
* becomes an implicit AND, which is what people expect from a search box.
|
|
228
|
+
* A trailing `*` on the last token gives prefix matching so results appear
|
|
229
|
+
* while still typing.
|
|
230
|
+
*/
|
|
231
|
+
export function toMatchExpression(raw) {
|
|
232
|
+
const tokens = raw
|
|
233
|
+
.trim()
|
|
234
|
+
.split(/\s+/)
|
|
235
|
+
.map((t) => t.replace(/"/g, ""))
|
|
236
|
+
.filter((t) => t.length > 0);
|
|
237
|
+
if (tokens.length === 0)
|
|
238
|
+
return null;
|
|
239
|
+
return tokens.map((t, i) => (i === tokens.length - 1 ? `"${t}"*` : `"${t}"`)).join(" AND ");
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=index-store.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type pino from "pino";
|
|
2
|
+
import { SessionIndexStore } from "./index-store.js";
|
|
3
|
+
import { type ExtractedRecord } from "./extract.js";
|
|
4
|
+
export interface DiscoveredFile {
|
|
5
|
+
path: string;
|
|
6
|
+
source: string;
|
|
7
|
+
sid: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* How a provider lays out a file on disk.
|
|
12
|
+
*
|
|
13
|
+
* "jsonl" — append-only, one JSON record per line. Incremental byte offsets
|
|
14
|
+
* apply; this is the case that makes a 214 MB transcript cheap.
|
|
15
|
+
* "json" — a single pretty-printed JSON document per file, REWRITTEN in
|
|
16
|
+
* place on every update (paseo agent records are 46 lines each).
|
|
17
|
+
* Byte offsets are meaningless here: a partial read is invalid JSON,
|
|
18
|
+
* so these files are always parsed whole and replaced wholesale.
|
|
19
|
+
*/
|
|
20
|
+
export type SourceFormat = "jsonl" | "json";
|
|
21
|
+
export interface SourceAdapter {
|
|
22
|
+
name: string;
|
|
23
|
+
/** On-disk layout. Defaults to "jsonl". */
|
|
24
|
+
format?: SourceFormat;
|
|
25
|
+
/** Enumerate transcript files for this provider. Missing root → []. */
|
|
26
|
+
discover(): Promise<DiscoveredFile[]>;
|
|
27
|
+
/** Parse one raw record into extractable blocks. */
|
|
28
|
+
extract(record: unknown): ExtractedRecord;
|
|
29
|
+
}
|
|
30
|
+
export declare function createClaudeAdapter(root?: string): SourceAdapter;
|
|
31
|
+
export declare function createCodexAdapter(root?: string): SourceAdapter;
|
|
32
|
+
export declare function createPaseoAdapter(root?: string): SourceAdapter;
|
|
33
|
+
export declare function defaultAdapters(): SourceAdapter[];
|
|
34
|
+
export interface IndexPassResult {
|
|
35
|
+
/** Files left mid-read because they hit maxBytesPerFile this pass. */
|
|
36
|
+
filesPartial: number;
|
|
37
|
+
filesScanned: number;
|
|
38
|
+
filesUpdated: number;
|
|
39
|
+
filesReset: number;
|
|
40
|
+
filesPruned: number;
|
|
41
|
+
bytesRead: number;
|
|
42
|
+
chunksAdded: number;
|
|
43
|
+
errors: number;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
}
|
|
46
|
+
export interface IndexPassOptions {
|
|
47
|
+
adapters?: SourceAdapter[];
|
|
48
|
+
logger?: pino.Logger;
|
|
49
|
+
/** Abort cooperatively between files (daemon shutdown). */
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
/** Bytes per file per pass. Keeps the first pass over a 214 MB file bounded. */
|
|
52
|
+
maxBytesPerFile?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One incremental pass over every discovered transcript. Safe to call
|
|
56
|
+
* repeatedly; work is proportional to bytes appended since the last call, not
|
|
57
|
+
* to corpus size.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runIndexPass(store: SessionIndexStore, options?: IndexPassOptions): Promise<IndexPassResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Default on-disk location for the index (a derived cache, safe to delete).
|
|
62
|
+
*
|
|
63
|
+
* MUST resolve through `resolvePaseoHome()` and not `os.homedir()`: a daemon
|
|
64
|
+
* started with PASEO_HOME pointing elsewhere (tests, a second instance, an
|
|
65
|
+
* isolated E2E run) would otherwise write its index into the real ~/.paseo and
|
|
66
|
+
* silently share state with the user's live daemon.
|
|
67
|
+
*/
|
|
68
|
+
export declare function defaultIndexPath(): string;
|
|
69
|
+
//# sourceMappingURL=indexer.d.ts.map
|