@gaunt-sloth/core 2.0.0-beta.4 → 2.0.0-beta.6
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/config/schema.d.ts +4 -0
- package/dist/config/schema.js +64 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tokenBudget.d.ts +88 -0
- package/dist/config/tokenBudget.js +155 -0
- package/dist/config/tokenBudget.js.map +1 -0
- package/dist/config/types.d.ts +33 -4
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +24 -0
- package/dist/core/GthAbstractAgent.js +37 -1
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +216 -1
- package/dist/core/GthAgentRunner.js +424 -3
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +196 -0
- package/dist/core/GthLangChainAgent.js +392 -2
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/approvalRequest.d.ts +142 -0
- package/dist/core/approvals/approvalRequest.js +198 -3
- package/dist/core/approvals/approvalRequest.js.map +1 -1
- package/dist/core/approvals/conversationGrants.d.ts +60 -0
- package/dist/core/approvals/conversationGrants.js +77 -0
- package/dist/core/approvals/conversationGrants.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +16 -0
- package/dist/core/approvals/grants.js +20 -5
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/compaction.d.ts +181 -0
- package/dist/core/compaction.js +293 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/compactionThreshold.d.ts +158 -0
- package/dist/core/compactionThreshold.js +183 -0
- package/dist/core/compactionThreshold.js.map +1 -0
- package/dist/core/contextWindow.d.ts +146 -0
- package/dist/core/contextWindow.js +256 -0
- package/dist/core/contextWindow.js.map +1 -0
- package/dist/core/exitOutputChannel.d.ts +51 -0
- package/dist/core/exitOutputChannel.js +65 -0
- package/dist/core/exitOutputChannel.js.map +1 -0
- package/dist/core/refusal.d.ts +17 -2
- package/dist/core/refusal.js +80 -14
- package/dist/core/refusal.js.map +1 -1
- package/dist/core/runStats.d.ts +1 -1
- package/dist/core/terminationNotice.d.ts +8 -0
- package/dist/core/terminationNotice.js +10 -4
- package/dist/core/terminationNotice.js.map +1 -1
- package/dist/core/terminationReason.d.ts +28 -0
- package/dist/core/terminationReason.js +27 -0
- package/dist/core/terminationReason.js.map +1 -1
- package/dist/core/types.d.ts +35 -1
- package/dist/core/types.js.map +1 -1
- package/dist/history/checkpointRetention.d.ts +279 -0
- package/dist/history/checkpointRetention.js +567 -0
- package/dist/history/checkpointRetention.js.map +1 -0
- package/dist/history/checkpointSaver.d.ts +93 -0
- package/dist/history/checkpointSaver.js +464 -0
- package/dist/history/checkpointSaver.js.map +1 -0
- package/dist/history/historyEnabled.d.ts +27 -0
- package/dist/history/historyEnabled.js +23 -0
- package/dist/history/historyEnabled.js.map +1 -0
- package/dist/history/historyFormat.d.ts +27 -0
- package/dist/history/historyFormat.js +125 -2
- package/dist/history/historyFormat.js.map +1 -1
- package/dist/history/historyStore.d.ts +61 -0
- package/dist/history/historyStore.js +180 -7
- package/dist/history/historyStore.js.map +1 -1
- package/dist/history/recordSession.d.ts +84 -22
- package/dist/history/recordSession.js +187 -12
- package/dist/history/recordSession.js.map +1 -1
- package/dist/history/sessionCheckpointer.d.ts +48 -0
- package/dist/history/sessionCheckpointer.js +200 -0
- package/dist/history/sessionCheckpointer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/modelCatalog.d.ts +14 -0
- package/dist/providers/modelCatalog.js +4 -0
- package/dist/providers/modelCatalog.js.map +1 -1
- package/dist/providers/modelDiscovery.d.ts +3 -1
- package/dist/providers/modelDiscovery.js +22 -8
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.js +3 -19
- package/dist/providers/ollama.js.map +1 -1
- package/dist/runtime/conversation.js +7 -1
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.js +6 -1
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +77 -0
- package/dist/utils/consoleUtils.js +81 -0
- package/dist/utils/consoleUtils.js.map +1 -1
- package/package.json +2 -2
- package/schema/gsloth-config.schema.json +33 -14
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-107 — **the retention policy for the checkpoint tables, and the readout that makes their
|
|
4
|
+
* size visible.**
|
|
5
|
+
*
|
|
6
|
+
* A checkpoint is not a history row. A `sessions` row is a prompt and a response; a checkpoint is
|
|
7
|
+
* the state the agent was working with — tool results verbatim, file contents that were read,
|
|
8
|
+
* command output, whatever an MCP server returned. `history.enabled` is on by default, so every
|
|
9
|
+
* interactive session writes full graph state per super-step, and until this module nothing ever
|
|
10
|
+
* reclaimed a byte.
|
|
11
|
+
*
|
|
12
|
+
* ## The policy, and why it is split in two
|
|
13
|
+
*
|
|
14
|
+
* **Automatic reclamation removes only what can never be resumed. Anything that would cost a user a
|
|
15
|
+
* resume is available only as an explicit command they type.** That follows the ruling GS2-20 was
|
|
16
|
+
* built to: resume sheds nothing, and shedding is the user's own act. An age-based sweep that
|
|
17
|
+
* silently made an old conversation unresumable would remove, without being asked, the one
|
|
18
|
+
* capability the durable checkpointer exists to provide.
|
|
19
|
+
*
|
|
20
|
+
* So:
|
|
21
|
+
*
|
|
22
|
+
* - {@link reclaimUnresumableThreads} is automatic and cannot cost a resume, because it only
|
|
23
|
+
* deletes threads **no conversation row names** — see the predicate below.
|
|
24
|
+
* - {@link selectPrunableConversations} + {@link deleteThreads} is `gth history prune`: it removes
|
|
25
|
+
* state a user *could* still have resumed, so it is never automatic, never has a silent default
|
|
26
|
+
* bound, and says what it will remove before it removes it.
|
|
27
|
+
*
|
|
28
|
+
* ## The predicate: one class, not two
|
|
29
|
+
*
|
|
30
|
+
* A resume travels exactly one link — `gth history list` prints a conversation id,
|
|
31
|
+
* `conversations.thread_id` turns that id into a thread, and the thread is what
|
|
32
|
+
* `resolveResumeTarget` looks up. So a thread that **no conversation row names** cannot be reached
|
|
33
|
+
* by any id a person could type: it is unaddressable, and deleting it removes no capability anyone
|
|
34
|
+
* had.
|
|
35
|
+
*
|
|
36
|
+
* That single predicate covers both classes retention has to reclaim, because the second collapses
|
|
37
|
+
* into the first at the row level:
|
|
38
|
+
*
|
|
39
|
+
* - a thread with **no conversation row at all** — a `/clear` mints a fresh thread that nothing ever
|
|
40
|
+
* names ([[EXT-109]]), and an abandoned boot or a test can leave one too;
|
|
41
|
+
* - a conversation whose **`thread_id` is NULL** — written by `clearConversationThread` when a
|
|
42
|
+
* checkpoint write fails. NULLing the column *destroys the link*, so the thread it used to name is
|
|
43
|
+
* thereafter an orphan by exactly the definition above. There is no second query to write.
|
|
44
|
+
*
|
|
45
|
+
* ## Whole threads, never a prefix
|
|
46
|
+
*
|
|
47
|
+
* Pruning the middle of a thread is forbidden here even though it was **measured not to break this
|
|
48
|
+
* graph**: `channelsFromCheckpoint` walks ancestors only for a `DeltaChannel` absent from
|
|
49
|
+
* `channel_values`, and this state schema has none (`messages` is a `BinaryOperatorAggregate` and
|
|
50
|
+
* every checkpoint carries the whole array), so `getDeltaChannelHistory` is never called on a
|
|
51
|
+
* resume. A prefix policy is nevertheless a wider decision than retention makes, and it would become
|
|
52
|
+
* wrong the moment a channel moves behind a reducer. Whole threads only.
|
|
53
|
+
*
|
|
54
|
+
* ## Two guards, and which one covers which case
|
|
55
|
+
*
|
|
56
|
+
* `/clear` rotates a **live** session onto a thread no conversation row names, and it stays there
|
|
57
|
+
* for the rest of the session. So "unaddressable" does not imply "finished": a thread being written
|
|
58
|
+
* right now can satisfy the predicate, and deleting it is silent amnesia rather than a lost resume,
|
|
59
|
+
* because both interactive surfaces send only the new message and let checkpoint state carry the
|
|
60
|
+
* conversation.
|
|
61
|
+
*
|
|
62
|
+
* - **In this process, the write set.** `GthSqliteSaver` remembers every `thread_id` it has written
|
|
63
|
+
* and excludes that set from every pass it runs. It is the only place that knows: the runner
|
|
64
|
+
* rotates threads without notifying the checkpointer, so an exclusion assembled by a caller from
|
|
65
|
+
* the id a session started with names the wrong thread after the first `/clear` or `/resume`.
|
|
66
|
+
* - **Across processes, {@link RECLAIM_GRACE_MS}.** Another process cannot be asked whether it is
|
|
67
|
+
* still there, so nothing is reclaimed until its newest checkpoint is older than the window. A
|
|
68
|
+
* thread whose age cannot be established is left alone.
|
|
69
|
+
*
|
|
70
|
+
* The residual is a session in another process that has been idle longer than the window — the
|
|
71
|
+
* write set does not reach it and the age gate no longer holds it. Closing that needs shared
|
|
72
|
+
* cross-process session state, which retention does not add.
|
|
73
|
+
*/
|
|
74
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
75
|
+
import { existsSync, statSync } from 'node:fs';
|
|
76
|
+
/**
|
|
77
|
+
* How stale a thread's newest checkpoint must be before automatic reclamation will touch it.
|
|
78
|
+
*
|
|
79
|
+
* Not a tidy-up margin: after a `/clear` a live session writes every remaining checkpoint under a
|
|
80
|
+
* thread no conversation row names, and another process cannot be asked whether it is still there.
|
|
81
|
+
* A day is far longer than the gap between two super-steps of a session anyone is still using,
|
|
82
|
+
* while still reclaiming the rows within a day of the session ending.
|
|
83
|
+
*/
|
|
84
|
+
export const RECLAIM_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
85
|
+
/** How many threads the readout names individually. */
|
|
86
|
+
export const DEFAULT_TOP_THREADS = 5;
|
|
87
|
+
const EMPTY_RECLAIM = {
|
|
88
|
+
threadCount: 0,
|
|
89
|
+
checkpointCount: 0,
|
|
90
|
+
writeCount: 0,
|
|
91
|
+
bytes: 0,
|
|
92
|
+
};
|
|
93
|
+
/** Whether a table exists in the open database. */
|
|
94
|
+
function hasTable(db, name) {
|
|
95
|
+
try {
|
|
96
|
+
const row = db
|
|
97
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
98
|
+
.get(name);
|
|
99
|
+
return row !== undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True when both halves of the link this module reasons over are present. Without `conversations`
|
|
107
|
+
* every thread would look unaddressable and a reclamation pass would delete the whole store — so a
|
|
108
|
+
* database missing that table is left entirely alone rather than swept.
|
|
109
|
+
*/
|
|
110
|
+
export function retentionTablesReady(db) {
|
|
111
|
+
return hasTable(db, 'checkpoints') && hasTable(db, 'conversations');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The `ts` a checkpoint recorded for itself, or `undefined` when it cannot be read.
|
|
115
|
+
*
|
|
116
|
+
* The saver stores the serializer's own bytes; the shipped `JsonPlusSerializer` writes plain JSON
|
|
117
|
+
* with a top-level ISO `ts`, which every checkpoint LangGraph builds carries. Anything else — a row
|
|
118
|
+
* from another serializer, a truncated blob — yields `undefined`, and the callers treat that as
|
|
119
|
+
* "age unknown", which is the safe direction: an unknown age is never old enough to reclaim.
|
|
120
|
+
*/
|
|
121
|
+
function checkpointTs(blob) {
|
|
122
|
+
if (!(blob instanceof Uint8Array))
|
|
123
|
+
return undefined;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(new TextDecoder().decode(blob));
|
|
126
|
+
return typeof parsed.ts === 'string' ? parsed.ts : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* How many thread ids go into one `IN (…)` list. SQLite caps the number of bound parameters in a
|
|
134
|
+
* statement (999 on a default build), and a store with more unaddressable threads than that is
|
|
135
|
+
* exactly the store this feature exists for — so the query is chunked rather than assumed small.
|
|
136
|
+
*/
|
|
137
|
+
const ID_CHUNK = 200;
|
|
138
|
+
/**
|
|
139
|
+
* The predicate itself, as one constant.
|
|
140
|
+
*
|
|
141
|
+
* It asks `conversations` a question once per distinct thread in the store, so it is only cheap
|
|
142
|
+
* while `conversations.thread_id` is indexed — `conversations.id` is a rowid alias and indexes
|
|
143
|
+
* nothing else, which left this scanning the whole table per thread (measured: 3.18s at 6,000
|
|
144
|
+
* threads, 13ms with `idx_conversations_thread_id`, created in `historyStore.migrate`). Named here
|
|
145
|
+
* rather than inlined so the spec that asks SQLite for the query plan is looking at the same text
|
|
146
|
+
* this runs, and cannot go on passing after the query is edited.
|
|
147
|
+
*/
|
|
148
|
+
export const UNADDRESSABLE_THREADS_SQL = `SELECT DISTINCT c.thread_id AS thread_id
|
|
149
|
+
FROM checkpoints c
|
|
150
|
+
WHERE NOT EXISTS (
|
|
151
|
+
SELECT 1 FROM conversations v WHERE v.thread_id = c.thread_id
|
|
152
|
+
)`;
|
|
153
|
+
/** Bytes stored under each of `threadIds`, as one grouped query per chunk. */
|
|
154
|
+
function bytesByThread(db, threadIds) {
|
|
155
|
+
const out = new Map();
|
|
156
|
+
if (threadIds.length === 0)
|
|
157
|
+
return out;
|
|
158
|
+
for (const id of threadIds)
|
|
159
|
+
out.set(id, { ...EMPTY_RECLAIM, threadCount: 1 });
|
|
160
|
+
for (let start = 0; start < threadIds.length; start += ID_CHUNK) {
|
|
161
|
+
const chunk = threadIds.slice(start, start + ID_CHUNK);
|
|
162
|
+
const holes = chunk.map(() => '?').join(', ');
|
|
163
|
+
const checkpointRows = db
|
|
164
|
+
.prepare(`SELECT thread_id,
|
|
165
|
+
COUNT(*) AS n,
|
|
166
|
+
COALESCE(SUM(LENGTH(checkpoint) + LENGTH(metadata)), 0) AS bytes
|
|
167
|
+
FROM checkpoints
|
|
168
|
+
WHERE thread_id IN (${holes})
|
|
169
|
+
GROUP BY thread_id`)
|
|
170
|
+
.all(...chunk);
|
|
171
|
+
const writeRows = db
|
|
172
|
+
.prepare(`SELECT thread_id, COUNT(*) AS n, COALESCE(SUM(LENGTH(value)), 0) AS bytes
|
|
173
|
+
FROM checkpoint_writes
|
|
174
|
+
WHERE thread_id IN (${holes})
|
|
175
|
+
GROUP BY thread_id`)
|
|
176
|
+
.all(...chunk);
|
|
177
|
+
for (const r of checkpointRows) {
|
|
178
|
+
const entry = out.get(String(r.thread_id));
|
|
179
|
+
if (!entry)
|
|
180
|
+
continue;
|
|
181
|
+
entry.checkpointCount = Number(r.n ?? 0);
|
|
182
|
+
entry.bytes += Number(r.bytes ?? 0);
|
|
183
|
+
}
|
|
184
|
+
for (const r of writeRows) {
|
|
185
|
+
const entry = out.get(String(r.thread_id));
|
|
186
|
+
if (!entry)
|
|
187
|
+
continue;
|
|
188
|
+
entry.writeCount = Number(r.n ?? 0);
|
|
189
|
+
entry.bytes += Number(r.bytes ?? 0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Every thread in the checkpoint tables that **no conversation row names** — see the module note for
|
|
196
|
+
* why that one predicate covers both unresumable classes.
|
|
197
|
+
*
|
|
198
|
+
* `includeWithinGrace: true` answers "what is unaddressable", which is what the readout reports;
|
|
199
|
+
* the default answers "what may be deleted now", which additionally requires the thread's newest
|
|
200
|
+
* checkpoint to be older than {@link RECLAIM_GRACE_MS}. Never throws.
|
|
201
|
+
*/
|
|
202
|
+
export function findUnaddressableThreads(db, options = {}) {
|
|
203
|
+
if (!retentionTablesReady(db))
|
|
204
|
+
return [];
|
|
205
|
+
const graceMs = options.graceMs ?? RECLAIM_GRACE_MS;
|
|
206
|
+
const now = options.now ?? Date.now();
|
|
207
|
+
const excluded = new Set(options.excludeThreadIds ?? []);
|
|
208
|
+
try {
|
|
209
|
+
// The predicate itself, with no blobs in it: the readout asks this question over the whole
|
|
210
|
+
// store, and dragging a checkpoint payload back for every thread would make a report cost what
|
|
211
|
+
// a delete costs.
|
|
212
|
+
const rows = db.prepare(UNADDRESSABLE_THREADS_SQL).all();
|
|
213
|
+
const candidates = rows
|
|
214
|
+
.map((row) => String(row.thread_id))
|
|
215
|
+
.filter((threadId) => !excluded.has(threadId));
|
|
216
|
+
if (options.includeWithinGrace)
|
|
217
|
+
return candidates;
|
|
218
|
+
// The age gate. Only the NEWEST checkpoint of each candidate is read — one row per thread, not
|
|
219
|
+
// a scan of its history — because a thread is live if anything in it is recent. Checkpoint ids
|
|
220
|
+
// are uuid6 and sort lexicographically in creation order, the same ordering `getTuple` uses for
|
|
221
|
+
// "the latest checkpoint on this thread".
|
|
222
|
+
const newest = db.prepare(`SELECT checkpoint FROM checkpoints
|
|
223
|
+
WHERE thread_id = ?
|
|
224
|
+
ORDER BY checkpoint_id DESC
|
|
225
|
+
LIMIT 1`);
|
|
226
|
+
const threads = [];
|
|
227
|
+
for (const threadId of candidates) {
|
|
228
|
+
const row = newest.get(threadId);
|
|
229
|
+
const ts = checkpointTs(row?.checkpoint);
|
|
230
|
+
if (ts === undefined)
|
|
231
|
+
continue; // age unknown ⇒ never old enough
|
|
232
|
+
const age = now - Date.parse(ts);
|
|
233
|
+
if (!Number.isFinite(age) || age < graceMs)
|
|
234
|
+
continue;
|
|
235
|
+
threads.push(threadId);
|
|
236
|
+
}
|
|
237
|
+
return threads;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Delete every checkpoint and pending write of the named threads, and report what went.
|
|
245
|
+
*
|
|
246
|
+
* The single-thread spelling on the saver (`GthSqliteSaver.deleteThread`) routes through here, so
|
|
247
|
+
* there is one implementation of the delete rather than two that can drift.
|
|
248
|
+
*
|
|
249
|
+
* **Both tables go in one transaction.** A thread's rows live in `checkpoints` and in
|
|
250
|
+
* `checkpoint_writes`, and only the first of those is what anything looks for: every candidate query
|
|
251
|
+
* in this module reads `FROM checkpoints`. So a failure between the two statements would leave
|
|
252
|
+
* `checkpoint_writes` rows belonging to a thread that no longer appears in `checkpoints` — bytes the
|
|
253
|
+
* readout still counts as stored, that no later pass can find, and that no reader can reach. Not a
|
|
254
|
+
* self-healing leak; a permanent one. Either both deletes land or neither does.
|
|
255
|
+
*
|
|
256
|
+
* The caller must not already be inside a transaction: the rollback here would discard theirs. No
|
|
257
|
+
* caller is — the two entry points are the close hook and `gth history prune`.
|
|
258
|
+
*/
|
|
259
|
+
export function deleteThreads(db, threadIds) {
|
|
260
|
+
const ids = [...new Set(threadIds)].filter((id) => id.length > 0);
|
|
261
|
+
if (ids.length === 0)
|
|
262
|
+
return { ...EMPTY_RECLAIM };
|
|
263
|
+
try {
|
|
264
|
+
const perThread = bytesByThread(db, ids);
|
|
265
|
+
const summary = { ...EMPTY_RECLAIM };
|
|
266
|
+
const deleteCheckpoints = db.prepare(`DELETE FROM checkpoints WHERE thread_id = ?`);
|
|
267
|
+
const deleteWrites = db.prepare(`DELETE FROM checkpoint_writes WHERE thread_id = ?`);
|
|
268
|
+
db.exec('BEGIN');
|
|
269
|
+
try {
|
|
270
|
+
for (const id of ids) {
|
|
271
|
+
deleteCheckpoints.run(id);
|
|
272
|
+
deleteWrites.run(id);
|
|
273
|
+
const counted = perThread.get(id);
|
|
274
|
+
if (!counted)
|
|
275
|
+
continue;
|
|
276
|
+
summary.threadCount += 1;
|
|
277
|
+
summary.checkpointCount += counted.checkpointCount;
|
|
278
|
+
summary.writeCount += counted.writeCount;
|
|
279
|
+
summary.bytes += counted.bytes;
|
|
280
|
+
}
|
|
281
|
+
db.exec('COMMIT');
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
try {
|
|
285
|
+
db.exec('ROLLBACK');
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
/* the connection may already have unwound the transaction; nothing left to undo */
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
return summary;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Nothing was deleted, so nothing is reported as deleted — the summary the partial loop had
|
|
296
|
+
// accumulated describes rows the rollback put back.
|
|
297
|
+
return { ...EMPTY_RECLAIM };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The automatic half of the policy: delete every thread no conversation row names and whose newest
|
|
302
|
+
* checkpoint is past the grace window. Never throws, and never touches a thread a resume could
|
|
303
|
+
* reach.
|
|
304
|
+
*/
|
|
305
|
+
export function reclaimUnresumableThreads(db, options = {}) {
|
|
306
|
+
const threads = findUnaddressableThreads(db, options);
|
|
307
|
+
if (threads.length === 0)
|
|
308
|
+
return { ...EMPTY_RECLAIM };
|
|
309
|
+
return deleteThreads(db, threads);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The conversations `gth history prune` would remove the stored state of, newest activity first.
|
|
313
|
+
*
|
|
314
|
+
* A conversation qualifies when it still names a thread that has checkpoints (there is something to
|
|
315
|
+
* remove) and it satisfies **every** bound given. `olderThanDays` and `keepLast` therefore compose
|
|
316
|
+
* as a conjunction: with both, a conversation is pruned only when it is older than the age AND
|
|
317
|
+
* outside the newest N. Passing neither selects nothing — the command requires an explicit bound,
|
|
318
|
+
* so that there is no silent default for an operation that can cost a resume.
|
|
319
|
+
*
|
|
320
|
+
* **Neither of the automatic pass's two guards applies here, and that is the design.** The bounds
|
|
321
|
+
* the person typed are the whole selection: a grace window layered on top would quietly shrink the
|
|
322
|
+
* set they asked for, and the in-process write set is empty in a `gth history prune` process, which
|
|
323
|
+
* shares no state with the session in the window next door. What this does instead is *say so* —
|
|
324
|
+
* {@link PrunableConversation.recentlyActive} marks every candidate whose last turn is inside
|
|
325
|
+
* {@link RECLAIM_GRACE_MS}, the plan prints that marker, and the command asks before removing
|
|
326
|
+
* anything.
|
|
327
|
+
*/
|
|
328
|
+
export function selectPrunableConversations(db, bounds) {
|
|
329
|
+
if (!retentionTablesReady(db))
|
|
330
|
+
return [];
|
|
331
|
+
if (bounds.olderThanDays === undefined && bounds.keepLast === undefined)
|
|
332
|
+
return [];
|
|
333
|
+
try {
|
|
334
|
+
const rows = db
|
|
335
|
+
.prepare(`SELECT c.id AS id, c.thread_id AS thread_id, c.command AS command,
|
|
336
|
+
c.started_ts AS started_ts,
|
|
337
|
+
COUNT(s.id) AS turn_count,
|
|
338
|
+
MAX(s.ts) AS last_ts
|
|
339
|
+
FROM conversations c
|
|
340
|
+
LEFT JOIN sessions s ON s.conversation_id = c.id
|
|
341
|
+
WHERE c.thread_id IS NOT NULL AND c.thread_id <> ''
|
|
342
|
+
GROUP BY c.id`)
|
|
343
|
+
.all();
|
|
344
|
+
const candidates = rows.map((r) => ({
|
|
345
|
+
conversationId: Number(r.id),
|
|
346
|
+
threadId: String(r.thread_id),
|
|
347
|
+
command: r.command != null ? String(r.command) : undefined,
|
|
348
|
+
lastActivityTs: r.last_ts != null ? String(r.last_ts) : String(r.started_ts),
|
|
349
|
+
turnCount: Number(r.turn_count ?? 0),
|
|
350
|
+
}));
|
|
351
|
+
// Newest activity first, so `keepLast` counts from the most recent conversation.
|
|
352
|
+
candidates.sort((a, b) => b.lastActivityTs.localeCompare(a.lastActivityTs));
|
|
353
|
+
const now = bounds.now ?? Date.now();
|
|
354
|
+
const cutoff = bounds.olderThanDays === undefined
|
|
355
|
+
? undefined
|
|
356
|
+
: now - bounds.olderThanDays * 24 * 60 * 60 * 1000;
|
|
357
|
+
const selected = candidates.filter((candidate, index) => {
|
|
358
|
+
if (bounds.keepLast !== undefined && index < bounds.keepLast)
|
|
359
|
+
return false;
|
|
360
|
+
if (cutoff !== undefined) {
|
|
361
|
+
const at = Date.parse(candidate.lastActivityTs);
|
|
362
|
+
// An unparseable timestamp is an unknown age, and an unknown age never satisfies an age
|
|
363
|
+
// bound — the same safe direction the grace window takes.
|
|
364
|
+
if (!Number.isFinite(at) || at >= cutoff)
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
return true;
|
|
368
|
+
});
|
|
369
|
+
if (selected.length === 0)
|
|
370
|
+
return [];
|
|
371
|
+
const perThread = bytesByThread(db, selected.map((c) => c.threadId));
|
|
372
|
+
// Only a conversation with something actually stored is offered: naming one whose thread holds
|
|
373
|
+
// no rows would report a removal that reclaims nothing.
|
|
374
|
+
return selected
|
|
375
|
+
.map((c) => {
|
|
376
|
+
const counted = perThread.get(c.threadId) ?? EMPTY_RECLAIM;
|
|
377
|
+
const at = Date.parse(c.lastActivityTs);
|
|
378
|
+
return {
|
|
379
|
+
...c,
|
|
380
|
+
checkpointCount: counted.checkpointCount,
|
|
381
|
+
bytes: counted.bytes,
|
|
382
|
+
// Same window the automatic pass uses, read off the turn row rather than the checkpoint
|
|
383
|
+
// blob: the turn is written after the checkpoints of that turn, so it is the later of the
|
|
384
|
+
// two and a conservative answer to "was something happening here recently".
|
|
385
|
+
recentlyActive: Number.isFinite(at) && now - at < RECLAIM_GRACE_MS,
|
|
386
|
+
};
|
|
387
|
+
})
|
|
388
|
+
.filter((c) => c.checkpointCount > 0);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* `VACUUM` — the part that actually gives the disk space back. Deleting rows in SQLite moves pages
|
|
396
|
+
* onto the free list and leaves the file exactly as large as it was, so a prune that skipped this
|
|
397
|
+
* would report bytes removed while the file a user can see never moved.
|
|
398
|
+
*
|
|
399
|
+
* Returns whether it ran. It cannot run inside a transaction, and it needs the file to itself; a
|
|
400
|
+
* failure is reported rather than thrown, because the rows are already gone and the run should say
|
|
401
|
+
* so.
|
|
402
|
+
*/
|
|
403
|
+
export function vacuumStore(db) {
|
|
404
|
+
try {
|
|
405
|
+
db.exec('VACUUM');
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* The readout: what the checkpoint tables hold, and what of it is unaddressable.
|
|
414
|
+
*
|
|
415
|
+
* `fileBytes` and `checkpointBytes` are reported separately on purpose — the same file also holds
|
|
416
|
+
* the session transcripts and the FTS index, so one number labelled "checkpoints" that is really the
|
|
417
|
+
* whole file would be a false statement on the very screen this exists to make honest.
|
|
418
|
+
*/
|
|
419
|
+
export function collectCheckpointStoreStats(db, dbPath, topN = DEFAULT_TOP_THREADS) {
|
|
420
|
+
const empty = {
|
|
421
|
+
dbPath,
|
|
422
|
+
fileBytes: 0,
|
|
423
|
+
checkpointBytes: 0,
|
|
424
|
+
checkpointCount: 0,
|
|
425
|
+
writeCount: 0,
|
|
426
|
+
threadCount: 0,
|
|
427
|
+
largestThreads: [],
|
|
428
|
+
unresumableThreadCount: 0,
|
|
429
|
+
unresumableBytes: 0,
|
|
430
|
+
};
|
|
431
|
+
let fileBytes = 0;
|
|
432
|
+
try {
|
|
433
|
+
if (existsSync(dbPath))
|
|
434
|
+
fileBytes = statSync(dbPath).size;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
/* an unreadable file is reported as zero bytes rather than failing the readout */
|
|
438
|
+
}
|
|
439
|
+
if (!hasTable(db, 'checkpoints'))
|
|
440
|
+
return { ...empty, fileBytes };
|
|
441
|
+
try {
|
|
442
|
+
const totals = db
|
|
443
|
+
.prepare(`SELECT COUNT(*) AS n,
|
|
444
|
+
COUNT(DISTINCT thread_id) AS threads,
|
|
445
|
+
COALESCE(SUM(LENGTH(checkpoint) + LENGTH(metadata)), 0) AS bytes
|
|
446
|
+
FROM checkpoints`)
|
|
447
|
+
.get();
|
|
448
|
+
const writes = db
|
|
449
|
+
.prepare(`SELECT COUNT(*) AS n, COALESCE(SUM(LENGTH(value)), 0) AS bytes FROM checkpoint_writes`)
|
|
450
|
+
.get();
|
|
451
|
+
// The per-thread roll-up carries the conversation that names each thread, so the readout can
|
|
452
|
+
// say which of the biggest threads is a conversation someone could resume and which is dead
|
|
453
|
+
// weight. A LEFT JOIN, because the unaddressable ones are exactly those with no match.
|
|
454
|
+
const conversationsPresent = hasTable(db, 'conversations');
|
|
455
|
+
const threadRows = db
|
|
456
|
+
.prepare(conversationsPresent
|
|
457
|
+
? `SELECT t.thread_id AS thread_id, t.n AS n, t.bytes AS bytes,
|
|
458
|
+
v.id AS conversation_id, v.command AS command
|
|
459
|
+
FROM (SELECT thread_id, COUNT(*) AS n,
|
|
460
|
+
COALESCE(SUM(LENGTH(checkpoint) + LENGTH(metadata)), 0) AS bytes
|
|
461
|
+
FROM checkpoints GROUP BY thread_id) t
|
|
462
|
+
LEFT JOIN conversations v ON v.thread_id = t.thread_id
|
|
463
|
+
ORDER BY t.bytes DESC, t.thread_id ASC
|
|
464
|
+
LIMIT ?`
|
|
465
|
+
: `SELECT thread_id, COUNT(*) AS n,
|
|
466
|
+
COALESCE(SUM(LENGTH(checkpoint) + LENGTH(metadata)), 0) AS bytes,
|
|
467
|
+
NULL AS conversation_id, NULL AS command
|
|
468
|
+
FROM checkpoints
|
|
469
|
+
GROUP BY thread_id
|
|
470
|
+
ORDER BY bytes DESC, thread_id ASC
|
|
471
|
+
LIMIT ?`)
|
|
472
|
+
.all(Math.max(0, topN));
|
|
473
|
+
const unaddressable = findUnaddressableThreads(db, { includeWithinGrace: true });
|
|
474
|
+
const unaddressableBytes = [...bytesByThread(db, unaddressable).values()].reduce((sum, entry) => sum + entry.bytes, 0);
|
|
475
|
+
return {
|
|
476
|
+
dbPath,
|
|
477
|
+
fileBytes,
|
|
478
|
+
checkpointBytes: Number(totals.bytes ?? 0) + Number(writes.bytes ?? 0),
|
|
479
|
+
checkpointCount: Number(totals.n ?? 0),
|
|
480
|
+
writeCount: Number(writes.n ?? 0),
|
|
481
|
+
threadCount: Number(totals.threads ?? 0),
|
|
482
|
+
largestThreads: threadRows.map((r) => ({
|
|
483
|
+
threadId: String(r.thread_id),
|
|
484
|
+
conversationId: r.conversation_id != null ? Number(r.conversation_id) : undefined,
|
|
485
|
+
command: r.command != null ? String(r.command) : undefined,
|
|
486
|
+
checkpointCount: Number(r.n ?? 0),
|
|
487
|
+
bytes: Number(r.bytes ?? 0),
|
|
488
|
+
})),
|
|
489
|
+
unresumableThreadCount: unaddressable.length,
|
|
490
|
+
unresumableBytes: unaddressableBytes,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return { ...empty, fileBytes };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* A read/write connection to the history file for the maintenance commands, or `null` when there is
|
|
499
|
+
* nothing there. Fail-soft in the same shape as `openHistoryStore` / `openCheckpointSaver`, and it
|
|
500
|
+
* never CREATES the file: `gth history prune` on a machine with no history should say there is none,
|
|
501
|
+
* not leave an empty database behind.
|
|
502
|
+
*/
|
|
503
|
+
export class CheckpointMaintenance {
|
|
504
|
+
db;
|
|
505
|
+
constructor(db) {
|
|
506
|
+
this.db = db;
|
|
507
|
+
}
|
|
508
|
+
static open(dbPath) {
|
|
509
|
+
if (dbPath !== ':memory:' && !existsSync(dbPath))
|
|
510
|
+
return null;
|
|
511
|
+
let db;
|
|
512
|
+
try {
|
|
513
|
+
db = new DatabaseSync(dbPath);
|
|
514
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
515
|
+
// A garbage file opens cleanly and only fails on the first statement, so the probe is what
|
|
516
|
+
// decides whether this handle is usable at all.
|
|
517
|
+
db.prepare(`SELECT name FROM sqlite_master LIMIT 1`).all();
|
|
518
|
+
return new CheckpointMaintenance(db);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
try {
|
|
522
|
+
db?.close();
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
/* ignore: the connection may already be broken */
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
stats(dbPath, topN = DEFAULT_TOP_THREADS) {
|
|
531
|
+
return collectCheckpointStoreStats(this.db, dbPath, topN);
|
|
532
|
+
}
|
|
533
|
+
prunable(bounds) {
|
|
534
|
+
return selectPrunableConversations(this.db, bounds);
|
|
535
|
+
}
|
|
536
|
+
unaddressable(options = {}) {
|
|
537
|
+
return findUnaddressableThreads(this.db, options);
|
|
538
|
+
}
|
|
539
|
+
remove(threadIds) {
|
|
540
|
+
return deleteThreads(this.db, threadIds);
|
|
541
|
+
}
|
|
542
|
+
/** Bytes stored under exactly these threads — what removing them would reclaim. */
|
|
543
|
+
bytesOf(threadIds) {
|
|
544
|
+
try {
|
|
545
|
+
return [...bytesByThread(this.db, [...new Set(threadIds)]).values()].reduce((sum, entry) => sum + entry.bytes, 0);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
vacuum() {
|
|
552
|
+
return vacuumStore(this.db);
|
|
553
|
+
}
|
|
554
|
+
close() {
|
|
555
|
+
try {
|
|
556
|
+
this.db.close();
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
/* ignore */
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** Fail-soft opener for {@link CheckpointMaintenance}; `null` when there is no store to maintain. */
|
|
564
|
+
export function openCheckpointMaintenance(dbPath) {
|
|
565
|
+
return CheckpointMaintenance.open(dbPath);
|
|
566
|
+
}
|
|
567
|
+
//# sourceMappingURL=checkpointRetention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpointRetention.js","sourceRoot":"","sources":["../../src/history/checkpointRetention.ts"],"names":[],"mappings":"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"}
|