pi-sessions 0.3.2 → 0.4.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/extensions/session-search/extract.ts +218 -50
- package/extensions/session-search/hooks.ts +253 -103
- package/extensions/session-search/reindex.ts +48 -55
- package/extensions/shared/session-index/common.ts +4 -1
- package/extensions/shared/session-index/lineage.ts +70 -2
- package/extensions/shared/session-index/schema.ts +24 -11
- package/extensions/shared/session-index/search.ts +2 -2
- package/extensions/shared/session-index/sqlite.ts +45 -12
- package/extensions/shared/session-index/store.ts +125 -33
- package/package.json +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
2
|
import { parseTypeBoxValue } from "../typebox.js";
|
|
4
3
|
import {
|
|
5
4
|
INDEX_SCHEMA_VERSION,
|
|
@@ -17,13 +16,6 @@ export function ensureIndexDir(dir: string): string {
|
|
|
17
16
|
return dir;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function createTempIndexPath(finalPath: string): string {
|
|
21
|
-
const dir = path.dirname(finalPath);
|
|
22
|
-
const baseName = path.basename(finalPath, path.extname(finalPath));
|
|
23
|
-
ensureIndexDir(dir);
|
|
24
|
-
return path.join(dir, `${baseName}.tmp-${process.pid}-${Date.now()}.sqlite`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
export function openIndexDatabase(
|
|
28
20
|
dbPath: string,
|
|
29
21
|
options?: { create?: boolean; timeoutMs?: number },
|
|
@@ -55,6 +47,9 @@ export function initializeSchema(db: SessionIndexDatabase): void {
|
|
|
55
47
|
session_origin TEXT,
|
|
56
48
|
handoff_goal TEXT,
|
|
57
49
|
handoff_next_task TEXT,
|
|
50
|
+
indexed_file_size INTEGER,
|
|
51
|
+
indexed_file_mtime_ms INTEGER,
|
|
52
|
+
indexed_file_anchor TEXT,
|
|
58
53
|
index_version INTEGER NOT NULL,
|
|
59
54
|
indexed_at_ts TEXT NOT NULL,
|
|
60
55
|
index_source TEXT NOT NULL
|
|
@@ -97,11 +92,29 @@ export function initializeSchema(db: SessionIndexDatabase): void {
|
|
|
97
92
|
CREATE INDEX IF NOT EXISTS session_text_chunks_ts_idx ON session_text_chunks(ts DESC);
|
|
98
93
|
|
|
99
94
|
CREATE VIRTUAL TABLE IF NOT EXISTS session_text_chunks_fts USING fts5(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
text,
|
|
96
|
+
content='session_text_chunks',
|
|
97
|
+
content_rowid='id'
|
|
103
98
|
);
|
|
104
99
|
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_ai
|
|
101
|
+
AFTER INSERT ON session_text_chunks BEGIN
|
|
102
|
+
INSERT INTO session_text_chunks_fts(rowid, text) VALUES (new.id, new.text);
|
|
103
|
+
END;
|
|
104
|
+
|
|
105
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_ad
|
|
106
|
+
AFTER DELETE ON session_text_chunks BEGIN
|
|
107
|
+
INSERT INTO session_text_chunks_fts(session_text_chunks_fts, rowid, text)
|
|
108
|
+
VALUES ('delete', old.id, old.text);
|
|
109
|
+
END;
|
|
110
|
+
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS session_text_chunks_fts_au
|
|
112
|
+
AFTER UPDATE ON session_text_chunks BEGIN
|
|
113
|
+
INSERT INTO session_text_chunks_fts(session_text_chunks_fts, rowid, text)
|
|
114
|
+
VALUES ('delete', old.id, old.text);
|
|
115
|
+
INSERT INTO session_text_chunks_fts(rowid, text) VALUES (new.id, new.text);
|
|
116
|
+
END;
|
|
117
|
+
|
|
105
118
|
CREATE TABLE IF NOT EXISTS session_file_touches (
|
|
106
119
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
107
120
|
session_id TEXT NOT NULL,
|
|
@@ -325,12 +325,12 @@ function getTextMatchRows(db: SessionIndexDatabase, filters: SearchFilters): Sea
|
|
|
325
325
|
s.session_origin as sessionOrigin,
|
|
326
326
|
s.handoff_goal as handoffGoal,
|
|
327
327
|
s.handoff_next_task as handoffNextTask,
|
|
328
|
-
snippet(session_text_chunks_fts,
|
|
328
|
+
snippet(session_text_chunks_fts, 0, ?, ?, ?, 12) as snippet,
|
|
329
329
|
bm25(session_text_chunks_fts) as rank,
|
|
330
330
|
c.entry_id as entryId,
|
|
331
331
|
c.source_kind as sourceKind
|
|
332
332
|
FROM session_text_chunks_fts
|
|
333
|
-
JOIN session_text_chunks c ON c.id =
|
|
333
|
+
JOIN session_text_chunks c ON c.id = session_text_chunks_fts.rowid
|
|
334
334
|
JOIN sessions s ON s.session_id = c.session_id
|
|
335
335
|
WHERE session_text_chunks_fts MATCH ?
|
|
336
336
|
AND (? IS NULL OR s.modified_ts >= ?)
|
|
@@ -3,6 +3,8 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
const requireModule = createRequire(import.meta.url);
|
|
4
4
|
const isBun = Boolean(process.versions.bun);
|
|
5
5
|
|
|
6
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
7
|
+
|
|
6
8
|
export type SqliteBindValue = string | number | bigint | null;
|
|
7
9
|
|
|
8
10
|
export interface SqliteRunResult {
|
|
@@ -16,13 +18,17 @@ export interface SqliteStatement {
|
|
|
16
18
|
run(...params: SqliteBindValue[]): SqliteRunResult;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
export interface SqliteTransaction<Args extends unknown[], Result> {
|
|
22
|
+
(...args: Args): Result;
|
|
23
|
+
immediate(...args: Args): Result;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export interface SqliteDatabase {
|
|
20
|
-
readonly inTransaction: boolean;
|
|
21
27
|
prepare(sql: string): SqliteStatement;
|
|
22
28
|
exec(sql: string): void;
|
|
23
29
|
transaction<Args extends unknown[], Result>(
|
|
24
30
|
fn: (...args: Args) => Result,
|
|
25
|
-
):
|
|
31
|
+
): SqliteTransaction<Args, Result>;
|
|
26
32
|
close(): void;
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -47,19 +53,46 @@ export function openSqlite(
|
|
|
47
53
|
const Database = loadDatabaseConstructor();
|
|
48
54
|
const db = isBun
|
|
49
55
|
? new Database(dbPath, { create: options.create, readwrite: true })
|
|
50
|
-
: new Database(
|
|
51
|
-
dbPath,
|
|
52
|
-
options.timeoutMs === undefined
|
|
53
|
-
? { fileMustExist: !options.create }
|
|
54
|
-
: { fileMustExist: !options.create, timeout: options.timeoutMs },
|
|
55
|
-
);
|
|
56
|
+
: new Database(dbPath, { fileMustExist: !options.create });
|
|
56
57
|
|
|
58
|
+
// busy_timeout must be set before any other statement: the journal_mode
|
|
59
|
+
// pragma below is the connection's first lock acquisition (and may run WAL
|
|
60
|
+
// recovery), and immediate transactions rely on this timeout to queue behind
|
|
61
|
+
// concurrent writers instead of failing.
|
|
62
|
+
db.exec(`PRAGMA busy_timeout = ${options.timeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
57
63
|
db.exec("PRAGMA journal_mode = WAL");
|
|
58
64
|
db.exec("PRAGMA synchronous = NORMAL");
|
|
59
65
|
db.exec("PRAGMA foreign_keys = ON");
|
|
60
|
-
if (isBun && options.timeoutMs !== undefined) {
|
|
61
|
-
db.exec(`PRAGMA busy_timeout = ${options.timeoutMs}`);
|
|
62
|
-
}
|
|
63
66
|
|
|
64
|
-
return db;
|
|
67
|
+
return withStatementCache(db);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Neither driver memoizes prepare(), and the write paths prepare the same
|
|
71
|
+
// statements once per row. SQLite re-prepares cached statements transparently
|
|
72
|
+
// when the schema changes, so reusing them across DDL is safe.
|
|
73
|
+
function withStatementCache(db: SqliteDatabase): SqliteDatabase {
|
|
74
|
+
const statements = new Map<string, SqliteStatement>();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
prepare(sql) {
|
|
78
|
+
const cached = statements.get(sql);
|
|
79
|
+
if (cached) {
|
|
80
|
+
return cached;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const statement = db.prepare(sql);
|
|
84
|
+
statements.set(sql, statement);
|
|
85
|
+
return statement;
|
|
86
|
+
},
|
|
87
|
+
exec(sql) {
|
|
88
|
+
db.exec(sql);
|
|
89
|
+
},
|
|
90
|
+
transaction(fn) {
|
|
91
|
+
return db.transaction(fn);
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
statements.clear();
|
|
95
|
+
db.close();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
65
98
|
}
|
|
@@ -1,12 +1,38 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
import { parseTypeBoxValue } from "../typebox.js";
|
|
1
3
|
import {
|
|
2
4
|
compactSessionId,
|
|
3
5
|
INDEX_SCHEMA_VERSION,
|
|
6
|
+
NULLABLE_STRING_SCHEMA,
|
|
7
|
+
parseRepoRoots,
|
|
8
|
+
SESSION_ORIGIN_SCHEMA,
|
|
4
9
|
type SessionFileTouchRow,
|
|
5
10
|
type SessionIndexDatabase,
|
|
6
11
|
type SessionRow,
|
|
7
12
|
type SessionTextChunkRow,
|
|
8
13
|
} from "./common.js";
|
|
9
14
|
|
|
15
|
+
const SESSION_ROW_QUERY_SCHEMA = Type.Object({
|
|
16
|
+
sessionId: Type.String(),
|
|
17
|
+
sessionPath: Type.String(),
|
|
18
|
+
sessionName: Type.String(),
|
|
19
|
+
firstUserPrompt: NULLABLE_STRING_SCHEMA,
|
|
20
|
+
cwd: Type.String(),
|
|
21
|
+
repoRootsJson: Type.String(),
|
|
22
|
+
startedAt: Type.String(),
|
|
23
|
+
modifiedAt: Type.String(),
|
|
24
|
+
messageCount: Type.Number(),
|
|
25
|
+
entryCount: Type.Number(),
|
|
26
|
+
parentSessionPath: NULLABLE_STRING_SCHEMA,
|
|
27
|
+
parentSessionId: NULLABLE_STRING_SCHEMA,
|
|
28
|
+
sessionOrigin: Type.Union([SESSION_ORIGIN_SCHEMA, Type.Null()]),
|
|
29
|
+
handoffGoal: NULLABLE_STRING_SCHEMA,
|
|
30
|
+
handoffNextTask: NULLABLE_STRING_SCHEMA,
|
|
31
|
+
indexedFileSize: Type.Union([Type.Number(), Type.Null()]),
|
|
32
|
+
indexedFileMtimeMs: Type.Union([Type.Number(), Type.Null()]),
|
|
33
|
+
indexedFileAnchor: NULLABLE_STRING_SCHEMA,
|
|
34
|
+
});
|
|
35
|
+
|
|
10
36
|
function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
11
37
|
return [
|
|
12
38
|
row.sessionId,
|
|
@@ -24,6 +50,9 @@ function sessionRowBindings(row: SessionRow, indexSource: string) {
|
|
|
24
50
|
row.sessionOrigin ?? null,
|
|
25
51
|
row.handoffGoal ?? null,
|
|
26
52
|
row.handoffNextTask ?? null,
|
|
53
|
+
row.indexedFileSize ?? null,
|
|
54
|
+
row.indexedFileMtimeMs ?? null,
|
|
55
|
+
row.indexedFileAnchor ?? null,
|
|
27
56
|
INDEX_SCHEMA_VERSION,
|
|
28
57
|
new Date().toISOString(),
|
|
29
58
|
indexSource,
|
|
@@ -42,8 +71,9 @@ export function insertSession(
|
|
|
42
71
|
created_ts, modified_ts, message_count, entry_count,
|
|
43
72
|
parent_session_path, parent_session_id, session_origin,
|
|
44
73
|
handoff_goal, handoff_next_task,
|
|
74
|
+
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
45
75
|
index_version, indexed_at_ts, index_source
|
|
46
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
77
|
`,
|
|
48
78
|
).run(...sessionRowBindings(row, indexSource));
|
|
49
79
|
insertSessionIdChunk(db, row);
|
|
@@ -61,8 +91,9 @@ export function upsertSession(
|
|
|
61
91
|
created_ts, modified_ts, message_count, entry_count,
|
|
62
92
|
parent_session_path, parent_session_id, session_origin,
|
|
63
93
|
handoff_goal, handoff_next_task,
|
|
94
|
+
indexed_file_size, indexed_file_mtime_ms, indexed_file_anchor,
|
|
64
95
|
index_version, indexed_at_ts, index_source
|
|
65
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
66
97
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
67
98
|
session_path = excluded.session_path,
|
|
68
99
|
session_name = excluded.session_name,
|
|
@@ -78,6 +109,9 @@ export function upsertSession(
|
|
|
78
109
|
session_origin = excluded.session_origin,
|
|
79
110
|
handoff_goal = excluded.handoff_goal,
|
|
80
111
|
handoff_next_task = excluded.handoff_next_task,
|
|
112
|
+
indexed_file_size = excluded.indexed_file_size,
|
|
113
|
+
indexed_file_mtime_ms = excluded.indexed_file_mtime_ms,
|
|
114
|
+
indexed_file_anchor = excluded.indexed_file_anchor,
|
|
81
115
|
index_version = excluded.index_version,
|
|
82
116
|
indexed_at_ts = excluded.indexed_at_ts,
|
|
83
117
|
index_source = excluded.index_source
|
|
@@ -86,6 +120,72 @@ export function upsertSession(
|
|
|
86
120
|
syncSessionIdChunk(db, row);
|
|
87
121
|
}
|
|
88
122
|
|
|
123
|
+
export function getSessionRowByPath(
|
|
124
|
+
db: SessionIndexDatabase,
|
|
125
|
+
sessionPath: string,
|
|
126
|
+
): SessionRow | undefined {
|
|
127
|
+
const row = db
|
|
128
|
+
.prepare(
|
|
129
|
+
`
|
|
130
|
+
SELECT
|
|
131
|
+
session_id as sessionId,
|
|
132
|
+
session_path as sessionPath,
|
|
133
|
+
session_name as sessionName,
|
|
134
|
+
first_user_prompt as firstUserPrompt,
|
|
135
|
+
cwd,
|
|
136
|
+
repo_roots_json as repoRootsJson,
|
|
137
|
+
created_ts as startedAt,
|
|
138
|
+
modified_ts as modifiedAt,
|
|
139
|
+
message_count as messageCount,
|
|
140
|
+
entry_count as entryCount,
|
|
141
|
+
parent_session_path as parentSessionPath,
|
|
142
|
+
parent_session_id as parentSessionId,
|
|
143
|
+
session_origin as sessionOrigin,
|
|
144
|
+
handoff_goal as handoffGoal,
|
|
145
|
+
handoff_next_task as handoffNextTask,
|
|
146
|
+
indexed_file_size as indexedFileSize,
|
|
147
|
+
indexed_file_mtime_ms as indexedFileMtimeMs,
|
|
148
|
+
indexed_file_anchor as indexedFileAnchor
|
|
149
|
+
FROM sessions
|
|
150
|
+
WHERE session_path = ?
|
|
151
|
+
ORDER BY indexed_at_ts DESC
|
|
152
|
+
LIMIT 1
|
|
153
|
+
`,
|
|
154
|
+
)
|
|
155
|
+
.get(sessionPath);
|
|
156
|
+
|
|
157
|
+
if (row === undefined || row === null) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return buildSessionRow(
|
|
162
|
+
parseTypeBoxValue(SESSION_ROW_QUERY_SCHEMA, row, `Invalid session row for path ${sessionPath}`),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildSessionRow(row: Static<typeof SESSION_ROW_QUERY_SCHEMA>): SessionRow {
|
|
167
|
+
return {
|
|
168
|
+
sessionId: row.sessionId,
|
|
169
|
+
sessionPath: row.sessionPath,
|
|
170
|
+
sessionName: row.sessionName,
|
|
171
|
+
firstUserPrompt: row.firstUserPrompt ?? undefined,
|
|
172
|
+
cwd: row.cwd,
|
|
173
|
+
repoRoots: parseRepoRoots(row.repoRootsJson),
|
|
174
|
+
startedAt: row.startedAt,
|
|
175
|
+
modifiedAt: row.modifiedAt,
|
|
176
|
+
messageCount: row.messageCount,
|
|
177
|
+
entryCount: row.entryCount,
|
|
178
|
+
parentSessionPath: row.parentSessionPath ?? undefined,
|
|
179
|
+
parentSessionId: row.parentSessionId ?? undefined,
|
|
180
|
+
sessionOrigin: row.sessionOrigin ?? undefined,
|
|
181
|
+
handoffGoal: row.handoffGoal ?? undefined,
|
|
182
|
+
handoffNextTask: row.handoffNextTask ?? undefined,
|
|
183
|
+
indexedFileSize: row.indexedFileSize ?? undefined,
|
|
184
|
+
indexedFileMtimeMs: row.indexedFileMtimeMs ?? undefined,
|
|
185
|
+
indexedFileAnchor: row.indexedFileAnchor ?? undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
89
189
|
function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
90
190
|
insertTextChunk(db, {
|
|
91
191
|
sessionId: row.sessionId,
|
|
@@ -97,18 +197,7 @@ function insertSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
|
97
197
|
}
|
|
98
198
|
|
|
99
199
|
function syncSessionIdChunk(db: SessionIndexDatabase, row: SessionRow): void {
|
|
100
|
-
db.
|
|
101
|
-
`DELETE FROM session_text_chunks_fts
|
|
102
|
-
WHERE chunk_id IN (
|
|
103
|
-
SELECT CAST(id AS TEXT) FROM session_text_chunks
|
|
104
|
-
WHERE session_id = ? AND source_kind = 'session_id'
|
|
105
|
-
)`,
|
|
106
|
-
).run(row.sessionId);
|
|
107
|
-
|
|
108
|
-
db.prepare(
|
|
109
|
-
`DELETE FROM session_text_chunks WHERE session_id = ? AND source_kind = 'session_id'`,
|
|
110
|
-
).run(row.sessionId);
|
|
111
|
-
|
|
200
|
+
clearSessionChunksBySourceKind(db, row.sessionId, "session_id");
|
|
112
201
|
insertSessionIdChunk(db, row);
|
|
113
202
|
}
|
|
114
203
|
|
|
@@ -117,35 +206,38 @@ function buildSessionIdSearchText(sessionId: string): string {
|
|
|
117
206
|
return compact === sessionId ? sessionId : `${sessionId} ${compact}`;
|
|
118
207
|
}
|
|
119
208
|
|
|
209
|
+
export function clearSessionChunksBySourceKind(
|
|
210
|
+
db: SessionIndexDatabase,
|
|
211
|
+
sessionId: string,
|
|
212
|
+
sourceKind: string,
|
|
213
|
+
): void {
|
|
214
|
+
db.prepare(`DELETE FROM session_text_chunks WHERE session_id = ? AND source_kind = ?`).run(
|
|
215
|
+
sessionId,
|
|
216
|
+
sourceKind,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
120
220
|
export function clearSessionIndexedData(db: SessionIndexDatabase, sessionId: string): void {
|
|
121
|
-
db.prepare(`DELETE FROM session_text_chunks_fts WHERE session_id = ?`).run(sessionId);
|
|
122
221
|
db.prepare(`DELETE FROM session_text_chunks WHERE session_id = ?`).run(sessionId);
|
|
123
222
|
db.prepare(`DELETE FROM session_file_touches WHERE session_id = ?`).run(sessionId);
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
export function insertTextChunk(db: SessionIndexDatabase, row: SessionTextChunkRow): void {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`
|
|
226
|
+
db.prepare(
|
|
227
|
+
`
|
|
130
228
|
INSERT INTO session_text_chunks(
|
|
131
229
|
session_id, entry_id, entry_type, role, ts, source_kind, text
|
|
132
230
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
133
231
|
`,
|
|
134
|
-
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const chunkId = Number(result.lastInsertRowid);
|
|
146
|
-
db.prepare(
|
|
147
|
-
`INSERT INTO session_text_chunks_fts(chunk_id, session_id, text) VALUES (?, ?, ?)`,
|
|
148
|
-
).run(chunkId, row.sessionId, row.text);
|
|
232
|
+
).run(
|
|
233
|
+
row.sessionId,
|
|
234
|
+
row.entryId ?? null,
|
|
235
|
+
row.entryType,
|
|
236
|
+
row.role ?? null,
|
|
237
|
+
row.ts,
|
|
238
|
+
row.sourceKind,
|
|
239
|
+
row.text,
|
|
240
|
+
);
|
|
149
241
|
}
|
|
150
242
|
|
|
151
243
|
export function insertSessionFileTouch(db: SessionIndexDatabase, row: SessionFileTouchRow): void {
|