@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
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @packageDocumentation
|
|
3
|
-
* GS2-7 (B20) — the
|
|
3
|
+
* GS2-7 (B20) — the bridge from a finished run to the {@link HistoryStore}.
|
|
4
4
|
*
|
|
5
|
-
* {@link recordSessionSafe} is the ONE entry point the run path calls. It
|
|
6
|
-
* `history.enabled` is
|
|
7
|
-
* - a **default run**
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
5
|
+
* {@link recordSessionSafe} is the ONE entry point the run path calls. It writes unless
|
|
6
|
+
* `history.enabled` is `false`, and it swallows every error, so:
|
|
7
|
+
* - a **default run** records its turns locally, under the user's own `~/.gsloth` dir, and
|
|
8
|
+
* `history.enabled: false` is the opt-out that restores the stateless identity; and
|
|
9
|
+
* - with history on, a DB problem (locked/corrupt/read-only fs) can never abort or alter
|
|
10
10
|
* the run: the worst case is that one session isn't recorded.
|
|
11
11
|
*/
|
|
12
|
-
import type { ConversationMeta, SessionRecord } from '#src/history/historyStore.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
history?: {
|
|
16
|
-
enabled?: boolean;
|
|
17
|
-
dbPath?: string;
|
|
18
|
-
};
|
|
19
|
-
}
|
|
12
|
+
import type { ConversationMeta, ConversationSummary, SessionRecord } from '#src/history/historyStore.js';
|
|
13
|
+
export type { HistoryConfigView } from '#src/history/historyEnabled.js';
|
|
14
|
+
import type { HistoryConfigView } from '#src/history/historyEnabled.js';
|
|
20
15
|
/**
|
|
21
|
-
* Record one finished session
|
|
22
|
-
* when history is
|
|
16
|
+
* Record one finished session unless `history.enabled` is `false`. Returns the new row id, or
|
|
17
|
+
* `null` when history is turned off or anything went wrong (both are non-events for the caller).
|
|
23
18
|
*
|
|
24
19
|
* Deliberately fully guarded: opening the store, ensuring the global dir, and the insert all run
|
|
25
20
|
* behind a single try/catch, and the store itself is fail-soft. Callers put this in a `finally`
|
|
@@ -27,11 +22,78 @@ export interface HistoryConfigView {
|
|
|
27
22
|
*/
|
|
28
23
|
export declare function recordSessionSafe(config: HistoryConfigView, record: SessionRecord): number | null;
|
|
29
24
|
/**
|
|
30
|
-
* GS2-19 — open one conversation for an interactive session
|
|
31
|
-
* (or `null` when
|
|
32
|
-
* {@link recordSessionSafe} so all its turns group under one conversation. Same guarantees as
|
|
33
|
-
* recorder:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
25
|
+
* GS2-19 — open one conversation for an interactive session unless `history.enabled` is `false`,
|
|
26
|
+
* returning its id (or `null` when turned off / anything failed). The session passes that id on
|
|
27
|
+
* every {@link recordSessionSafe} so all its turns group under one conversation. Same guarantees as
|
|
28
|
+
* the recorder: fully fail-soft, never throws. When it returns `null` under an enabled store (a
|
|
29
|
+
* rare open failure), turns simply fall back to per-turn 1-turn conversations — grouping is lost
|
|
30
|
+
* but nothing is dropped or crashed.
|
|
31
|
+
*
|
|
32
|
+
* GS2-20 — `meta.threadId` is what makes the conversation resumable: it names the LangGraph thread
|
|
33
|
+
* whose durable checkpoint holds the session's graph state.
|
|
36
34
|
*/
|
|
37
35
|
export declare function openConversationSafe(config: HistoryConfigView, meta: ConversationMeta): number | null;
|
|
36
|
+
/**
|
|
37
|
+
* GS2-20 — record, ON DISK, that a conversation can no longer be resumed, by clearing its thread
|
|
38
|
+
* link. Called once per session, the first time a checkpoint write fails after the store opened.
|
|
39
|
+
*
|
|
40
|
+
* The session itself keeps running and the user is told; this is the half that has to outlive the
|
|
41
|
+
* process. A checkpoint chain that stopped growing mid-conversation is still a well-formed chain,
|
|
42
|
+
* so nothing a later `--resume` could inspect would reveal that it is truncated — the refusal has
|
|
43
|
+
* to be written down at the moment the writing broke.
|
|
44
|
+
*
|
|
45
|
+
* Fail-soft, like its neighbours: the session is already degraded, and a failure here must not turn
|
|
46
|
+
* that into a second error.
|
|
47
|
+
*/
|
|
48
|
+
export declare function markConversationUnresumableSafe(config: HistoryConfigView, conversationId: number): void;
|
|
49
|
+
/**
|
|
50
|
+
* GS2-20 — the reverse trip: from a conversation id (what `gth history list` prints and a user
|
|
51
|
+
* types) to the LangGraph thread whose checkpoint holds its state, or `null` when there isn't one.
|
|
52
|
+
*
|
|
53
|
+
* `null` covers every way this can come up empty — history turned off, no store, an id that names
|
|
54
|
+
* nothing, or a conversation recorded without a checkpointer — and never a neighbouring
|
|
55
|
+
* conversation. A caller resuming on this must treat `null` as a refusal to resume and say so;
|
|
56
|
+
* quietly starting a fresh session, or picking the most recent conversation instead, would hand
|
|
57
|
+
* someone another conversation's state under the id they typed.
|
|
58
|
+
*/
|
|
59
|
+
export declare function lookupConversationThreadSafe(config: HistoryConfigView, conversationId: number): string | null;
|
|
60
|
+
/** GS2-20 — what a resume needs to know about a stored conversation before re-entering it. */
|
|
61
|
+
export interface StoredConversation {
|
|
62
|
+
/** The listing row: when it started, which command and model recorded it, its workspace. */
|
|
63
|
+
summary: ConversationSummary;
|
|
64
|
+
/** Its recorded turns, oldest first — what the surface replays as restored turns. */
|
|
65
|
+
turns: SessionRecord[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* GS2-20 — the conversation row and its recorded turns, or `null` when history is off, the store
|
|
69
|
+
* cannot be opened, or no conversation has that id.
|
|
70
|
+
*
|
|
71
|
+
* This is the *display and policy* half of a resume — the workspace the conversation belongs to,
|
|
72
|
+
* the command it was recorded under, the turns to show — and deliberately not the thread decision:
|
|
73
|
+
* whether the conversation can be re-entered is answered by {@link lookupConversationThreadSafe},
|
|
74
|
+
* whose exact-match-never-fallback contract is the one a resume rests on. Fail-soft, never throws.
|
|
75
|
+
*/
|
|
76
|
+
export declare function lookupConversationSafe(config: HistoryConfigView, conversationId: number): StoredConversation | null;
|
|
77
|
+
/**
|
|
78
|
+
* GS2-20 — the conversations a `/resume` picker may offer: the most recent ones that carry a thread
|
|
79
|
+
* (so a resume could actually re-enter them), minus the one the session is already in. `[]` when
|
|
80
|
+
* history is off or the store cannot be opened, which the caller renders as "nothing to resume".
|
|
81
|
+
* Fail-soft, never throws.
|
|
82
|
+
*/
|
|
83
|
+
export declare function listResumableConversationsSafe(config: HistoryConfigView, options?: {
|
|
84
|
+
limit?: number;
|
|
85
|
+
exclude?: number;
|
|
86
|
+
}): ConversationSummary[];
|
|
87
|
+
/**
|
|
88
|
+
* GS2-20 — the stored approval-grants document of one conversation, as the opaque JSON string the
|
|
89
|
+
* approvals layer wrote (`core/approvals/conversationGrants.ts` owns the format), or `null` when
|
|
90
|
+
* there is none. Governed by the same switch as everything else here. Fail-soft, never throws.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readConversationGrantsSafe(config: HistoryConfigView, conversationId: number): string | null;
|
|
93
|
+
/**
|
|
94
|
+
* GS2-20 — replace one conversation's stored approval-grants document. Called whenever the session's
|
|
95
|
+
* grants change, so the row always holds the current set and a resume restores exactly what the
|
|
96
|
+
* conversation had. Returns whether the row was written. Fail-soft, never throws — a grant that
|
|
97
|
+
* could not be recorded costs one re-prompt after a resume, and must never cost the turn.
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeConversationGrantsSafe(config: HistoryConfigView, conversationId: number, grantsJson: string | null): boolean;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { openHistoryStore, resolveHistoryDbPath } from '#src/history/historyStore.js';
|
|
2
|
+
import { isHistoryEnabled } from '#src/history/historyEnabled.js';
|
|
2
3
|
/**
|
|
3
|
-
* Record one finished session
|
|
4
|
-
* when history is
|
|
4
|
+
* Record one finished session unless `history.enabled` is `false`. Returns the new row id, or
|
|
5
|
+
* `null` when history is turned off or anything went wrong (both are non-events for the caller).
|
|
5
6
|
*
|
|
6
7
|
* Deliberately fully guarded: opening the store, ensuring the global dir, and the insert all run
|
|
7
8
|
* behind a single try/catch, and the store itself is fail-soft. Callers put this in a `finally`
|
|
@@ -9,9 +10,9 @@ import { openHistoryStore, resolveHistoryDbPath } from '#src/history/historyStor
|
|
|
9
10
|
*/
|
|
10
11
|
export function recordSessionSafe(config, record) {
|
|
11
12
|
try {
|
|
12
|
-
if (!config
|
|
13
|
+
if (!isHistoryEnabled(config))
|
|
13
14
|
return null;
|
|
14
|
-
const dbPath = resolveHistoryDbPath(config.history
|
|
15
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath, /* ensureDir */ true);
|
|
15
16
|
const store = openHistoryStore(dbPath, { create: true });
|
|
16
17
|
if (!store)
|
|
17
18
|
return null;
|
|
@@ -27,18 +28,21 @@ export function recordSessionSafe(config, record) {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
|
-
* GS2-19 — open one conversation for an interactive session
|
|
31
|
-
* (or `null` when
|
|
32
|
-
* {@link recordSessionSafe} so all its turns group under one conversation. Same guarantees as
|
|
33
|
-
* recorder:
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* GS2-19 — open one conversation for an interactive session unless `history.enabled` is `false`,
|
|
32
|
+
* returning its id (or `null` when turned off / anything failed). The session passes that id on
|
|
33
|
+
* every {@link recordSessionSafe} so all its turns group under one conversation. Same guarantees as
|
|
34
|
+
* the recorder: fully fail-soft, never throws. When it returns `null` under an enabled store (a
|
|
35
|
+
* rare open failure), turns simply fall back to per-turn 1-turn conversations — grouping is lost
|
|
36
|
+
* but nothing is dropped or crashed.
|
|
37
|
+
*
|
|
38
|
+
* GS2-20 — `meta.threadId` is what makes the conversation resumable: it names the LangGraph thread
|
|
39
|
+
* whose durable checkpoint holds the session's graph state.
|
|
36
40
|
*/
|
|
37
41
|
export function openConversationSafe(config, meta) {
|
|
38
42
|
try {
|
|
39
|
-
if (!config
|
|
43
|
+
if (!isHistoryEnabled(config))
|
|
40
44
|
return null;
|
|
41
|
-
const dbPath = resolveHistoryDbPath(config.history
|
|
45
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath, /* ensureDir */ true);
|
|
42
46
|
const store = openHistoryStore(dbPath, { create: true });
|
|
43
47
|
if (!store)
|
|
44
48
|
return null;
|
|
@@ -53,4 +57,175 @@ export function openConversationSafe(config, meta) {
|
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* GS2-20 — record, ON DISK, that a conversation can no longer be resumed, by clearing its thread
|
|
62
|
+
* link. Called once per session, the first time a checkpoint write fails after the store opened.
|
|
63
|
+
*
|
|
64
|
+
* The session itself keeps running and the user is told; this is the half that has to outlive the
|
|
65
|
+
* process. A checkpoint chain that stopped growing mid-conversation is still a well-formed chain,
|
|
66
|
+
* so nothing a later `--resume` could inspect would reveal that it is truncated — the refusal has
|
|
67
|
+
* to be written down at the moment the writing broke.
|
|
68
|
+
*
|
|
69
|
+
* Fail-soft, like its neighbours: the session is already degraded, and a failure here must not turn
|
|
70
|
+
* that into a second error.
|
|
71
|
+
*/
|
|
72
|
+
export function markConversationUnresumableSafe(config, conversationId) {
|
|
73
|
+
try {
|
|
74
|
+
if (!isHistoryEnabled(config))
|
|
75
|
+
return;
|
|
76
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
77
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
78
|
+
if (!store)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
store.clearConversationThread(conversationId);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
store.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* GS2-20 — the reverse trip: from a conversation id (what `gth history list` prints and a user
|
|
93
|
+
* types) to the LangGraph thread whose checkpoint holds its state, or `null` when there isn't one.
|
|
94
|
+
*
|
|
95
|
+
* `null` covers every way this can come up empty — history turned off, no store, an id that names
|
|
96
|
+
* nothing, or a conversation recorded without a checkpointer — and never a neighbouring
|
|
97
|
+
* conversation. A caller resuming on this must treat `null` as a refusal to resume and say so;
|
|
98
|
+
* quietly starting a fresh session, or picking the most recent conversation instead, would hand
|
|
99
|
+
* someone another conversation's state under the id they typed.
|
|
100
|
+
*/
|
|
101
|
+
export function lookupConversationThreadSafe(config, conversationId) {
|
|
102
|
+
try {
|
|
103
|
+
if (!isHistoryEnabled(config))
|
|
104
|
+
return null;
|
|
105
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
106
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
107
|
+
if (!store)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
return store.getConversationThreadId(conversationId);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
store.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* GS2-20 — the conversation row and its recorded turns, or `null` when history is off, the store
|
|
122
|
+
* cannot be opened, or no conversation has that id.
|
|
123
|
+
*
|
|
124
|
+
* This is the *display and policy* half of a resume — the workspace the conversation belongs to,
|
|
125
|
+
* the command it was recorded under, the turns to show — and deliberately not the thread decision:
|
|
126
|
+
* whether the conversation can be re-entered is answered by {@link lookupConversationThreadSafe},
|
|
127
|
+
* whose exact-match-never-fallback contract is the one a resume rests on. Fail-soft, never throws.
|
|
128
|
+
*/
|
|
129
|
+
export function lookupConversationSafe(config, conversationId) {
|
|
130
|
+
try {
|
|
131
|
+
if (!isHistoryEnabled(config))
|
|
132
|
+
return null;
|
|
133
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
134
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
135
|
+
if (!store)
|
|
136
|
+
return null;
|
|
137
|
+
try {
|
|
138
|
+
const summary = store.getConversation(conversationId);
|
|
139
|
+
if (!summary)
|
|
140
|
+
return null;
|
|
141
|
+
return { summary, turns: store.getConversationThread(conversationId) };
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
store.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* GS2-20 — the conversations a `/resume` picker may offer: the most recent ones that carry a thread
|
|
153
|
+
* (so a resume could actually re-enter them), minus the one the session is already in. `[]` when
|
|
154
|
+
* history is off or the store cannot be opened, which the caller renders as "nothing to resume".
|
|
155
|
+
* Fail-soft, never throws.
|
|
156
|
+
*/
|
|
157
|
+
export function listResumableConversationsSafe(config, options = {}) {
|
|
158
|
+
try {
|
|
159
|
+
if (!isHistoryEnabled(config))
|
|
160
|
+
return [];
|
|
161
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
162
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
163
|
+
if (!store)
|
|
164
|
+
return [];
|
|
165
|
+
try {
|
|
166
|
+
// Over-fetch so the filter below still yields up to `limit` rows when recent conversations
|
|
167
|
+
// are single-shot runs (no thread) or the excluded one.
|
|
168
|
+
const limit = options.limit ?? 20;
|
|
169
|
+
return store
|
|
170
|
+
.listConversations(limit * 3)
|
|
171
|
+
.filter((c) => c.threadId !== undefined && c.id !== options.exclude)
|
|
172
|
+
.slice(0, limit);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
store.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* GS2-20 — the stored approval-grants document of one conversation, as the opaque JSON string the
|
|
184
|
+
* approvals layer wrote (`core/approvals/conversationGrants.ts` owns the format), or `null` when
|
|
185
|
+
* there is none. Governed by the same switch as everything else here. Fail-soft, never throws.
|
|
186
|
+
*/
|
|
187
|
+
export function readConversationGrantsSafe(config, conversationId) {
|
|
188
|
+
try {
|
|
189
|
+
if (!isHistoryEnabled(config))
|
|
190
|
+
return null;
|
|
191
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
192
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
193
|
+
if (!store)
|
|
194
|
+
return null;
|
|
195
|
+
try {
|
|
196
|
+
return store.getConversationGrants(conversationId);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
store.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* GS2-20 — replace one conversation's stored approval-grants document. Called whenever the session's
|
|
208
|
+
* grants change, so the row always holds the current set and a resume restores exactly what the
|
|
209
|
+
* conversation had. Returns whether the row was written. Fail-soft, never throws — a grant that
|
|
210
|
+
* could not be recorded costs one re-prompt after a resume, and must never cost the turn.
|
|
211
|
+
*/
|
|
212
|
+
export function writeConversationGrantsSafe(config, conversationId, grantsJson) {
|
|
213
|
+
try {
|
|
214
|
+
if (!isHistoryEnabled(config))
|
|
215
|
+
return false;
|
|
216
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath);
|
|
217
|
+
const store = openHistoryStore(dbPath, { create: false });
|
|
218
|
+
if (!store)
|
|
219
|
+
return false;
|
|
220
|
+
try {
|
|
221
|
+
return store.setConversationGrants(conversationId, grantsJson);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
store.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
56
231
|
//# sourceMappingURL=recordSession.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordSession.js","sourceRoot":"","sources":["../../src/history/recordSession.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recordSession.js","sourceRoot":"","sources":["../../src/history/recordSession.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseCheckpointSaver } from '@langchain/langgraph';
|
|
2
|
+
import { type HistoryConfigView } from '#src/history/historyEnabled.js';
|
|
3
|
+
/** The checkpointer a session drives, plus what the session needs to know about it. */
|
|
4
|
+
export interface SessionCheckpointer {
|
|
5
|
+
/** The saver to hand to `GthAgentRunner.init` / `GthAgentInterface.init`. Never null. */
|
|
6
|
+
saver: BaseCheckpointSaver;
|
|
7
|
+
/**
|
|
8
|
+
* True when checkpoints are reaching disk — i.e. when this session can be resumed later. False
|
|
9
|
+
* for a `MemorySaver`, whether that is the deliberate opt-out or the fallback.
|
|
10
|
+
*/
|
|
11
|
+
durable: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The LangGraph thread this session's state is stored under, minted here so the conversation row
|
|
14
|
+
* can record it BEFORE the runner is initialised — `init` can throw partway, and a conversation
|
|
15
|
+
* whose thread id was never written is a listing entry that can never be resumed.
|
|
16
|
+
*/
|
|
17
|
+
threadId: string;
|
|
18
|
+
/**
|
|
19
|
+
* Name the conversation row this session's turns group under, so a later checkpoint-write failure
|
|
20
|
+
* can mark it unresumable ON DISK. Call once per conversation the session records under:
|
|
21
|
+
* immediately after `openConversationSafe`, and again when a mid-session `/resume` moves the
|
|
22
|
+
* session onto another conversation — the saver is the same, so the row it would mark has to
|
|
23
|
+
* follow the session. Passing `undefined` (history off, or the row could not be opened) is fine
|
|
24
|
+
* and simply leaves nothing to mark. Safe to call before or after a failure has already happened
|
|
25
|
+
* — a failure that arrives first is applied here instead, and a rebind after one marks the new
|
|
26
|
+
* row too, which is correct: a saver that has stopped writing is truncating whichever
|
|
27
|
+
* conversation it is now on. Never throws.
|
|
28
|
+
*
|
|
29
|
+
* **Optional on the interface, and called with `?.`**, because a dozen-odd specs stub
|
|
30
|
+
* `openSessionCheckpointerSafe` with a plain object literal to keep a session off the real
|
|
31
|
+
* database. Requiring it would break every one of them at runtime for no gain: a stub that omits
|
|
32
|
+
* it simply has no conversation to mark, which is the correct behaviour for a test that is not
|
|
33
|
+
* about this.
|
|
34
|
+
*/
|
|
35
|
+
bindConversation?(conversationId: number | undefined): void;
|
|
36
|
+
/** Release the underlying connection (a no-op when the saver is in memory). Never throws. */
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
export interface OpenSessionCheckpointerOptions {
|
|
40
|
+
/** Drive a specific thread instead of a fresh one — how a resume re-enters a stored thread. */
|
|
41
|
+
threadId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Where the fallback notice goes. Defaults to {@link displayWarning}; injectable so a caller on a
|
|
44
|
+
* managed surface can route it, and so a test can read it without mocking the console.
|
|
45
|
+
*/
|
|
46
|
+
notify?: (message: string) => void;
|
|
47
|
+
}
|
|
48
|
+
export declare function openSessionCheckpointerSafe(config: HistoryConfigView, options?: OpenSessionCheckpointerOptions): SessionCheckpointer;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* GS2-20 — the bridge from a session's config to the checkpointer it drives, and the counterpart to
|
|
4
|
+
* the recorder bridge in `recordSession.ts`: one call, never throws, always returns a usable saver.
|
|
5
|
+
*
|
|
6
|
+
* **The fallback is a `MemorySaver`, never the absence of one.** With a tool-approval interrupt
|
|
7
|
+
* installed and no checkpointer at all, the first gated tool call throws a LangGraph-internal
|
|
8
|
+
* `MISSING_CHECKPOINTER` in the middle of a turn — a failure with no relationship to what the user
|
|
9
|
+
* was doing. So a database this session cannot open costs exactly one thing, resumability, and the
|
|
10
|
+
* user is told that plainly rather than discovering it later.
|
|
11
|
+
*
|
|
12
|
+
* **A write that fails later costs the same one thing.** This module owns the degrade policy the
|
|
13
|
+
* saver reports into: one notice per session, and — the part that has to outlive the process — the
|
|
14
|
+
* conversation's thread link cleared on disk, so a later resume refuses it. A checkpoint chain that
|
|
15
|
+
* stopped growing mid-conversation is still well-formed, so without that mark the next `--resume`
|
|
16
|
+
* would load a truncated conversation and present it as complete.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { MemorySaver } from '@langchain/langgraph';
|
|
20
|
+
import { openCheckpointSaver } from '#src/history/checkpointSaver.js';
|
|
21
|
+
import { isHistoryEnabled } from '#src/history/historyEnabled.js';
|
|
22
|
+
import { resolveHistoryDbPath } from '#src/history/historyStore.js';
|
|
23
|
+
import { markConversationUnresumableSafe } from '#src/history/recordSession.js';
|
|
24
|
+
import { displayWarning } from '#src/utils/consoleUtils.js';
|
|
25
|
+
/**
|
|
26
|
+
* Open the session's checkpointer. Returns a durable, SQLite-backed saver when history is on and
|
|
27
|
+
* the database opens; a `MemorySaver` otherwise. Never throws.
|
|
28
|
+
*
|
|
29
|
+
* The `MemorySaver` case is two different situations that must not be conflated:
|
|
30
|
+
* - `history.enabled: false` — the user turned persistence off. Silent: they asked for this.
|
|
31
|
+
* - the database would not open — the user asked for persistence and did not get it, so
|
|
32
|
+
* {@link OpenSessionCheckpointerOptions.notify} says so.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Every durable checkpointer currently open in this process, as its own close function.
|
|
36
|
+
*
|
|
37
|
+
* A session normally closes its connection in a `finally`, but a process killed by a signal never
|
|
38
|
+
* reaches one: `systemUtils` answers `SIGINT`/`SIGTERM` with `process.exit(0)`, which unwinds no
|
|
39
|
+
* `try`/`finally` at all, and a `SIGHUP` — a user closing the terminal window — terminates without
|
|
40
|
+
* running anything. Leaving the database open until the OS tears the process down is harmless on
|
|
41
|
+
* POSIX and not on win32, where a live handle blocks the file from being deleted or replaced.
|
|
42
|
+
*/
|
|
43
|
+
const openSavers = new Set();
|
|
44
|
+
/** Installed at most once per process, on the first durable checkpointer. */
|
|
45
|
+
let terminationHooksInstalled = false;
|
|
46
|
+
/**
|
|
47
|
+
* Close every open checkpointer on the way out of the process.
|
|
48
|
+
*
|
|
49
|
+
* `exit` rather than a `SIGINT`/`SIGTERM` handler of our own, deliberately: `systemUtils` registers
|
|
50
|
+
* its signal handlers when it is first imported, which is long before any session starts, and they
|
|
51
|
+
* call `process.exit(0)`. Node runs signal listeners in registration order, so a later listener of
|
|
52
|
+
* ours would never be reached — but `process.exit()` does run `exit` listeners, so that one hook
|
|
53
|
+
* covers `SIGINT`, `SIGTERM`, an explicit exit and a normal return alike. `SIGHUP` has no such
|
|
54
|
+
* handler anywhere and so needs its own.
|
|
55
|
+
*/
|
|
56
|
+
function ensureTerminationHooks() {
|
|
57
|
+
if (terminationHooksInstalled)
|
|
58
|
+
return;
|
|
59
|
+
terminationHooksInstalled = true;
|
|
60
|
+
const closeAll = () => {
|
|
61
|
+
for (const close of [...openSavers]) {
|
|
62
|
+
try {
|
|
63
|
+
close();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Nothing useful can be done about a failed close while the process is already leaving.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
openSavers.clear();
|
|
70
|
+
};
|
|
71
|
+
process.on('exit', closeAll);
|
|
72
|
+
const onHangup = () => {
|
|
73
|
+
// Remove ourselves first so the default disposition applies again and the process dies with the
|
|
74
|
+
// status it would have had. Swallowing a hangup would leave a session running invisibly.
|
|
75
|
+
process.off('SIGHUP', onHangup);
|
|
76
|
+
closeAll();
|
|
77
|
+
process.kill(process.pid, 'SIGHUP');
|
|
78
|
+
};
|
|
79
|
+
process.on('SIGHUP', onHangup);
|
|
80
|
+
}
|
|
81
|
+
export function openSessionCheckpointerSafe(config, options = {}) {
|
|
82
|
+
const threadId = options.threadId ?? randomUUID();
|
|
83
|
+
const notify = options.notify ?? displayWarning;
|
|
84
|
+
const inMemory = () => ({
|
|
85
|
+
saver: new MemorySaver(),
|
|
86
|
+
durable: false,
|
|
87
|
+
threadId,
|
|
88
|
+
bindConversation: () => { },
|
|
89
|
+
close: () => { },
|
|
90
|
+
});
|
|
91
|
+
// GS2-20 — the degrade path. A checkpoint write that fails mid-session drops the write and lets
|
|
92
|
+
// the turn continue; these two closures are the "loudly" half. They are declared before the saver
|
|
93
|
+
// because the saver is handed `degrade` at construction.
|
|
94
|
+
//
|
|
95
|
+
// The two facts are tracked separately on purpose. `degraded` is what happened; `conversationId`
|
|
96
|
+
// is where to write it down, and it is not known yet — the conversation row is opened by the
|
|
97
|
+
// caller AFTER this returns, because the row has to carry the thread id minted just above. So a
|
|
98
|
+
// failure can precede the binding, and `applyMark` is called from both sides to close that gap.
|
|
99
|
+
let degraded = false;
|
|
100
|
+
let conversationId;
|
|
101
|
+
const applyMark = () => {
|
|
102
|
+
if (!degraded || conversationId === undefined)
|
|
103
|
+
return;
|
|
104
|
+
markConversationUnresumableSafe(config, conversationId);
|
|
105
|
+
};
|
|
106
|
+
const degrade = () => {
|
|
107
|
+
// Once per session, not once per write: a full disk fails every super-step, and a notice per
|
|
108
|
+
// write would bury the turn the user is still having under its own error report.
|
|
109
|
+
if (degraded)
|
|
110
|
+
return;
|
|
111
|
+
degraded = true;
|
|
112
|
+
notify('Could not save this conversation to the conversation store, so it will not be resumable ' +
|
|
113
|
+
'later — the rest of this session is unaffected and your work is not lost. The usual cause ' +
|
|
114
|
+
'is a full or read-only disk. Set `history.enabled: false` in your config to stop trying.');
|
|
115
|
+
applyMark();
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
if (!isHistoryEnabled(config))
|
|
119
|
+
return inMemory();
|
|
120
|
+
const dbPath = resolveHistoryDbPath(config.history?.dbPath, /* ensureDir */ true);
|
|
121
|
+
const saver = openCheckpointSaver(dbPath, { onWriteFailure: degrade });
|
|
122
|
+
if (!saver) {
|
|
123
|
+
notify(`Could not open the conversation store at ${dbPath}, so this session will not be ` +
|
|
124
|
+
'resumable later. It runs normally otherwise; set `history.enabled: false` in your ' +
|
|
125
|
+
'config to stop trying.');
|
|
126
|
+
return inMemory();
|
|
127
|
+
}
|
|
128
|
+
// GS2-20 — hold the connection open only as long as the session actually runs. `saver.close()`
|
|
129
|
+
// is itself fail-soft, so the guard is about not counting a closed saver as open rather than
|
|
130
|
+
// about a double close throwing.
|
|
131
|
+
let closed = false;
|
|
132
|
+
const closeOnce = () => {
|
|
133
|
+
if (closed)
|
|
134
|
+
return;
|
|
135
|
+
closed = true;
|
|
136
|
+
saver.close();
|
|
137
|
+
};
|
|
138
|
+
openSavers.add(closeOnce);
|
|
139
|
+
ensureTerminationHooks();
|
|
140
|
+
// GS2-107 — the automatic half of the retention policy runs HERE, on the orderly close of a
|
|
141
|
+
// session that actually ran, and nowhere else.
|
|
142
|
+
//
|
|
143
|
+
// **Why the close and not the open.** The connection is already there, so it adds no open of its
|
|
144
|
+
// own, and it lands after the person has stopped waiting on the tool rather than in front of
|
|
145
|
+
// their first prompt — a user with a large store must not pay for retention at startup.
|
|
146
|
+
//
|
|
147
|
+
// **Why not in `closeOnce`.** That closure is also what the process-exit and SIGHUP hooks call.
|
|
148
|
+
// A row delete while the process is already leaving is work nobody is waiting for and nothing
|
|
149
|
+
// can report, on a path that a signal can interrupt halfway.
|
|
150
|
+
//
|
|
151
|
+
// **Why the `bindConversation` gate.** Both surfaces bind straight after the resume checks pass,
|
|
152
|
+
// so a checkpointer closed before that point is one whose session never started — the
|
|
153
|
+
// `--resume` refusal path, which exits 1 promising that nothing was changed. Deleting rows there
|
|
154
|
+
// would make that sentence false.
|
|
155
|
+
//
|
|
156
|
+
// **What keeps this session's own thread safe.** Not the id minted above: the runner rotates
|
|
157
|
+
// threads and does not tell anyone — `resetThread()` on `/clear`, `resumeConversation` onto a
|
|
158
|
+
// stored thread — so after any rotation that id names a thread nobody wrote. The saver excludes
|
|
159
|
+
// the threads IT wrote instead, which is the same set by construction however often the session
|
|
160
|
+
// rotated, so nothing is passed here. What remains outside that set is a live session in
|
|
161
|
+
// ANOTHER process, and it is bounded only by the grace window; closing that gap needs shared
|
|
162
|
+
// cross-process session state, which this ticket does not add.
|
|
163
|
+
let served = false;
|
|
164
|
+
let reclaimed = false;
|
|
165
|
+
const reclaimOnClose = () => {
|
|
166
|
+
if (reclaimed || !served || closed)
|
|
167
|
+
return;
|
|
168
|
+
reclaimed = true;
|
|
169
|
+
try {
|
|
170
|
+
saver.reclaimUnresumableThreads();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* retention is housekeeping: it must never be the reason a session fails to exit */
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
saver,
|
|
178
|
+
durable: true,
|
|
179
|
+
threadId,
|
|
180
|
+
bindConversation: (id) => {
|
|
181
|
+
served = true;
|
|
182
|
+
conversationId = id;
|
|
183
|
+
applyMark();
|
|
184
|
+
},
|
|
185
|
+
close: () => {
|
|
186
|
+
reclaimOnClose();
|
|
187
|
+
openSavers.delete(closeOnce);
|
|
188
|
+
closeOnce();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Reaching here means something outside the saver's own fail-soft open threw — resolving the
|
|
194
|
+
// path, or creating `~/.gsloth`. Same outcome, same sentence: the session runs, unresumable.
|
|
195
|
+
notify('Could not open the conversation store, so this session will not be resumable later. ' +
|
|
196
|
+
'It runs normally otherwise; set `history.enabled: false` in your config to stop trying.');
|
|
197
|
+
return inMemory();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=sessionCheckpointer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessionCheckpointer.js","sourceRoot":"","sources":["../../src/history/sessionCheckpointer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
export * from '#src/constants.js';
|
|
2
2
|
export * from '#src/core/types.js';
|
|
3
3
|
export { gthLeanAgentFactory } from '#src/core/gthLeanAgentFactory.js';
|
|
4
|
+
export * from '#src/core/compaction.js';
|
|
5
|
+
export { AutocompactController } from '#src/core/compactionThreshold.js';
|
|
6
|
+
export type { AutocompactConfig, AutocompactControllerOptions, AutocompactStatus, AutocompactThresholdOrigin, ResolvedAutocompactConfig, } from '#src/core/compactionThreshold.js';
|
|
7
|
+
export type { ContextWindowOrigin, ContextWindowReading, ContextWindowResolutionOptions, ContextWindowSource, OllamaLikeModel, ResolvedContextWindow, } from '#src/core/contextWindow.js';
|
|
4
8
|
export * from '#src/config.js';
|
|
5
9
|
export * from '#src/providers/modelDiscovery.js';
|
|
10
|
+
export type { CatalogOptions, EnrichedModel, ModelCatalogEntry, ModelCost, ModelLimit, ModelModalities, ProviderCatalog, } from '#src/providers/modelCatalog.js';
|
|
6
11
|
export * from '#src/history/historyStore.js';
|
|
7
12
|
export * from '#src/history/recordSession.js';
|
|
8
13
|
export * from '#src/history/historyFormat.js';
|
|
14
|
+
export type { CheckpointStoreStats, PrunableConversation, ReclaimSummary, ThreadUsage, } from '#src/history/checkpointRetention.js';
|