@gaunt-sloth/core 2.0.0-beta.5 → 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.
Files changed (91) hide show
  1. package/dist/config/schema.d.ts +4 -0
  2. package/dist/config/schema.js +64 -2
  3. package/dist/config/schema.js.map +1 -1
  4. package/dist/config/tokenBudget.d.ts +88 -0
  5. package/dist/config/tokenBudget.js +155 -0
  6. package/dist/config/tokenBudget.js.map +1 -0
  7. package/dist/config/types.d.ts +33 -4
  8. package/dist/config/types.js.map +1 -1
  9. package/dist/config.d.ts +1 -0
  10. package/dist/config.js +5 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/GthAbstractAgent.d.ts +24 -0
  13. package/dist/core/GthAbstractAgent.js +37 -1
  14. package/dist/core/GthAbstractAgent.js.map +1 -1
  15. package/dist/core/GthAgentRunner.d.ts +216 -1
  16. package/dist/core/GthAgentRunner.js +424 -3
  17. package/dist/core/GthAgentRunner.js.map +1 -1
  18. package/dist/core/GthLangChainAgent.d.ts +196 -0
  19. package/dist/core/GthLangChainAgent.js +392 -2
  20. package/dist/core/GthLangChainAgent.js.map +1 -1
  21. package/dist/core/approvals/conversationGrants.d.ts +60 -0
  22. package/dist/core/approvals/conversationGrants.js +77 -0
  23. package/dist/core/approvals/conversationGrants.js.map +1 -0
  24. package/dist/core/approvals/grants.d.ts +16 -0
  25. package/dist/core/approvals/grants.js +20 -5
  26. package/dist/core/approvals/grants.js.map +1 -1
  27. package/dist/core/compaction.d.ts +181 -0
  28. package/dist/core/compaction.js +293 -0
  29. package/dist/core/compaction.js.map +1 -0
  30. package/dist/core/compactionThreshold.d.ts +158 -0
  31. package/dist/core/compactionThreshold.js +183 -0
  32. package/dist/core/compactionThreshold.js.map +1 -0
  33. package/dist/core/contextWindow.d.ts +146 -0
  34. package/dist/core/contextWindow.js +256 -0
  35. package/dist/core/contextWindow.js.map +1 -0
  36. package/dist/core/exitOutputChannel.d.ts +51 -0
  37. package/dist/core/exitOutputChannel.js +65 -0
  38. package/dist/core/exitOutputChannel.js.map +1 -0
  39. package/dist/core/refusal.d.ts +17 -2
  40. package/dist/core/refusal.js +80 -14
  41. package/dist/core/refusal.js.map +1 -1
  42. package/dist/core/runStats.d.ts +1 -1
  43. package/dist/core/terminationNotice.d.ts +8 -0
  44. package/dist/core/terminationNotice.js +10 -4
  45. package/dist/core/terminationNotice.js.map +1 -1
  46. package/dist/core/terminationReason.d.ts +28 -0
  47. package/dist/core/terminationReason.js +27 -0
  48. package/dist/core/terminationReason.js.map +1 -1
  49. package/dist/core/types.d.ts +35 -1
  50. package/dist/core/types.js.map +1 -1
  51. package/dist/history/checkpointRetention.d.ts +279 -0
  52. package/dist/history/checkpointRetention.js +567 -0
  53. package/dist/history/checkpointRetention.js.map +1 -0
  54. package/dist/history/checkpointSaver.d.ts +93 -0
  55. package/dist/history/checkpointSaver.js +464 -0
  56. package/dist/history/checkpointSaver.js.map +1 -0
  57. package/dist/history/historyEnabled.d.ts +27 -0
  58. package/dist/history/historyEnabled.js +23 -0
  59. package/dist/history/historyEnabled.js.map +1 -0
  60. package/dist/history/historyFormat.d.ts +27 -0
  61. package/dist/history/historyFormat.js +125 -2
  62. package/dist/history/historyFormat.js.map +1 -1
  63. package/dist/history/historyStore.d.ts +61 -0
  64. package/dist/history/historyStore.js +180 -7
  65. package/dist/history/historyStore.js.map +1 -1
  66. package/dist/history/recordSession.d.ts +84 -22
  67. package/dist/history/recordSession.js +187 -12
  68. package/dist/history/recordSession.js.map +1 -1
  69. package/dist/history/sessionCheckpointer.d.ts +48 -0
  70. package/dist/history/sessionCheckpointer.js +200 -0
  71. package/dist/history/sessionCheckpointer.js.map +1 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/providers/modelCatalog.d.ts +14 -0
  76. package/dist/providers/modelCatalog.js +4 -0
  77. package/dist/providers/modelCatalog.js.map +1 -1
  78. package/dist/providers/modelDiscovery.d.ts +3 -1
  79. package/dist/providers/modelDiscovery.js +22 -8
  80. package/dist/providers/modelDiscovery.js.map +1 -1
  81. package/dist/providers/ollama.js +3 -19
  82. package/dist/providers/ollama.js.map +1 -1
  83. package/dist/runtime/conversation.js +7 -1
  84. package/dist/runtime/conversation.js.map +1 -1
  85. package/dist/runtime/singleShot.js +6 -1
  86. package/dist/runtime/singleShot.js.map +1 -1
  87. package/dist/utils/consoleUtils.d.ts +77 -0
  88. package/dist/utils/consoleUtils.js +81 -0
  89. package/dist/utils/consoleUtils.js.map +1 -1
  90. package/package.json +2 -2
  91. package/schema/gsloth-config.schema.json +33 -14
