@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.
Files changed (94) 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/approvalRequest.d.ts +142 -0
  22. package/dist/core/approvals/approvalRequest.js +198 -3
  23. package/dist/core/approvals/approvalRequest.js.map +1 -1
  24. package/dist/core/approvals/conversationGrants.d.ts +60 -0
  25. package/dist/core/approvals/conversationGrants.js +77 -0
  26. package/dist/core/approvals/conversationGrants.js.map +1 -0
  27. package/dist/core/approvals/grants.d.ts +16 -0
  28. package/dist/core/approvals/grants.js +20 -5
  29. package/dist/core/approvals/grants.js.map +1 -1
  30. package/dist/core/compaction.d.ts +181 -0
  31. package/dist/core/compaction.js +293 -0
  32. package/dist/core/compaction.js.map +1 -0
  33. package/dist/core/compactionThreshold.d.ts +158 -0
  34. package/dist/core/compactionThreshold.js +183 -0
  35. package/dist/core/compactionThreshold.js.map +1 -0
  36. package/dist/core/contextWindow.d.ts +146 -0
  37. package/dist/core/contextWindow.js +256 -0
  38. package/dist/core/contextWindow.js.map +1 -0
  39. package/dist/core/exitOutputChannel.d.ts +51 -0
  40. package/dist/core/exitOutputChannel.js +65 -0
  41. package/dist/core/exitOutputChannel.js.map +1 -0
  42. package/dist/core/refusal.d.ts +17 -2
  43. package/dist/core/refusal.js +80 -14
  44. package/dist/core/refusal.js.map +1 -1
  45. package/dist/core/runStats.d.ts +1 -1
  46. package/dist/core/terminationNotice.d.ts +8 -0
  47. package/dist/core/terminationNotice.js +10 -4
  48. package/dist/core/terminationNotice.js.map +1 -1
  49. package/dist/core/terminationReason.d.ts +28 -0
  50. package/dist/core/terminationReason.js +27 -0
  51. package/dist/core/terminationReason.js.map +1 -1
  52. package/dist/core/types.d.ts +35 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/history/checkpointRetention.d.ts +279 -0
  55. package/dist/history/checkpointRetention.js +567 -0
  56. package/dist/history/checkpointRetention.js.map +1 -0
  57. package/dist/history/checkpointSaver.d.ts +93 -0
  58. package/dist/history/checkpointSaver.js +464 -0
  59. package/dist/history/checkpointSaver.js.map +1 -0
  60. package/dist/history/historyEnabled.d.ts +27 -0
  61. package/dist/history/historyEnabled.js +23 -0
  62. package/dist/history/historyEnabled.js.map +1 -0
  63. package/dist/history/historyFormat.d.ts +27 -0
  64. package/dist/history/historyFormat.js +125 -2
  65. package/dist/history/historyFormat.js.map +1 -1
  66. package/dist/history/historyStore.d.ts +61 -0
  67. package/dist/history/historyStore.js +180 -7
  68. package/dist/history/historyStore.js.map +1 -1
  69. package/dist/history/recordSession.d.ts +84 -22
  70. package/dist/history/recordSession.js +187 -12
  71. package/dist/history/recordSession.js.map +1 -1
  72. package/dist/history/sessionCheckpointer.d.ts +48 -0
  73. package/dist/history/sessionCheckpointer.js +200 -0
  74. package/dist/history/sessionCheckpointer.js.map +1 -0
  75. package/dist/index.d.ts +6 -0
  76. package/dist/index.js +8 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/providers/modelCatalog.d.ts +14 -0
  79. package/dist/providers/modelCatalog.js +4 -0
  80. package/dist/providers/modelCatalog.js.map +1 -1
  81. package/dist/providers/modelDiscovery.d.ts +3 -1
  82. package/dist/providers/modelDiscovery.js +22 -8
  83. package/dist/providers/modelDiscovery.js.map +1 -1
  84. package/dist/providers/ollama.js +3 -19
  85. package/dist/providers/ollama.js.map +1 -1
  86. package/dist/runtime/conversation.js +7 -1
  87. package/dist/runtime/conversation.js.map +1 -1
  88. package/dist/runtime/singleShot.js +6 -1
  89. package/dist/runtime/singleShot.js.map +1 -1
  90. package/dist/utils/consoleUtils.d.ts +77 -0
  91. package/dist/utils/consoleUtils.js +81 -0
  92. package/dist/utils/consoleUtils.js.map +1 -1
  93. package/package.json +2 -2
  94. package/schema/gsloth-config.schema.json +33 -14
