@timqi/pier 0.1.0 → 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 +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- 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 +7 -23
- 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 +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- 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 +4 -12
- 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 +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- 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 +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
- 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-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
- 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-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
- 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-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
- 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-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
- 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 +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.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 +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -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 +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/agent/listing.js
CHANGED
|
@@ -1,46 +1,23 @@
|
|
|
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
|
-
// The same pass indexes what was said. Every user message and every reply
|
|
15
|
-
// goes into `session_fts` beside the index row, in the same transaction, so
|
|
16
|
-
// the palette can search messages without a second reader of the same bytes.
|
|
17
|
-
//
|
|
18
|
-
// Reading Pi's on-disk entries is the cost of that; agent/ is where the
|
|
19
|
-
// knowledge of Pi's formats already lives, and Pi's own reader is not
|
|
20
|
-
// 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.
|
|
21
6
|
import { createReadStream, promises as fs } from "node:fs";
|
|
22
7
|
import { join } from "node:path";
|
|
23
8
|
import { pierDb, statements, transact } from "../db.js";
|
|
24
|
-
import { SESSION_TITLE_MAX } from "../
|
|
9
|
+
import { SESSION_TITLE_MAX } from "../core/types.js";
|
|
25
10
|
import { logger } from "../log.js";
|
|
26
11
|
import { defaultAgentDir } from "./config.js";
|
|
27
12
|
import { hasToolCalls, textOf } from "./events.js";
|
|
28
13
|
const log = logger("agent");
|
|
29
|
-
/**
|
|
30
|
-
* change surfaces within a boot or two, few enough to cost nothing. */
|
|
14
|
+
/** Enough that a format change surfaces within a boot or two. */
|
|
31
15
|
const AUDIT_SAMPLE = 5;
|
|
32
|
-
/** The name a session was given, else its first message. Two arguments rather
|
|
33
|
-
* than a row, because the stored row and the parsed one spell the second one
|
|
34
|
-
* differently and an object would silently accept either. */
|
|
35
16
|
const titleOf = (name, first) => name || first || undefined;
|
|
36
17
|
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* thinking, a tool result, or a system input. `clean` takes off what was
|
|
41
|
-
* written for the model rather than said — the speaker header a channel
|
|
42
|
-
* prefixes (core/identity.ts), which as text would make "operator" hit every
|
|
43
|
-
* session the workbench ever opened. */
|
|
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. */
|
|
44
21
|
function saidIn(message, entryAt, clean) {
|
|
45
22
|
if (message?.role !== "user" && message?.role !== "assistant")
|
|
46
23
|
return undefined;
|
|
@@ -52,16 +29,11 @@ function saidIn(message, entryAt, clean) {
|
|
|
52
29
|
const at = typeof message.timestamp === "number" ? message.timestamp : Date.parse(entryAt ?? "");
|
|
53
30
|
return { role: message.role, at: Number.isFinite(at) ? at : 0, text };
|
|
54
31
|
}
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
* `undefined` is "no header yet" (blank and unparseable lines before it are
|
|
58
|
-
* skipped, as Pi's own reader does), `null` is "not a session file" — a first
|
|
59
|
-
* entry that is not a session header — and anything else is the state so far.
|
|
60
|
-
*/
|
|
32
|
+
/** `undefined` is "no header yet" (leading junk is skipped, as Pi's reader
|
|
33
|
+
* does), `null` is "not a session file". */
|
|
61
34
|
function fold(acc, line, clean) {
|
|
62
|
-
// The bulk of a transcript is
|
|
63
|
-
//
|
|
64
|
-
// say whether a line could matter, at a fraction of parsing one that does not.
|
|
35
|
+
// The bulk of a transcript is tool results; two substring tests cost a
|
|
36
|
+
// fraction of parsing one.
|
|
65
37
|
if (acc && !line.includes('"message"') && !line.includes('"session_info"'))
|
|
66
38
|
return acc;
|
|
67
39
|
let entry;
|
|
@@ -71,6 +43,7 @@ function fold(acc, line, clean) {
|
|
|
71
43
|
entry = value;
|
|
72
44
|
}
|
|
73
45
|
catch {
|
|
46
|
+
// The tail of a transcript Pi is still appending to; the next scan reads it whole.
|
|
74
47
|
return acc ?? undefined;
|
|
75
48
|
}
|
|
76
49
|
if (!acc) {
|
|
@@ -90,23 +63,22 @@ function fold(acc, line, clean) {
|
|
|
90
63
|
if (!said)
|
|
91
64
|
return acc;
|
|
92
65
|
acc.said.push(said);
|
|
93
|
-
// The title keeps the header the index drops
|
|
94
|
-
// through core/identity.ts
|
|
66
|
+
// The title keeps the header the index drops; surfaces read it back
|
|
67
|
+
// through core/identity.ts.
|
|
95
68
|
if (acc.first || said.role !== "user")
|
|
96
69
|
return acc;
|
|
97
70
|
return { ...acc, first: textOf(message?.content).trim().slice(0, SESSION_TITLE_MAX) };
|
|
98
71
|
}
|
|
99
72
|
return acc;
|
|
100
73
|
}
|
|
101
|
-
/**
|
|
74
|
+
/** A picker, not a results page. */
|
|
102
75
|
const SEARCH_LIMIT = 8;
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
* word and its neighbours — and the LIKE path centres the same span itself. */
|
|
76
|
+
/** A trigram token is one character, so `snippet()` is asked for its ceiling
|
|
77
|
+
* of 64 tokens; the default 12 fits one word. */
|
|
106
78
|
const SNIPPET_CHARS = 64;
|
|
107
|
-
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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. */
|
|
110
82
|
function around(text, query) {
|
|
111
83
|
const hit = text.toLowerCase().indexOf(query.toLowerCase());
|
|
112
84
|
if (hit < 0)
|
|
@@ -115,29 +87,23 @@ function around(text, query) {
|
|
|
115
87
|
const end = Math.min(text.length, hit + query.length + SNIPPET_CHARS / 2);
|
|
116
88
|
return `${start ? "…" : ""}${text.slice(start, hit)}\u0001${text.slice(hit, hit + query.length)}\u0002${text.slice(hit + query.length, end)}${end < text.length ? "…" : ""}`;
|
|
117
89
|
}
|
|
118
|
-
/** The listing Pier runs on: Pi's session directory, remembered in pier.db. */
|
|
119
90
|
export class IndexedListing {
|
|
120
91
|
dir;
|
|
121
92
|
clean;
|
|
122
93
|
#db;
|
|
123
94
|
#statements;
|
|
124
95
|
constructor(dir = join(defaultAgentDir(), "sessions"), db,
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
* agent/ does not import core at runtime — main.ts, which imports both,
|
|
128
|
-
* joins them. */
|
|
96
|
+
/** Handed in rather than imported: the header rule is core's, and agent/
|
|
97
|
+
* does not import core at runtime. */
|
|
129
98
|
clean = (text) => text) {
|
|
130
99
|
this.dir = dir;
|
|
131
100
|
this.clean = clean;
|
|
132
101
|
this.#db = db;
|
|
133
102
|
}
|
|
134
|
-
/** Opened on the first scan
|
|
135
|
-
* agent factory must not open the instance database — tests build bare ones
|
|
136
|
-
* and never list. */
|
|
103
|
+
/** Opened on the first scan: building a factory must not open the database. */
|
|
137
104
|
#store() {
|
|
138
105
|
return (this.#db ??= pierDb());
|
|
139
106
|
}
|
|
140
|
-
/** The scan's three statements, compiled on the first scan and not again. */
|
|
141
107
|
#sql() {
|
|
142
108
|
return (this.#statements ??= statements(this.#store()));
|
|
143
109
|
}
|
|
@@ -160,11 +126,8 @@ export class IndexedListing {
|
|
|
160
126
|
});
|
|
161
127
|
continue;
|
|
162
128
|
}
|
|
163
|
-
// Only a file that
|
|
164
|
-
//
|
|
165
|
-
// length with a different mtime, which resuming would answer with the
|
|
166
|
-
// old derived data stamped with the new mtime: a row that then matches on
|
|
167
|
-
// every later scan and is never corrected.
|
|
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.
|
|
168
131
|
const resumed = row !== undefined && file.size > row.size;
|
|
169
132
|
const parsed = await this.#read(file.path, resumed
|
|
170
133
|
? {
|
|
@@ -205,14 +168,9 @@ export class IndexedListing {
|
|
|
205
168
|
this.#save(write, [...known.keys()]);
|
|
206
169
|
return records.sort((a, b) => b.modified - a.modified);
|
|
207
170
|
}
|
|
208
|
-
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* moves. Reads the newest few both ways; a disagreement is logged with both
|
|
212
|
-
* answers and the index row is dropped, so the next scan reads that file
|
|
213
|
-
* whole rather than trusting what this one derived. Returns how many
|
|
214
|
-
* disagreed — off the interactive path, never blocking a boot.
|
|
215
|
-
*/
|
|
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. */
|
|
216
174
|
async audit(native) {
|
|
217
175
|
const sample = (await this.scan()).slice(0, AUDIT_SAMPLE);
|
|
218
176
|
if (!sample.length)
|
|
@@ -236,24 +194,18 @@ export class IndexedListing {
|
|
|
236
194
|
this.#save([], stale);
|
|
237
195
|
return stale.length;
|
|
238
196
|
}
|
|
239
|
-
/**
|
|
240
|
-
*
|
|
241
|
-
* Three characters or more is a trigram phrase, ranked by FTS; shorter is a
|
|
242
|
-
* substring scan, newest first — the tokenizer has nothing to match under
|
|
243
|
-
* three, and a two-character query is what a CJK word often is. Counted in
|
|
244
|
-
* code points: the tokenizer counts characters, not UTF-16 units.
|
|
245
|
-
*/
|
|
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. */
|
|
246
199
|
search(query, limit = SEARCH_LIMIT) {
|
|
247
200
|
const sql = this.#sql();
|
|
248
201
|
const rows = [...query].length >= 3
|
|
249
|
-
//
|
|
202
|
+
// Quoted: the query is a string to find, never FTS syntax.
|
|
250
203
|
? sql(`SELECT session_id, role, at, snippet(session_fts, 0, char(1), char(2), '…', ${SNIPPET_CHARS}) AS snippet
|
|
251
204
|
FROM session_fts WHERE text MATCH ? ORDER BY bm25(session_fts), at DESC`).iterate(`"${query.replaceAll('"', '""')}"`)
|
|
252
205
|
: sql(`SELECT session_id, role, at, text FROM session_fts WHERE text LIKE ? ESCAPE '\\' ORDER BY at DESC`).iterate(`%${query.replaceAll(/[\\%_]/g, "\\$&")}%`);
|
|
253
206
|
const hits = [];
|
|
254
207
|
const seen = new Set();
|
|
255
|
-
// Walked, not fetched: a chatty session has hundreds of rows for one hit
|
|
256
|
-
// and the walk stops at the first row of the `limit`th session.
|
|
208
|
+
// Walked, not fetched: a chatty session has hundreds of rows for one hit.
|
|
257
209
|
for (const row of rows) {
|
|
258
210
|
if (seen.has(row.session_id))
|
|
259
211
|
continue;
|
|
@@ -269,8 +221,6 @@ export class IndexedListing {
|
|
|
269
221
|
}
|
|
270
222
|
return hits;
|
|
271
223
|
}
|
|
272
|
-
/** Every session file with its size and mtime — the only syscalls a scan
|
|
273
|
-
* where nothing changed makes. */
|
|
274
224
|
async #files() {
|
|
275
225
|
const entries = await fs.readdir(this.dir, { withFileTypes: true }).catch(() => []);
|
|
276
226
|
const dirs = entries.filter((e) => e.isDirectory() || e.isSymbolicLink());
|
|
@@ -287,7 +237,6 @@ export class IndexedListing {
|
|
|
287
237
|
}));
|
|
288
238
|
return found.flat().filter((f) => !!f);
|
|
289
239
|
}
|
|
290
|
-
/** From `from.at`, or from the start when there is nothing to resume. */
|
|
291
240
|
async #read(path, from) {
|
|
292
241
|
let acc = from;
|
|
293
242
|
let at = from?.at ?? 0;
|
|
@@ -307,17 +256,15 @@ export class IndexedListing {
|
|
|
307
256
|
}
|
|
308
257
|
}
|
|
309
258
|
catch (err) {
|
|
310
|
-
//
|
|
311
|
-
// 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.
|
|
312
260
|
log.warn(`session file ${path} could not be read`, err);
|
|
313
261
|
return null;
|
|
314
262
|
}
|
|
315
263
|
return acc ? { ...acc, at } : null;
|
|
316
264
|
}
|
|
317
|
-
/** One transaction
|
|
318
|
-
* next scan a size it never parsed to
|
|
319
|
-
*
|
|
320
|
-
* its bytes are all being said again; a resumed read only adds. */
|
|
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. */
|
|
321
268
|
#save(rows, gone) {
|
|
322
269
|
if (!rows.length && !gone.length)
|
|
323
270
|
return;
|