@@ -0,0 +1,279 @@
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
+ /**
76
+ * How stale a thread's newest checkpoint must be before automatic reclamation will touch it.
77
+ *
78
+ * Not a tidy-up margin: after a `/clear` a live session writes every remaining checkpoint under a
79
+ * thread no conversation row names, and another process cannot be asked whether it is still there.
80
+ * A day is far longer than the gap between two super-steps of a session anyone is still using,
81
+ * while still reclaiming the rows within a day of the session ending.
82
+ */
83
+ export declare const RECLAIM_GRACE_MS: number;
84
+ /** How many threads the readout names individually. */
85
+ export declare const DEFAULT_TOP_THREADS = 5;
86
+ /** One thread's footprint in the checkpoint tables. */
87
+ export interface ThreadUsage {
88
+ threadId: string;
89
+ /** The conversation that names this thread, when one does — absent means unaddressable. */
90
+ conversationId?: number;
91
+ command?: string;
92
+ checkpointCount: number;
93
+ /** Bytes of checkpoint / metadata / pending-write blobs stored under the thread. */
94
+ bytes: number;
95
+ /** The `ts` of the newest checkpoint, when it could be read. */
96
+ newestTs?: string;
97
+ }
98
+ /** What the checkpoint tables hold, for the readout. */
99
+ export interface CheckpointStoreStats {
100
+ dbPath: string;
101
+ /**
102
+ * Size of the database file, which also holds `sessions` / `conversations` / the FTS index — so it
103
+ * is reported beside {@link checkpointBytes} rather than instead of it. Zero when the file is
104
+ * absent.
105
+ */
106
+ fileBytes: number;
107
+ /** Bytes of checkpoint, metadata and pending-write blobs — the checkpoint tables' own share. */
108
+ checkpointBytes: number;
109
+ checkpointCount: number;
110
+ writeCount: number;
111
+ threadCount: number;
112
+ /** The biggest threads by stored bytes, largest first. */
113
+ largestThreads: ThreadUsage[];
114
+ /** Threads no conversation row names, and what they cost — what reclamation will take. */
115
+ unresumableThreadCount: number;
116
+ unresumableBytes: number;
117
+ }
118
+ /** What one {@link reclaimUnresumableThreads} / {@link deleteThreads} pass removed. */
119
+ export interface ReclaimSummary {
120
+ threadCount: number;
121
+ checkpointCount: number;
122
+ writeCount: number;
123
+ bytes: number;
124
+ }
125
+ /** A conversation `gth history prune` would remove the stored state of. */
126
+ export interface PrunableConversation {
127
+ conversationId: number;
128
+ threadId: string;
129
+ command?: string;
130
+ /** Last recorded activity: the newest turn's timestamp, or the conversation's start. */
131
+ lastActivityTs: string;
132
+ turnCount: number;
133
+ checkpointCount: number;
134
+ bytes: number;
135
+ /**
136
+ * True when the last recorded turn is newer than {@link RECLAIM_GRACE_MS} — so this conversation
137
+ * may be **open in another window right now**, and pruning it would take a live session's memory
138
+ * rather than an old one's.
139
+ *
140
+ * A flag and not an exclusion, deliberately. `gth history prune` is given an explicit bound by the
141
+ * person typing it, and silently keeping back rows inside that bound would make the bound a lie;
142
+ * liveness across processes is also not knowable from here, so a refusal would be a guess wearing
143
+ * a guarantee. The automatic pass can be conservative because nobody asked for it. This one says
144
+ * what it is about to do and lets the person answer.
145
+ */
146
+ recentlyActive: boolean;
147
+ }
148
+ /** Bounds for {@link selectPrunableConversations}. At least one is required by the command. */
149
+ export interface PruneBounds {
150
+ /** Prune conversations whose last activity is older than this many days. */
151
+ olderThanDays?: number;
152
+ /**
153
+ * Keep the N most recently active resumable conversations **whole** and prune the rest.
154
+ *
155
+ * Deliberately NOT "keep the last N super-steps of each thread": that is the prefix policy this
156
+ * module forbids, and a flag whose name suggested it would be a documentation hazard.
157
+ */
158
+ keepLast?: number;
159
+ /** Injectable clock for the age bound. */
160
+ now?: number;
161
+ }
162
+ /**
163
+ * True when both halves of the link this module reasons over are present. Without `conversations`
164
+ * every thread would look unaddressable and a reclamation pass would delete the whole store — so a
165
+ * database missing that table is left entirely alone rather than swept.
166
+ */
167
+ export declare function retentionTablesReady(db: DatabaseSync): boolean;
168
+ /**
169
+ * The predicate itself, as one constant.
170
+ *
171
+ * It asks `conversations` a question once per distinct thread in the store, so it is only cheap
172
+ * while `conversations.thread_id` is indexed — `conversations.id` is a rowid alias and indexes
173
+ * nothing else, which left this scanning the whole table per thread (measured: 3.18s at 6,000
174
+ * threads, 13ms with `idx_conversations_thread_id`, created in `historyStore.migrate`). Named here
175
+ * rather than inlined so the spec that asks SQLite for the query plan is looking at the same text
176
+ * this runs, and cannot go on passing after the query is edited.
177
+ */
178
+ export declare const UNADDRESSABLE_THREADS_SQL = "SELECT DISTINCT c.thread_id AS thread_id\n FROM checkpoints c\n WHERE NOT EXISTS (\n SELECT 1 FROM conversations v WHERE v.thread_id = c.thread_id\n )";
179
+ /**
180
+ * Every thread in the checkpoint tables that **no conversation row names** — see the module note for
181
+ * why that one predicate covers both unresumable classes.
182
+ *
183
+ * `includeWithinGrace: true` answers "what is unaddressable", which is what the readout reports;
184
+ * the default answers "what may be deleted now", which additionally requires the thread's newest
185
+ * checkpoint to be older than {@link RECLAIM_GRACE_MS}. Never throws.
186
+ */
187
+ export declare function findUnaddressableThreads(db: DatabaseSync, options?: {
188
+ now?: number;
189
+ graceMs?: number;
190
+ includeWithinGrace?: boolean;
191
+ excludeThreadIds?: readonly string[];
192
+ }): string[];
193
+ /**
194
+ * Delete every checkpoint and pending write of the named threads, and report what went.
195
+ *
196
+ * The single-thread spelling on the saver (`GthSqliteSaver.deleteThread`) routes through here, so
197
+ * there is one implementation of the delete rather than two that can drift.
198
+ *
199
+ * **Both tables go in one transaction.** A thread's rows live in `checkpoints` and in
200
+ * `checkpoint_writes`, and only the first of those is what anything looks for: every candidate query
201
+ * in this module reads `FROM checkpoints`. So a failure between the two statements would leave
202
+ * `checkpoint_writes` rows belonging to a thread that no longer appears in `checkpoints` — bytes the
203
+ * readout still counts as stored, that no later pass can find, and that no reader can reach. Not a
204
+ * self-healing leak; a permanent one. Either both deletes land or neither does.
205
+ *
206
+ * The caller must not already be inside a transaction: the rollback here would discard theirs. No
207
+ * caller is — the two entry points are the close hook and `gth history prune`.
208
+ */
209
+ export declare function deleteThreads(db: DatabaseSync, threadIds: readonly string[]): ReclaimSummary;
210
+ /**
211
+ * The automatic half of the policy: delete every thread no conversation row names and whose newest
212
+ * checkpoint is past the grace window. Never throws, and never touches a thread a resume could
213
+ * reach.
214
+ */
215
+ export declare function reclaimUnresumableThreads(db: DatabaseSync, options?: {
216
+ now?: number;
217
+ graceMs?: number;
218
+ excludeThreadIds?: readonly string[];
219
+ }): ReclaimSummary;
220
+ /**
221
+ * The conversations `gth history prune` would remove the stored state of, newest activity first.
222
+ *
223
+ * A conversation qualifies when it still names a thread that has checkpoints (there is something to
224
+ * remove) and it satisfies **every** bound given. `olderThanDays` and `keepLast` therefore compose
225
+ * as a conjunction: with both, a conversation is pruned only when it is older than the age AND
226
+ * outside the newest N. Passing neither selects nothing — the command requires an explicit bound,
227
+ * so that there is no silent default for an operation that can cost a resume.
228
+ *
229
+ * **Neither of the automatic pass's two guards applies here, and that is the design.** The bounds
230
+ * the person typed are the whole selection: a grace window layered on top would quietly shrink the
231
+ * set they asked for, and the in-process write set is empty in a `gth history prune` process, which
232
+ * shares no state with the session in the window next door. What this does instead is *say so* —
233
+ * {@link PrunableConversation.recentlyActive} marks every candidate whose last turn is inside
234
+ * {@link RECLAIM_GRACE_MS}, the plan prints that marker, and the command asks before removing
235
+ * anything.
236
+ */
237
+ export declare function selectPrunableConversations(db: DatabaseSync, bounds: PruneBounds): PrunableConversation[];
238
+ /**
239
+ * `VACUUM` — the part that actually gives the disk space back. Deleting rows in SQLite moves pages
240
+ * onto the free list and leaves the file exactly as large as it was, so a prune that skipped this
241
+ * would report bytes removed while the file a user can see never moved.
242
+ *
243
+ * Returns whether it ran. It cannot run inside a transaction, and it needs the file to itself; a
244
+ * failure is reported rather than thrown, because the rows are already gone and the run should say
245
+ * so.
246
+ */
247
+ export declare function vacuumStore(db: DatabaseSync): boolean;
248
+ /**
249
+ * The readout: what the checkpoint tables hold, and what of it is unaddressable.
250
+ *
251
+ * `fileBytes` and `checkpointBytes` are reported separately on purpose — the same file also holds
252
+ * the session transcripts and the FTS index, so one number labelled "checkpoints" that is really the
253
+ * whole file would be a false statement on the very screen this exists to make honest.
254
+ */
255
+ export declare function collectCheckpointStoreStats(db: DatabaseSync, dbPath: string, topN?: number): CheckpointStoreStats;
256
+ /**
257
+ * A read/write connection to the history file for the maintenance commands, or `null` when there is
258
+ * nothing there. Fail-soft in the same shape as `openHistoryStore` / `openCheckpointSaver`, and it
259
+ * never CREATES the file: `gth history prune` on a machine with no history should say there is none,
260
+ * not leave an empty database behind.
261
+ */
262
+ export declare class CheckpointMaintenance {
263
+ private readonly db;
264
+ private constructor();
265
+ static open(dbPath: string): CheckpointMaintenance | null;
266
+ stats(dbPath: string, topN?: number): CheckpointStoreStats;
267
+ prunable(bounds: PruneBounds): PrunableConversation[];
268
+ unaddressable(options?: {
269
+ now?: number;
270
+ graceMs?: number;
271
+ }): string[];
272
+ remove(threadIds: readonly string[]): ReclaimSummary;
273
+ /** Bytes stored under exactly these threads — what removing them would reclaim. */
274
+ bytesOf(threadIds: readonly string[]): number;
275
+ vacuum(): boolean;
276
+ close(): void;
277
+ }
278
+ /** Fail-soft opener for {@link CheckpointMaintenance}; `null` when there is no store to maintain. */
279
+ export declare function openCheckpointMaintenance(dbPath: string): CheckpointMaintenance | null;