@@ -0,0 +1,93 @@
1
+ import { BaseCheckpointSaver } from '@langchain/langgraph';
2
+ import type { Checkpoint, CheckpointMetadata, CheckpointTuple } from '@langchain/langgraph';
3
+ import type { RunnableConfig } from '@langchain/core/runnables';
4
+ import { type CheckpointStoreStats, type ReclaimSummary } from '#src/history/checkpointRetention.js';
5
+ /**
6
+ * The abstract members' own parameter types, read off the base class rather than imported.
7
+ * `@langchain/langgraph` re-exports `BaseCheckpointSaver` (and `Checkpoint`, `CheckpointMetadata`,
8
+ * `CheckpointTuple`) but not `ChannelVersions`, `CheckpointListOptions` or `PendingWrite`, which
9
+ * live only in `@langchain/langgraph-checkpoint` — a package this one does not depend on. Deriving
10
+ * them keeps the signatures exactly the base class's without adding a dependency whose second
11
+ * physical copy could disagree with the one LangGraph itself loads. `put`'s fourth parameter is
12
+ * derived inline at its signature for the same reason, kept out of this list so the alias does not
13
+ * surface as an undocumented type in the rendered API reference.
14
+ */
15
+ type ListOptions = NonNullable<Parameters<BaseCheckpointSaver['list']>[1]>;
16
+ type PendingWrites = Parameters<BaseCheckpointSaver['putWrites']>[1];
17
+ /** Options for {@link openCheckpointSaver}. */
18
+ export interface CheckpointSaverOptions {
19
+ /**
20
+ * Called when a `put` / `putWrites` could not reach the database. The write is dropped and the
21
+ * turn continues; the handler's job is to tell the user and to record that this conversation can
22
+ * no longer be resumed. Called on EVERY failed write — the once-per-session rule belongs to the
23
+ * handler, which is the layer that knows what a session is.
24
+ */
25
+ onWriteFailure?: (error: unknown) => void;
26
+ }
27
+ /**
28
+ * A LangGraph `BaseCheckpointSaver` persisting to a `node:sqlite` database.
29
+ *
30
+ * Obtain one through {@link openCheckpointSaver}, which is fail-soft; the constructor is private so
31
+ * there is no way to hold one whose tables were never created.
32
+ */
33
+ export declare class GthSqliteSaver extends BaseCheckpointSaver {
34
+ private db;
35
+ private onWriteFailure;
36
+ /**
37
+ * GS2-107 — every thread this saver has written to, which is what automatic reclamation excludes.
38
+ *
39
+ * The thread a session writes to is not fixed at open: `resetThread()` mints a fresh one on
40
+ * `/clear` and `resumeConversation` rebinds onto a stored one, and neither tells this object. An
41
+ * exclusion built from the id the session started with therefore names a thread nobody is writing
42
+ * and misses the one that is — which on the `/clear` path is a thread no conversation row names,
43
+ * i.e. exactly a reclamation candidate. Recording the ids as they arrive needs no notification at
44
+ * all: whatever the runner rotated onto, the write came through here.
45
+ */
46
+ private readonly writtenThreads;
47
+ private constructor();
48
+ /**
49
+ * Report a dropped write, and never let the reporting itself become the failure. A handler that
50
+ * throws here would land back inside `agent.invoke()` and end the turn — the exact outcome the
51
+ * degrade posture exists to avoid, arriving from the code that implements it.
52
+ */
53
+ private reportWriteFailure;
54
+ /**
55
+ * Open (and lazily initialise) the checkpoint tables at `dbPath`. Returns `null` on any failure —
56
+ * an unopenable, read-only or corrupt DB — so the caller can fall back to a `MemorySaver` without
57
+ * a try/catch of its own.
58
+ */
59
+ static open(dbPath: string, options?: CheckpointSaverOptions): GthSqliteSaver | null;
60
+ /** Close the underlying connection (fail-soft). */
61
+ close(): void;
62
+ /** Rebuild one stored row into the tuple LangGraph reads, pending writes and parent included. */
63
+ private toTuple;
64
+ getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;
65
+ list(config: RunnableConfig, options?: ListOptions): AsyncGenerator<CheckpointTuple>;
66
+ put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: Parameters<BaseCheckpointSaver['put']>[3]): Promise<RunnableConfig>;
67
+ putWrites(config: RunnableConfig, writes: PendingWrites, taskId: string): Promise<void>;
68
+ deleteThread(threadId: string): Promise<void>;
69
+ /**
70
+ * GS2-107 — delete every thread no conversation row names, past the grace window. The retention
71
+ * module owns the policy and the reasoning; this is the saver's own connection lent to it, so a
72
+ * session that already has the store open can reclaim without opening it again.
73
+ *
74
+ * **Every thread this saver wrote is excluded, always**, on top of whatever the caller names. A
75
+ * caller cannot supply that set: the runner rotates threads without telling anyone, so the only
76
+ * place that knows which thread the session ended on is the object the writes went through. The
77
+ * union is the contract — `excludeThreadIds` adds to the protection and can never subtract from
78
+ * it.
79
+ */
80
+ reclaimUnresumableThreads(options?: {
81
+ now?: number;
82
+ graceMs?: number;
83
+ excludeThreadIds?: readonly string[];
84
+ }): ReclaimSummary;
85
+ /** GS2-107 — what the checkpoint tables hold, over this saver's open connection. */
86
+ storeStats(dbPath: string, topN?: number): CheckpointStoreStats;
87
+ }
88
+ /**
89
+ * Fail-soft open of the durable checkpoint saver. Returns `null` (never throws) when the DB cannot
90
+ * be opened or its tables cannot be created.
91
+ */
92
+ export declare function openCheckpointSaver(dbPath: string, options?: CheckpointSaverOptions): GthSqliteSaver | null;
93
+ export {};
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-20 — a **durable LangGraph checkpointer** over the same local SQLite file the session
4
+ * history store uses, so a past conversation can be resumed with the graph state it actually had.
5
+ *
6
+ * Resuming by replaying stored messages into a fresh agent rebuilds a transcript and discards
7
+ * everything the LangGraph graph was holding — pending writes, a suspended interrupt, the channel
8
+ * versions that make `add_messages` reconcile rather than append. What a resume has to produce is
9
+ * that, from the agent's side, no interruption happened, and only a saver whose checkpoints outlive
10
+ * the process can produce it.
11
+ *
12
+ * **Written against the built-in `node:sqlite`, deliberately.** Gaunt Sloth is installed globally
13
+ * with `npm i -g`, and the history store (`historyStore.ts`) states the constraint this inherits:
14
+ * zero native dependency, no build step. The published LangGraph SQLite savers are built on
15
+ * `better-sqlite3`, a native module with a compile step, which would put a node-gyp build in the
16
+ * path of every install of the CLI.
17
+ *
18
+ * **Storage layout.** Two tables of this module's own — `checkpoints` and `checkpoint_writes` —
19
+ * created idempotently on open, in the same file as the store's `sessions` / `conversations`.
20
+ * One file, one opt-out, one thing to delete. The store owns its tables and this owns these; there
21
+ * is no shared DDL, so the store's per-call open (which re-runs its own migration every time)
22
+ * cannot race this module's long-lived connection over the same schema.
23
+ *
24
+ * **Failure posture: degrade, loudly.** {@link openCheckpointSaver} is fail-soft — it returns `null`
25
+ * rather than throwing when the DB cannot be opened or the tables cannot be created, which is what
26
+ * lets the session fall back to a `MemorySaver` (see `openSessionCheckpointerSafe` in
27
+ * `sessionCheckpointer.ts`). A write that fails AFTER that — a full disk, a filesystem that went
28
+ * read-only under a live handle — is caught here and reported through
29
+ * {@link CheckpointSaverOptions.onWriteFailure} instead of propagating.
30
+ *
31
+ * Neither of the two obvious postures is right, and the reason is worth keeping. **Swallowing**
32
+ * would report a resumed session as empty and turn a suspended tool approval into a graph nobody
33
+ * can resume — silent and wrong. **Throwing** propagates out of `agent.invoke()` into the session's
34
+ * outer catch and ends the session, which since GS2-20 made history the default would mean a full
35
+ * disk takes down the live session of a user who never asked for the feature; recording a
36
+ * conversation must not become a new way to lose one. So the third posture: the write is dropped,
37
+ * the turn continues, the user is told once, and the conversation is marked **unresumable on disk**
38
+ * — that last part is what stops a silent drop from becoming a resume of a truncated conversation,
39
+ * and it is why this differs from the history RECORDER, which merely swallows. A missing turn in a
40
+ * listing is a gap; a half-restored graph presented as whole is a lie.
41
+ *
42
+ * Reads are still loud: nothing is degraded by a failed `getTuple`, and a resume that cannot read
43
+ * must not pretend the thread was empty.
44
+ */
45
+ import { DatabaseSync } from 'node:sqlite';
46
+ import { BaseCheckpointSaver, copyCheckpoint } from '@langchain/langgraph';
47
+ import { collectCheckpointStoreStats, deleteThreads, reclaimUnresumableThreads, } from '#src/history/checkpointRetention.js';
48
+ /**
49
+ * LangGraph's reserved channel names and the fixed NEGATIVE slot each takes in a task's writes.
50
+ *
51
+ * A saver's `putWrites` is insert-once for ordinary writes (a retried super-step must not duplicate
52
+ * them) but replace-on-write for these four, which is why they need slots that cannot collide with
53
+ * a positional index. `__interrupt__` and `__resume__` are the pair the tool-approval gate rides on,
54
+ * so getting this wrong is not academic: a stale `__interrupt__` write kept beside a fresh one is a
55
+ * suspended approval that resumes twice.
56
+ *
57
+ * Mirrors `WRITES_IDX_MAP` in `@langchain/langgraph-checkpoint`, which does not re-export through
58
+ * `@langchain/langgraph`. Copied rather than depended on — see the note on the derived types above.
59
+ * A reserved name added upstream and missed here degrades to a positional slot: that write becomes
60
+ * insert-once instead of replace, which is visible only on a resume, so re-check this list when the
61
+ * LangGraph major moves.
62
+ */
63
+ const WRITES_IDX_MAP = Object.freeze({
64
+ __error__: -1,
65
+ __scheduled__: -2,
66
+ __interrupt__: -3,
67
+ __resume__: -4,
68
+ });
69
+ /**
70
+ * How long a statement waits for another connection's lock before giving up. The history recorder
71
+ * opens the same file for a moment at the end of every turn, so two writers on one file is the
72
+ * ordinary case here, not the exceptional one; without this a routine overlap surfaces as a
73
+ * `SQLITE_BUSY` that aborts a turn.
74
+ */
75
+ const BUSY_TIMEOUT_MS = 5000;
76
+ /** The checkpoint id named by a config, using LangGraph's own precedence (`thread_ts` is legacy). */
77
+ function checkpointIdOf(config) {
78
+ const configurable = config.configurable;
79
+ const id = configurable?.checkpoint_id ?? configurable?.thread_ts;
80
+ return typeof id === 'string' ? id : '';
81
+ }
82
+ /** Read a `configurable` field as a string, or `undefined` when it is absent / not a string. */
83
+ function stringField(config, key) {
84
+ const value = config.configurable?.[key];
85
+ return typeof value === 'string' ? value : undefined;
86
+ }
87
+ /**
88
+ * Narrow one raw SQLite row to {@link CheckpointRow}. Written out rather than cast because
89
+ * `node:sqlite` types every column as `SQLOutputValue`, so a blanket assertion would also hide a
90
+ * genuine column/shape mismatch — including the BLOB columns, which are the ones a wrong value
91
+ * would break silently inside the deserializer rather than here.
92
+ */
93
+ function toCheckpointRow(row) {
94
+ return {
95
+ thread_id: String(row.thread_id),
96
+ checkpoint_ns: row.checkpoint_ns != null ? String(row.checkpoint_ns) : '',
97
+ checkpoint_id: String(row.checkpoint_id),
98
+ parent_checkpoint_id: row.parent_checkpoint_id != null ? String(row.parent_checkpoint_id) : null,
99
+ type: row.type != null ? String(row.type) : null,
100
+ checkpoint: row.checkpoint,
101
+ metadata: row.metadata,
102
+ };
103
+ }
104
+ /**
105
+ * A LangGraph `BaseCheckpointSaver` persisting to a `node:sqlite` database.
106
+ *
107
+ * Obtain one through {@link openCheckpointSaver}, which is fail-soft; the constructor is private so
108
+ * there is no way to hold one whose tables were never created.
109
+ */
110
+ export class GthSqliteSaver extends BaseCheckpointSaver {
111
+ db;
112
+ onWriteFailure;
113
+ /**
114
+ * GS2-107 — every thread this saver has written to, which is what automatic reclamation excludes.
115
+ *
116
+ * The thread a session writes to is not fixed at open: `resetThread()` mints a fresh one on
117
+ * `/clear` and `resumeConversation` rebinds onto a stored one, and neither tells this object. An
118
+ * exclusion built from the id the session started with therefore names a thread nobody is writing
119
+ * and misses the one that is — which on the `/clear` path is a thread no conversation row names,
120
+ * i.e. exactly a reclamation candidate. Recording the ids as they arrive needs no notification at
121
+ * all: whatever the runner rotated onto, the write came through here.
122
+ */
123
+ writtenThreads = new Set();
124
+ constructor(db, onWriteFailure) {
125
+ super();
126
+ this.db = db;
127
+ this.onWriteFailure = onWriteFailure ?? (() => { });
128
+ }
129
+ /**
130
+ * Report a dropped write, and never let the reporting itself become the failure. A handler that
131
+ * throws here would land back inside `agent.invoke()` and end the turn — the exact outcome the
132
+ * degrade posture exists to avoid, arriving from the code that implements it.
133
+ */
134
+ reportWriteFailure(error) {
135
+ try {
136
+ this.onWriteFailure(error);
137
+ }
138
+ catch {
139
+ /* ignore */
140
+ }
141
+ }
142
+ /**
143
+ * Open (and lazily initialise) the checkpoint tables at `dbPath`. Returns `null` on any failure —
144
+ * an unopenable, read-only or corrupt DB — so the caller can fall back to a `MemorySaver` without
145
+ * a try/catch of its own.
146
+ */
147
+ static open(dbPath, options = {}) {
148
+ let db;
149
+ try {
150
+ db = new DatabaseSync(dbPath);
151
+ // GS2-42's lesson applied here too: SQLite does not validate the file header until the first
152
+ // statement runs, so a garbage file opens cleanly and fails on the DDL below. Everything from
153
+ // the constructor to the last `exec` is therefore inside one try, and the handle is closed on
154
+ // the way out — on win32 an unclosed handle blocks the file from being replaced or reopened
155
+ // until the process exits.
156
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
157
+ db.exec(`
158
+ CREATE TABLE IF NOT EXISTS checkpoints (
159
+ thread_id TEXT NOT NULL,
160
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
161
+ checkpoint_id TEXT NOT NULL,
162
+ parent_checkpoint_id TEXT,
163
+ type TEXT,
164
+ checkpoint BLOB,
165
+ metadata BLOB,
166
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
167
+ );
168
+ CREATE TABLE IF NOT EXISTS checkpoint_writes (
169
+ thread_id TEXT NOT NULL,
170
+ checkpoint_ns TEXT NOT NULL DEFAULT '',
171
+ checkpoint_id TEXT NOT NULL,
172
+ task_id TEXT NOT NULL,
173
+ idx INTEGER NOT NULL,
174
+ channel TEXT NOT NULL,
175
+ type TEXT,
176
+ value BLOB,
177
+ PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
178
+ );
179
+ `);
180
+ return new GthSqliteSaver(db, options.onWriteFailure);
181
+ }
182
+ catch {
183
+ try {
184
+ db?.close();
185
+ }
186
+ catch {
187
+ /* ignore: the connection may already be in a broken state */
188
+ }
189
+ return null;
190
+ }
191
+ }
192
+ /** Close the underlying connection (fail-soft). */
193
+ close() {
194
+ try {
195
+ this.db.close();
196
+ }
197
+ catch {
198
+ /* ignore */
199
+ }
200
+ }
201
+ /** Rebuild one stored row into the tuple LangGraph reads, pending writes and parent included. */
202
+ async toTuple(row, config) {
203
+ const type = row.type ?? 'json';
204
+ const checkpoint = (await this.serde.loadsTyped(type, row.checkpoint));
205
+ const metadata = (await this.serde.loadsTyped(type, row.metadata));
206
+ const writeRows = this.db
207
+ .prepare(`SELECT task_id, channel, type, value
208
+ FROM checkpoint_writes
209
+ WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?
210
+ ORDER BY task_id ASC, idx ASC`)
211
+ .all(row.thread_id, row.checkpoint_ns, row.checkpoint_id);
212
+ const pendingWrites = [];
213
+ for (const w of writeRows) {
214
+ pendingWrites.push([
215
+ String(w.task_id),
216
+ String(w.channel),
217
+ await this.serde.loadsTyped(String(w.type ?? 'json'), w.value),
218
+ ]);
219
+ }
220
+ const tuple = { config, checkpoint, metadata, pendingWrites };
221
+ // The parent link is part of the interface, not optional decoration:
222
+ // `BaseCheckpointSaver.getDeltaChannelHistory` reconstructs a DELTA channel by walking
223
+ // `getTuple` → `parentConfig` up the ancestor chain, and a tuple without it terminates that walk
224
+ // at the first checkpoint.
225
+ //
226
+ // **What that costs depends on the state schema, and on THIS graph it costs less than it looks.**
227
+ // The agent's `messages` channel stores a full array in `channel_values`, not a delta, so every
228
+ // checkpoint already carries the whole transcript and a broken walk cannot produce the
229
+ // "transcript present, tool result missing" shape. Dropping the link here reddens the
230
+ // parent-link test and nothing else — measured, not assumed. The reason to keep returning it is
231
+ // the conditional one: a state schema that puts any channel behind a binary operator (a reducer
232
+ // accumulating deltas, which `add_messages` is NOT under the current serialization) would
233
+ // reconstruct that channel from the ancestor chain, and then a missing parent silently seeds it
234
+ // empty. Correct now, and load-bearing the moment the schema changes.
235
+ if (row.parent_checkpoint_id != null) {
236
+ tuple.parentConfig = {
237
+ configurable: {
238
+ thread_id: row.thread_id,
239
+ checkpoint_ns: row.checkpoint_ns,
240
+ checkpoint_id: row.parent_checkpoint_id,
241
+ },
242
+ };
243
+ }
244
+ return tuple;
245
+ }
246
+ async getTuple(config) {
247
+ const threadId = stringField(config, 'thread_id');
248
+ if (threadId === undefined)
249
+ return undefined;
250
+ const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
251
+ const checkpointId = checkpointIdOf(config);
252
+ // Checkpoint ids are uuid6, which sort lexicographically in creation order, so "the latest
253
+ // checkpoint on this thread" is a plain DESC on the id — the same ordering MemorySaver gets
254
+ // from sorting its keys.
255
+ const raw = (checkpointId
256
+ ? this.db
257
+ .prepare(`SELECT * FROM checkpoints
258
+ WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?`)
259
+ .get(threadId, checkpointNs, checkpointId)
260
+ : this.db
261
+ .prepare(`SELECT * FROM checkpoints
262
+ WHERE thread_id = ? AND checkpoint_ns = ?
263
+ ORDER BY checkpoint_id DESC
264
+ LIMIT 1`)
265
+ .get(threadId, checkpointNs));
266
+ if (raw === undefined)
267
+ return undefined;
268
+ const row = toCheckpointRow(raw);
269
+ // When the caller named a checkpoint, echo their config back (they may carry other
270
+ // `configurable` fields); when they asked for the latest, name the one we actually found.
271
+ const tupleConfig = checkpointId
272
+ ? config
273
+ : {
274
+ configurable: {
275
+ thread_id: threadId,
276
+ checkpoint_ns: checkpointNs,
277
+ checkpoint_id: row.checkpoint_id,
278
+ },
279
+ };
280
+ return this.toTuple(row, tupleConfig);
281
+ }
282
+ async *list(config, options) {
283
+ const { before, limit, filter } = options ?? {};
284
+ const clauses = [];
285
+ const params = [];
286
+ const threadId = stringField(config, 'thread_id');
287
+ if (threadId !== undefined) {
288
+ clauses.push('thread_id = ?');
289
+ params.push(threadId);
290
+ }
291
+ const checkpointNs = stringField(config, 'checkpoint_ns');
292
+ if (checkpointNs !== undefined) {
293
+ clauses.push('checkpoint_ns = ?');
294
+ params.push(checkpointNs);
295
+ }
296
+ const checkpointId = checkpointIdOf(config);
297
+ if (checkpointId) {
298
+ clauses.push('checkpoint_id = ?');
299
+ params.push(checkpointId);
300
+ }
301
+ const beforeId = before ? checkpointIdOf(before) : '';
302
+ if (beforeId) {
303
+ clauses.push('checkpoint_id < ?');
304
+ params.push(beforeId);
305
+ }
306
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
307
+ const rows = this.db
308
+ .prepare(`SELECT * FROM checkpoints
309
+ ${where}
310
+ ORDER BY thread_id ASC, checkpoint_ns ASC, checkpoint_id DESC`)
311
+ .all(...params).map(toCheckpointRow);
312
+ // `limit` counts rows that SURVIVE `filter`, so it is applied here rather than as SQL LIMIT —
313
+ // a metadata filter is evaluated on the deserialized object, which SQL cannot see.
314
+ let remaining = limit;
315
+ for (const row of rows) {
316
+ if (remaining !== undefined && remaining <= 0)
317
+ return;
318
+ const tuple = await this.toTuple(row, {
319
+ configurable: {
320
+ thread_id: row.thread_id,
321
+ checkpoint_ns: row.checkpoint_ns,
322
+ checkpoint_id: row.checkpoint_id,
323
+ },
324
+ });
325
+ if (filter) {
326
+ const metadata = (tuple.metadata ?? {});
327
+ if (!Object.entries(filter).every(([key, value]) => metadata[key] === value))
328
+ continue;
329
+ }
330
+ if (remaining !== undefined)
331
+ remaining -= 1;
332
+ yield tuple;
333
+ }
334
+ }
335
+ async put(config, checkpoint, metadata, _newVersions) {
336
+ const threadId = stringField(config, 'thread_id');
337
+ if (threadId === undefined) {
338
+ throw new Error('Failed to put checkpoint: the RunnableConfig is missing a "thread_id" in its ' +
339
+ '"configurable" property. A checkpointer needs a thread_id to know which conversation ' +
340
+ 'to persist state for.');
341
+ }
342
+ // Recorded BEFORE the write is attempted, and kept even when it fails. A thread whose write was
343
+ // dropped is one the degrade path has just marked unresumable, and a live session is still
344
+ // sitting on it; excluding it costs a delete that the next process's pass will make anyway.
345
+ this.writtenThreads.add(threadId);
346
+ const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
347
+ try {
348
+ const [checkpointType, serializedCheckpoint] = await this.serde.dumpsTyped(copyCheckpoint(checkpoint));
349
+ const [metadataType, serializedMetadata] = await this.serde.dumpsTyped(metadata);
350
+ if (checkpointType !== metadataType) {
351
+ throw new Error('Failed to serialize the checkpoint and its metadata to the same type.');
352
+ }
353
+ this.db
354
+ .prepare(`INSERT OR REPLACE INTO checkpoints
355
+ (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata)
356
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
357
+ .run(threadId, checkpointNs, checkpoint.id, checkpointIdOf(config) || null, checkpointType, serializedCheckpoint, serializedMetadata);
358
+ }
359
+ catch (error) {
360
+ this.reportWriteFailure(error);
361
+ }
362
+ // Returned whether or not the row landed, and that is not an oversight. LangGraph takes this
363
+ // config as the parent of the NEXT super-step, so a failure here leaves the next successful
364
+ // write pointing at a `parent_checkpoint_id` that was never stored — a chain with a hole in it.
365
+ // What makes that safe is the handler above, which marks the conversation unresumable on disk:
366
+ // nothing will ever walk this chain again. Returning a config that names a checkpoint we did
367
+ // not write is the lesser evil against throwing, which ends the user's session.
368
+ return {
369
+ configurable: {
370
+ thread_id: threadId,
371
+ checkpoint_ns: checkpointNs,
372
+ checkpoint_id: checkpoint.id,
373
+ },
374
+ };
375
+ }
376
+ async putWrites(config, writes, taskId) {
377
+ const threadId = stringField(config, 'thread_id');
378
+ if (threadId === undefined) {
379
+ throw new Error('Failed to put writes: the RunnableConfig is missing a "thread_id" in its ' +
380
+ '"configurable" property.');
381
+ }
382
+ const checkpointId = checkpointIdOf(config);
383
+ if (!checkpointId) {
384
+ throw new Error('Failed to put writes: the RunnableConfig is missing a "checkpoint_id" in its ' +
385
+ '"configurable" property.');
386
+ }
387
+ this.writtenThreads.add(threadId);
388
+ const checkpointNs = stringField(config, 'checkpoint_ns') ?? '';
389
+ try {
390
+ // Two statements, chosen per write by the sign of its slot, because the two halves of the
391
+ // contract differ: an ordinary write (idx >= 0) is insert-ONCE, so a retried super-step
392
+ // writing the same slot again must leave the first value alone, while a reserved channel
393
+ // ({@link WRITES_IDX_MAP}) is replace-on-write, so the newest error / interrupt / resume wins.
394
+ // One blanket `INSERT OR REPLACE` would get the ordinary case wrong and one blanket
395
+ // `INSERT OR IGNORE` the reserved one, and either divergence is visible only on a resume.
396
+ const insertOnce = this.db.prepare(`INSERT OR IGNORE INTO checkpoint_writes
397
+ (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value)
398
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
399
+ const replace = this.db.prepare(`INSERT OR REPLACE INTO checkpoint_writes
400
+ (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value)
401
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
402
+ // **Not wrapped in a transaction, deliberately.** A failure partway through this loop leaves
403
+ // the earlier writes of the same task committed, which LangGraph tolerates: `putWrites` is
404
+ // insert-once per (task, idx), so the retry that follows re-writes the same slots and the
405
+ // survivors are ignored rather than duplicated. A transaction would buy atomicity across the
406
+ // task's slots at the cost of holding a write lock across `dumpsTyped` — serialization, inside
407
+ // the lock, on a file the history recorder also opens every turn. That is the trade to revisit
408
+ // if a reserved-channel write ever has to land atomically with an ordinary one; today none
409
+ // does. Under the degrade posture a torn task is doubly harmless: the conversation is marked
410
+ // unresumable, so nothing will read these rows again.
411
+ for (let position = 0; position < writes.length; position++) {
412
+ const [channel, value] = writes[position];
413
+ // `Object.hasOwn`, not a plain index, because a channel name is arbitrary text arriving
414
+ // from the graph: `WRITES_IDX_MAP['constructor']` resolves up the PROTOTYPE CHAIN to
415
+ // `Object`, and `toString` / `valueOf` likewise, so a plain read would hand `idx` a function
416
+ // and take the reserved-vs-positional branch on it. `Object.freeze` does not help — it seals
417
+ // the own properties and leaves the prototype reachable. Upstream's `WRITES_IDX_MAP` has the
418
+ // same shape; that is a reason to keep the mirror, not to keep the defect.
419
+ const reserved = Object.hasOwn(WRITES_IDX_MAP, channel)
420
+ ? WRITES_IDX_MAP[channel]
421
+ : undefined;
422
+ const idx = reserved ?? position;
423
+ const [type, serialized] = await this.serde.dumpsTyped(value);
424
+ const statement = idx < 0 ? replace : insertOnce;
425
+ statement.run(threadId, checkpointNs, checkpointId, taskId, idx, channel, type, serialized);
426
+ }
427
+ }
428
+ catch (error) {
429
+ this.reportWriteFailure(error);
430
+ }
431
+ }
432
+ async deleteThread(threadId) {
433
+ deleteThreads(this.db, [threadId]);
434
+ }
435
+ /**
436
+ * GS2-107 — delete every thread no conversation row names, past the grace window. The retention
437
+ * module owns the policy and the reasoning; this is the saver's own connection lent to it, so a
438
+ * session that already has the store open can reclaim without opening it again.
439
+ *
440
+ * **Every thread this saver wrote is excluded, always**, on top of whatever the caller names. A
441
+ * caller cannot supply that set: the runner rotates threads without telling anyone, so the only
442
+ * place that knows which thread the session ended on is the object the writes went through. The
443
+ * union is the contract — `excludeThreadIds` adds to the protection and can never subtract from
444
+ * it.
445
+ */
446
+ reclaimUnresumableThreads(options = {}) {
447
+ return reclaimUnresumableThreads(this.db, {
448
+ ...options,
449
+ excludeThreadIds: [...this.writtenThreads, ...(options.excludeThreadIds ?? [])],
450
+ });
451
+ }
452
+ /** GS2-107 — what the checkpoint tables hold, over this saver's open connection. */
453
+ storeStats(dbPath, topN) {
454
+ return collectCheckpointStoreStats(this.db, dbPath, topN);
455
+ }
456
+ }
457
+ /**
458
+ * Fail-soft open of the durable checkpoint saver. Returns `null` (never throws) when the DB cannot
459
+ * be opened or its tables cannot be created.
460
+ */
461
+ export function openCheckpointSaver(dbPath, options = {}) {
462
+ return GthSqliteSaver.open(dbPath, options);
463
+ }
464
+ //# sourceMappingURL=checkpointSaver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpointSaver.js","sourceRoot":"","sources":["../../src/history/checkpointSaver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-7 (B20) / GS2-20 — the ONE switch governing everything the local history feature persists:
4
+ * the per-turn session records and the durable LangGraph checkpoints a resume reads back.
5
+ *
6
+ * They share a switch rather than getting one each because they are two halves of one promise. A
7
+ * conversation listed by `gth history list` that cannot be resumed, or a resumable thread with no
8
+ * listing to find it from, is a state a user has no way to reason about — and a second key would
9
+ * make both reachable.
10
+ */
11
+ /** The subset of the resolved config history reads (structural, to avoid a hard type dep). */
12
+ export interface HistoryConfigView {
13
+ history?: {
14
+ enabled?: boolean;
15
+ dbPath?: string;
16
+ };
17
+ }
18
+ /**
19
+ * Whether this run persists history. **Absent means ON**: a default run records its turns to the
20
+ * local store and checkpoints its graph state, both under the user's own `~/.gsloth` dir and
21
+ * neither leaving the machine.
22
+ *
23
+ * `history.enabled: false` is the opt-out and is the only value that turns it off, which is why
24
+ * this tests against `false` rather than for truthiness — the difference between "absent" and
25
+ * "explicitly off" is the whole behaviour.
26
+ */
27
+ export declare function isHistoryEnabled(config: HistoryConfigView | undefined): boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-7 (B20) / GS2-20 — the ONE switch governing everything the local history feature persists:
4
+ * the per-turn session records and the durable LangGraph checkpoints a resume reads back.
5
+ *
6
+ * They share a switch rather than getting one each because they are two halves of one promise. A
7
+ * conversation listed by `gth history list` that cannot be resumed, or a resumable thread with no
8
+ * listing to find it from, is a state a user has no way to reason about — and a second key would
9
+ * make both reachable.
10
+ */
11
+ /**
12
+ * Whether this run persists history. **Absent means ON**: a default run records its turns to the
13
+ * local store and checkpoints its graph state, both under the user's own `~/.gsloth` dir and
14
+ * neither leaving the machine.
15
+ *
16
+ * `history.enabled: false` is the opt-out and is the only value that turns it off, which is why
17
+ * this tests against `false` rather than for truthiness — the difference between "absent" and
18
+ * "explicitly off" is the whole behaviour.
19
+ */
20
+ export function isHistoryEnabled(config) {
21
+ return config?.history?.enabled !== false;
22
+ }
23
+ //# sourceMappingURL=historyEnabled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"historyEnabled.js","sourceRoot":"","sources":["../../src/history/historyEnabled.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAqC;IACpE,OAAO,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC;AAC5C,CAAC"}