@timqi/pier 0.0.29 → 0.1.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/README.md +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/agent/listing.js
CHANGED
|
@@ -1,46 +1,40 @@
|
|
|
1
|
-
// Which sessions exist on disk
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// bytes ever written rather than with the number of sessions. Every surface
|
|
7
|
-
// asks (the rail, Activity, a task lookup, every reconnect), so the scan sits
|
|
8
|
-
// on the interactive path and gets slower for the life of the instance. But a
|
|
9
|
-
// transcript is append-only and the facts a listing needs are all derived from
|
|
10
|
-
// bytes already read, so each byte is read once: rows live in pier.db, keyed by
|
|
11
|
-
// path and validated by (size, mtime), and a file that grew is picked up at the
|
|
12
|
-
// byte the last scan stopped on.
|
|
13
|
-
//
|
|
14
|
-
// Reading Pi's on-disk entries is the cost of that; agent/ is where the
|
|
15
|
-
// knowledge of Pi's formats already lives, and Pi's own reader is not
|
|
16
|
-
// incremental.
|
|
1
|
+
// Which sessions exist on disk and what to call them, reading each transcript
|
|
2
|
+
// byte once: Pi's own listing parses every file whole (~250ms for 30MB) and
|
|
3
|
+
// every surface asks. Rows live in pier.db keyed by path and validated by
|
|
4
|
+
// (size, mtime); a file that grew resumes where the last scan stopped. The same
|
|
5
|
+
// pass indexes what was said into `session_fts` for the palette.
|
|
17
6
|
import { createReadStream, promises as fs } from "node:fs";
|
|
18
7
|
import { join } from "node:path";
|
|
19
8
|
import { pierDb, statements, transact } from "../db.js";
|
|
20
|
-
import { SESSION_TITLE_MAX } from "../
|
|
9
|
+
import { SESSION_TITLE_MAX } from "../core/types.js";
|
|
21
10
|
import { logger } from "../log.js";
|
|
22
11
|
import { defaultAgentDir } from "./config.js";
|
|
23
|
-
import { textOf } from "./events.js";
|
|
12
|
+
import { hasToolCalls, textOf } from "./events.js";
|
|
24
13
|
const log = logger("agent");
|
|
25
|
-
/**
|
|
26
|
-
* change surfaces within a boot or two, few enough to cost nothing. */
|
|
14
|
+
/** Enough that a format change surfaces within a boot or two. */
|
|
27
15
|
const AUDIT_SAMPLE = 5;
|
|
28
|
-
/** The name a session was given, else its first message. Two arguments rather
|
|
29
|
-
* than a row, because the stored row and the parsed one spell the second one
|
|
30
|
-
* differently and an object would silently accept either. */
|
|
31
16
|
const titleOf = (name, first) => name || first || undefined;
|
|
32
17
|
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
18
|
+
/** A reply that calls a tool is a step, not a reply (events.ts), and steps are
|
|
19
|
+
* not indexed. `clean` takes off the speaker header (core/identity.ts), which
|
|
20
|
+
* as text would make "operator" hit every session. */
|
|
21
|
+
function saidIn(message, entryAt, clean) {
|
|
22
|
+
if (message?.role !== "user" && message?.role !== "assistant")
|
|
23
|
+
return undefined;
|
|
24
|
+
if (message.role === "assistant" && hasToolCalls(message))
|
|
25
|
+
return undefined;
|
|
26
|
+
const text = clean(textOf(message.content)).trim();
|
|
27
|
+
if (!text)
|
|
28
|
+
return undefined;
|
|
29
|
+
const at = typeof message.timestamp === "number" ? message.timestamp : Date.parse(entryAt ?? "");
|
|
30
|
+
return { role: message.role, at: Number.isFinite(at) ? at : 0, text };
|
|
31
|
+
}
|
|
32
|
+
/** `undefined` is "no header yet" (leading junk is skipped, as Pi's reader
|
|
33
|
+
* does), `null` is "not a session file". */
|
|
34
|
+
function fold(acc, line, clean) {
|
|
35
|
+
// The bulk of a transcript is tool results; two substring tests cost a
|
|
36
|
+
// fraction of parsing one.
|
|
37
|
+
if (acc && !line.includes('"message"') && !line.includes('"session_info"'))
|
|
44
38
|
return acc;
|
|
45
39
|
let entry;
|
|
46
40
|
try {
|
|
@@ -49,6 +43,7 @@ function fold(acc, line) {
|
|
|
49
43
|
entry = value;
|
|
50
44
|
}
|
|
51
45
|
catch {
|
|
46
|
+
// The tail of a transcript Pi is still appending to; the next scan reads it whole.
|
|
52
47
|
return acc ?? undefined;
|
|
53
48
|
}
|
|
54
49
|
if (!acc) {
|
|
@@ -58,35 +53,57 @@ function fold(acc, line) {
|
|
|
58
53
|
const timestamp = str(entry.timestamp);
|
|
59
54
|
if (entry.type !== "session" || !id || !timestamp)
|
|
60
55
|
return null;
|
|
61
|
-
return { id, cwd: str(entry.cwd) ?? "", created: Date.parse(timestamp), at: 0 };
|
|
56
|
+
return { id, cwd: str(entry.cwd) ?? "", created: Date.parse(timestamp), said: [], at: 0 };
|
|
62
57
|
}
|
|
63
58
|
if (entry?.type === "session_info")
|
|
64
59
|
return { ...acc, name: str(entry.name)?.trim() || undefined };
|
|
65
|
-
if (entry?.type === "message"
|
|
60
|
+
if (entry?.type === "message") {
|
|
66
61
|
const message = entry.message;
|
|
67
|
-
|
|
62
|
+
const said = saidIn(message, str(entry.timestamp), clean);
|
|
63
|
+
if (!said)
|
|
64
|
+
return acc;
|
|
65
|
+
acc.said.push(said);
|
|
66
|
+
// The title keeps the header the index drops; surfaces read it back
|
|
67
|
+
// through core/identity.ts.
|
|
68
|
+
if (acc.first || said.role !== "user")
|
|
68
69
|
return acc;
|
|
69
|
-
|
|
70
|
-
return text ? { ...acc, first: text.slice(0, SESSION_TITLE_MAX) } : acc;
|
|
70
|
+
return { ...acc, first: textOf(message?.content).trim().slice(0, SESSION_TITLE_MAX) };
|
|
71
71
|
}
|
|
72
72
|
return acc;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/** A picker, not a results page. */
|
|
75
|
+
const SEARCH_LIMIT = 8;
|
|
76
|
+
/** A trigram token is one character, so `snippet()` is asked for its ceiling
|
|
77
|
+
* of 64 tokens; the default 12 fits one word. */
|
|
78
|
+
const SNIPPET_CHARS = 64;
|
|
79
|
+
/** Delimited like `snippet()` (\u0001 … \u0002, `…` for a cut), so a surface
|
|
80
|
+
* draws one shape for both paths. ASCII case folding: LIKE found the row by
|
|
81
|
+
* the same rule. */
|
|
82
|
+
function around(text, query) {
|
|
83
|
+
const hit = text.toLowerCase().indexOf(query.toLowerCase());
|
|
84
|
+
if (hit < 0)
|
|
85
|
+
return text.slice(0, SNIPPET_CHARS);
|
|
86
|
+
const start = Math.max(0, hit - SNIPPET_CHARS / 2);
|
|
87
|
+
const end = Math.min(text.length, hit + query.length + SNIPPET_CHARS / 2);
|
|
88
|
+
return `${start ? "…" : ""}${text.slice(start, hit)}\u0001${text.slice(hit, hit + query.length)}\u0002${text.slice(hit + query.length, end)}${end < text.length ? "…" : ""}`;
|
|
89
|
+
}
|
|
75
90
|
export class IndexedListing {
|
|
76
91
|
dir;
|
|
92
|
+
clean;
|
|
77
93
|
#db;
|
|
78
94
|
#statements;
|
|
79
|
-
constructor(dir = join(defaultAgentDir(), "sessions"), db
|
|
95
|
+
constructor(dir = join(defaultAgentDir(), "sessions"), db,
|
|
96
|
+
/** Handed in rather than imported: the header rule is core's, and agent/
|
|
97
|
+
* does not import core at runtime. */
|
|
98
|
+
clean = (text) => text) {
|
|
80
99
|
this.dir = dir;
|
|
100
|
+
this.clean = clean;
|
|
81
101
|
this.#db = db;
|
|
82
102
|
}
|
|
83
|
-
/** Opened on the first scan
|
|
84
|
-
* agent factory must not open the instance database — tests build bare ones
|
|
85
|
-
* and never list. */
|
|
103
|
+
/** Opened on the first scan: building a factory must not open the database. */
|
|
86
104
|
#store() {
|
|
87
105
|
return (this.#db ??= pierDb());
|
|
88
106
|
}
|
|
89
|
-
/** The scan's three statements, compiled on the first scan and not again. */
|
|
90
107
|
#sql() {
|
|
91
108
|
return (this.#statements ??= statements(this.#store()));
|
|
92
109
|
}
|
|
@@ -109,18 +126,17 @@ export class IndexedListing {
|
|
|
109
126
|
});
|
|
110
127
|
continue;
|
|
111
128
|
}
|
|
112
|
-
// Only a file that
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// every later scan and is never corrected.
|
|
117
|
-
const parsed = await this.#read(file.path, row && file.size > row.size
|
|
129
|
+
// Only a file that grew can be resumed. Same length with a new mtime is
|
|
130
|
+
// rewritten, and resuming would stamp old data with the new mtime forever.
|
|
131
|
+
const resumed = row !== undefined && file.size > row.size;
|
|
132
|
+
const parsed = await this.#read(file.path, resumed
|
|
118
133
|
? {
|
|
119
134
|
id: row.id,
|
|
120
135
|
cwd: row.cwd,
|
|
121
136
|
created: row.created_at,
|
|
122
137
|
...(row.name === null ? {} : { name: row.name }),
|
|
123
138
|
...(row.first_message === null ? {} : { first: row.first_message }),
|
|
139
|
+
said: [],
|
|
124
140
|
at: row.parsed_bytes,
|
|
125
141
|
}
|
|
126
142
|
: undefined);
|
|
@@ -136,6 +152,8 @@ export class IndexedListing {
|
|
|
136
152
|
size: file.size,
|
|
137
153
|
mtime: file.modified,
|
|
138
154
|
parsed_bytes: parsed.at,
|
|
155
|
+
said: parsed.said,
|
|
156
|
+
whole: !resumed,
|
|
139
157
|
});
|
|
140
158
|
const title = titleOf(parsed.name, parsed.first);
|
|
141
159
|
records.push({
|
|
@@ -150,14 +168,9 @@ export class IndexedListing {
|
|
|
150
168
|
this.#save(write, [...known.keys()]);
|
|
151
169
|
return records.sort((a, b) => b.modified - a.modified);
|
|
152
170
|
}
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* moves. Reads the newest few both ways; a disagreement is logged with both
|
|
157
|
-
* answers and the index row is dropped, so the next scan reads that file
|
|
158
|
-
* whole rather than trusting what this one derived. Returns how many
|
|
159
|
-
* disagreed — off the interactive path, never blocking a boot.
|
|
160
|
-
*/
|
|
171
|
+
/** This file parses Pi's transcripts itself, and only a comparison keeps it
|
|
172
|
+
* honest when the format moves. A disagreement drops the index row, so the
|
|
173
|
+
* next scan reads that file whole. */
|
|
161
174
|
async audit(native) {
|
|
162
175
|
const sample = (await this.scan()).slice(0, AUDIT_SAMPLE);
|
|
163
176
|
if (!sample.length)
|
|
@@ -181,8 +194,33 @@ export class IndexedListing {
|
|
|
181
194
|
this.#save([], stale);
|
|
182
195
|
return stale.length;
|
|
183
196
|
}
|
|
184
|
-
/**
|
|
185
|
-
*
|
|
197
|
+
/** Under three code points the trigram tokenizer has nothing to match, and a
|
|
198
|
+
* two-character query is what a CJK word often is: substring scan instead. */
|
|
199
|
+
search(query, limit = SEARCH_LIMIT) {
|
|
200
|
+
const sql = this.#sql();
|
|
201
|
+
const rows = [...query].length >= 3
|
|
202
|
+
// Quoted: the query is a string to find, never FTS syntax.
|
|
203
|
+
? sql(`SELECT session_id, role, at, snippet(session_fts, 0, char(1), char(2), '…', ${SNIPPET_CHARS}) AS snippet
|
|
204
|
+
FROM session_fts WHERE text MATCH ? ORDER BY bm25(session_fts), at DESC`).iterate(`"${query.replaceAll('"', '""')}"`)
|
|
205
|
+
: sql(`SELECT session_id, role, at, text FROM session_fts WHERE text LIKE ? ESCAPE '\\' ORDER BY at DESC`).iterate(`%${query.replaceAll(/[\\%_]/g, "\\$&")}%`);
|
|
206
|
+
const hits = [];
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
// Walked, not fetched: a chatty session has hundreds of rows for one hit.
|
|
209
|
+
for (const row of rows) {
|
|
210
|
+
if (seen.has(row.session_id))
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(row.session_id);
|
|
213
|
+
hits.push({
|
|
214
|
+
sessionId: row.session_id,
|
|
215
|
+
role: row.role,
|
|
216
|
+
at: row.at,
|
|
217
|
+
snippet: row.snippet ?? around(row.text ?? "", query),
|
|
218
|
+
});
|
|
219
|
+
if (hits.length >= limit)
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
return hits;
|
|
223
|
+
}
|
|
186
224
|
async #files() {
|
|
187
225
|
const entries = await fs.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
188
226
|
const dirs = entries.filter((e) => e.isDirectory() || e.isSymbolicLink());
|
|
@@ -199,7 +237,6 @@ export class IndexedListing {
|
|
|
199
237
|
}));
|
|
200
238
|
return found.flat().filter((f) => !!f);
|
|
201
239
|
}
|
|
202
|
-
/** From `from.at`, or from the start when there is nothing to resume. */
|
|
203
240
|
async #read(path, from) {
|
|
204
241
|
let acc = from;
|
|
205
242
|
let at = from?.at ?? 0;
|
|
@@ -211,7 +248,7 @@ export class IndexedListing {
|
|
|
211
248
|
const line = buffer.slice(0, nl);
|
|
212
249
|
buffer = buffer.slice(nl + 1);
|
|
213
250
|
at += Buffer.byteLength(line) + 1;
|
|
214
|
-
const next = fold(acc, line);
|
|
251
|
+
const next = fold(acc, line, this.clean);
|
|
215
252
|
if (next === null)
|
|
216
253
|
return null;
|
|
217
254
|
acc = next;
|
|
@@ -219,20 +256,22 @@ export class IndexedListing {
|
|
|
219
256
|
}
|
|
220
257
|
}
|
|
221
258
|
catch (err) {
|
|
222
|
-
//
|
|
223
|
-
// index, so the next scan tries it again rather than remembering a gap.
|
|
259
|
+
// Left out of the index too, so the next scan tries it again.
|
|
224
260
|
log.warn(`session file ${path} could not be read`, err);
|
|
225
261
|
return null;
|
|
226
262
|
}
|
|
227
263
|
return acc ? { ...acc, at } : null;
|
|
228
264
|
}
|
|
229
|
-
/** One transaction
|
|
230
|
-
* next scan a size it never parsed to.
|
|
265
|
+
/** One transaction after all reading: a half-written index would hand the
|
|
266
|
+
* next scan a size it never parsed to. A whole read replaces what the file
|
|
267
|
+
* had said; a resumed read only adds. */
|
|
231
268
|
#save(rows, gone) {
|
|
232
269
|
if (!rows.length && !gone.length)
|
|
233
270
|
return;
|
|
234
271
|
const db = this.#store();
|
|
235
272
|
const sql = this.#sql();
|
|
273
|
+
const forget = sql("DELETE FROM session_fts WHERE path = ?");
|
|
274
|
+
const say = sql("INSERT INTO session_fts(text, session_id, path, role, at) VALUES (?, ?, ?, ?, ?)");
|
|
236
275
|
const upsert = sql(`INSERT INTO session_index(path, id, cwd, created_at, name, first_message, size, mtime, parsed_bytes)
|
|
237
276
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
238
277
|
ON CONFLICT(path) DO UPDATE SET
|
|
@@ -243,10 +282,16 @@ export class IndexedListing {
|
|
|
243
282
|
const drop = sql("DELETE FROM session_index WHERE path = ?");
|
|
244
283
|
transact(db, () => {
|
|
245
284
|
for (const r of rows) {
|
|
285
|
+
if (r.whole)
|
|
286
|
+
forget.run(r.path);
|
|
287
|
+
for (const s of r.said)
|
|
288
|
+
say.run(s.text, r.id, r.path, s.role, s.at);
|
|
246
289
|
upsert.run(r.path, r.id, r.cwd, r.created_at, r.name, r.first_message, r.size, r.mtime, r.parsed_bytes);
|
|
247
290
|
}
|
|
248
|
-
for (const path of gone)
|
|
291
|
+
for (const path of gone) {
|
|
249
292
|
drop.run(path);
|
|
293
|
+
forget.run(path);
|
|
294
|
+
}
|
|
250
295
|
});
|
|
251
296
|
}
|
|
252
297
|
}
|