verifiable-thinking-mcp 0.4.0
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/LICENSE +21 -0
- package/README.md +339 -0
- package/package.json +75 -0
- package/src/index.ts +38 -0
- package/src/lib/cache.ts +246 -0
- package/src/lib/compression.ts +804 -0
- package/src/lib/compute/cache.ts +86 -0
- package/src/lib/compute/classifier.ts +555 -0
- package/src/lib/compute/confidence.ts +79 -0
- package/src/lib/compute/context.ts +154 -0
- package/src/lib/compute/extract.ts +200 -0
- package/src/lib/compute/filter.ts +224 -0
- package/src/lib/compute/index.ts +171 -0
- package/src/lib/compute/math.ts +247 -0
- package/src/lib/compute/patterns.ts +564 -0
- package/src/lib/compute/registry.ts +145 -0
- package/src/lib/compute/solvers/arithmetic.ts +65 -0
- package/src/lib/compute/solvers/calculus.ts +249 -0
- package/src/lib/compute/solvers/derivation-core.ts +371 -0
- package/src/lib/compute/solvers/derivation-latex.ts +160 -0
- package/src/lib/compute/solvers/derivation-mistakes.ts +1046 -0
- package/src/lib/compute/solvers/derivation-simplify.ts +451 -0
- package/src/lib/compute/solvers/derivation-transform.ts +620 -0
- package/src/lib/compute/solvers/derivation.ts +67 -0
- package/src/lib/compute/solvers/facts.ts +120 -0
- package/src/lib/compute/solvers/formula.ts +728 -0
- package/src/lib/compute/solvers/index.ts +36 -0
- package/src/lib/compute/solvers/logic.ts +422 -0
- package/src/lib/compute/solvers/probability.ts +307 -0
- package/src/lib/compute/solvers/statistics.ts +262 -0
- package/src/lib/compute/solvers/word-problems.ts +408 -0
- package/src/lib/compute/types.ts +107 -0
- package/src/lib/concepts.ts +111 -0
- package/src/lib/domain.ts +731 -0
- package/src/lib/extraction.ts +912 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/judge.ts +260 -0
- package/src/lib/math/ast.ts +842 -0
- package/src/lib/math/index.ts +8 -0
- package/src/lib/math/operators.ts +171 -0
- package/src/lib/math/tokenizer.ts +477 -0
- package/src/lib/patterns.ts +200 -0
- package/src/lib/session.ts +825 -0
- package/src/lib/think/challenge.ts +323 -0
- package/src/lib/think/complexity.ts +504 -0
- package/src/lib/think/confidence-drift.ts +507 -0
- package/src/lib/think/consistency.ts +347 -0
- package/src/lib/think/guidance.ts +188 -0
- package/src/lib/think/helpers.ts +568 -0
- package/src/lib/think/hypothesis.ts +216 -0
- package/src/lib/think/index.ts +127 -0
- package/src/lib/think/prompts.ts +262 -0
- package/src/lib/think/route.ts +358 -0
- package/src/lib/think/schema.ts +98 -0
- package/src/lib/think/scratchpad-schema.ts +662 -0
- package/src/lib/think/spot-check.ts +961 -0
- package/src/lib/think/types.ts +93 -0
- package/src/lib/think/verification.ts +260 -0
- package/src/lib/tokens.ts +177 -0
- package/src/lib/verification.ts +620 -0
- package/src/prompts/index.ts +10 -0
- package/src/prompts/templates.ts +336 -0
- package/src/resources/index.ts +8 -0
- package/src/resources/sessions.ts +196 -0
- package/src/tools/compress.ts +138 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/scratchpad.ts +2659 -0
- package/src/tools/sessions.ts +144 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager - High-performance session state with O(1) lookups
|
|
3
|
+
* Features:
|
|
4
|
+
* - O(1) step lookup via stepIndex Map
|
|
5
|
+
* - O(1) step existence check via stepNumbers Set
|
|
6
|
+
* - O(1) step-to-branch lookup via stepToBranchMap
|
|
7
|
+
* - Revision tracking with revised_by marker
|
|
8
|
+
* - Branch depth limits
|
|
9
|
+
* - Batched TTL cleanup for efficiency
|
|
10
|
+
* - Token tracking sync on cleanup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { clearSessionTokens } from "./tokens.ts";
|
|
14
|
+
|
|
15
|
+
export interface ThoughtRecord {
|
|
16
|
+
id: string;
|
|
17
|
+
step_number: number;
|
|
18
|
+
thought: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
branch_id: string;
|
|
21
|
+
verification?: {
|
|
22
|
+
passed: boolean;
|
|
23
|
+
confidence: number;
|
|
24
|
+
domain: string;
|
|
25
|
+
};
|
|
26
|
+
concepts?: string[];
|
|
27
|
+
compressed_context?: string;
|
|
28
|
+
// Compression stats
|
|
29
|
+
compression?: {
|
|
30
|
+
input_bytes_saved: number;
|
|
31
|
+
output_bytes_saved: number;
|
|
32
|
+
context_bytes_saved: number;
|
|
33
|
+
// Token tracking for LLM budget planning
|
|
34
|
+
original_tokens?: number;
|
|
35
|
+
compressed_tokens?: number;
|
|
36
|
+
};
|
|
37
|
+
// Revision tracking
|
|
38
|
+
revises_step?: number;
|
|
39
|
+
revision_reason?: string;
|
|
40
|
+
revised_by?: number; // Step number that revised this step
|
|
41
|
+
// Branching
|
|
42
|
+
branch_from?: number;
|
|
43
|
+
branch_name?: string;
|
|
44
|
+
branch_depth?: number;
|
|
45
|
+
// Dependencies
|
|
46
|
+
dependencies?: number[];
|
|
47
|
+
// Tool tracking
|
|
48
|
+
tools_used?: string[];
|
|
49
|
+
external_context?: Record<string, unknown>;
|
|
50
|
+
// Hypothesis-driven branching
|
|
51
|
+
hypothesis?: string;
|
|
52
|
+
success_criteria?: string;
|
|
53
|
+
// Preconditions (assumptions that must be true for this step)
|
|
54
|
+
preconditions?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Branch {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
from_step: number;
|
|
61
|
+
depth: number;
|
|
62
|
+
created_at: number;
|
|
63
|
+
/** Falsifiable hypothesis this branch tests */
|
|
64
|
+
hypothesis?: string;
|
|
65
|
+
/** Criteria for proving/disproving the hypothesis */
|
|
66
|
+
success_criteria?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Session {
|
|
70
|
+
id: string;
|
|
71
|
+
created_at: number;
|
|
72
|
+
updated_at: number;
|
|
73
|
+
thoughts: ThoughtRecord[];
|
|
74
|
+
branches: Map<string, Branch>;
|
|
75
|
+
metadata: Record<string, unknown>;
|
|
76
|
+
// O(1) lookup indexes
|
|
77
|
+
stepIndex: Map<number, ThoughtRecord>;
|
|
78
|
+
stepNumbers: Set<number>;
|
|
79
|
+
stepToBranchMap: Map<number, string>;
|
|
80
|
+
toolsUsedSet: Set<string>;
|
|
81
|
+
// Original question (for auto spot-check at complete)
|
|
82
|
+
question?: string;
|
|
83
|
+
// Pending thought that failed verification (awaiting recovery action)
|
|
84
|
+
pendingThought?: {
|
|
85
|
+
thought: ThoughtRecord;
|
|
86
|
+
verificationError: {
|
|
87
|
+
issue: string;
|
|
88
|
+
evidence: string;
|
|
89
|
+
suggestions: string[];
|
|
90
|
+
confidence: number;
|
|
91
|
+
domain: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface SessionManagerConfig {
|
|
97
|
+
ttl_ms: number;
|
|
98
|
+
cleanup_interval_ms: number;
|
|
99
|
+
max_sessions: number;
|
|
100
|
+
max_branch_depth: number;
|
|
101
|
+
max_history_size: number;
|
|
102
|
+
cleanup_batch_size: number; // Steps between cleanup checks
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const DEFAULT_CONFIG: SessionManagerConfig = {
|
|
106
|
+
ttl_ms: 30 * 60 * 1000, // 30 minutes
|
|
107
|
+
cleanup_interval_ms: 5 * 60 * 1000, // 5 minutes
|
|
108
|
+
max_sessions: 100,
|
|
109
|
+
max_branch_depth: 3,
|
|
110
|
+
max_history_size: 100,
|
|
111
|
+
cleanup_batch_size: 10, // Check cleanup every 10 steps
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Max pooled sessions to keep for reuse (avoids GC pressure from Map/Set allocations) */
|
|
115
|
+
const MAX_POOL_SIZE = 20;
|
|
116
|
+
|
|
117
|
+
class SessionManagerImpl {
|
|
118
|
+
private sessions: Map<string, Session> = new Map();
|
|
119
|
+
private config: SessionManagerConfig;
|
|
120
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
121
|
+
private stepsSinceCleanup = 0;
|
|
122
|
+
/** Pool of recycled sessions to avoid allocation churn */
|
|
123
|
+
private sessionPool: Session[] = [];
|
|
124
|
+
|
|
125
|
+
constructor(config: Partial<SessionManagerConfig> = {}) {
|
|
126
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
127
|
+
this.startCleanup();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private startCleanup(): void {
|
|
131
|
+
if (this.cleanupTimer) return;
|
|
132
|
+
this.cleanupTimer = setInterval(() => {
|
|
133
|
+
this.cleanup();
|
|
134
|
+
}, this.config.cleanup_interval_ms);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recycle a session by clearing its data structures without deallocating.
|
|
139
|
+
* This avoids GC pressure from repeatedly creating new Map/Set instances.
|
|
140
|
+
*/
|
|
141
|
+
private recycleSession(session: Session): void {
|
|
142
|
+
if (this.sessionPool.length >= MAX_POOL_SIZE) {
|
|
143
|
+
// Pool full, let GC handle it
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Clear data structures without deallocating
|
|
148
|
+
session.thoughts.length = 0;
|
|
149
|
+
session.branches.clear();
|
|
150
|
+
session.stepIndex.clear();
|
|
151
|
+
session.stepNumbers.clear();
|
|
152
|
+
session.stepToBranchMap.clear();
|
|
153
|
+
session.toolsUsedSet.clear();
|
|
154
|
+
session.metadata = {};
|
|
155
|
+
session.question = undefined;
|
|
156
|
+
session.pendingThought = undefined;
|
|
157
|
+
|
|
158
|
+
this.sessionPool.push(session);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get a pooled session if available, otherwise create new.
|
|
163
|
+
* Reusing sessions avoids Map/Set allocation overhead.
|
|
164
|
+
*/
|
|
165
|
+
private getPooledSession(sessionId: string): Session {
|
|
166
|
+
const pooled = this.sessionPool.pop();
|
|
167
|
+
if (pooled) {
|
|
168
|
+
// Reinitialize pooled session with new id
|
|
169
|
+
pooled.id = sessionId;
|
|
170
|
+
pooled.created_at = Date.now();
|
|
171
|
+
pooled.updated_at = Date.now();
|
|
172
|
+
pooled.branches.set("main", {
|
|
173
|
+
id: "main",
|
|
174
|
+
name: "Main",
|
|
175
|
+
from_step: 0,
|
|
176
|
+
depth: 0,
|
|
177
|
+
created_at: Date.now(),
|
|
178
|
+
});
|
|
179
|
+
return pooled;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// No pooled session available, create new
|
|
183
|
+
return this.createSession(sessionId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private cleanup(): void {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const expired: string[] = [];
|
|
189
|
+
|
|
190
|
+
for (const [id, session] of this.sessions) {
|
|
191
|
+
if (now - session.updated_at > this.config.ttl_ms) {
|
|
192
|
+
expired.push(id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const id of expired) {
|
|
197
|
+
// Clear token tracking FIRST (before session deletion and recycling)
|
|
198
|
+
clearSessionTokens(id);
|
|
199
|
+
|
|
200
|
+
const session = this.sessions.get(id);
|
|
201
|
+
// Delete from map BEFORE recycling to prevent use-after-free race
|
|
202
|
+
this.sessions.delete(id);
|
|
203
|
+
if (session) {
|
|
204
|
+
this.recycleSession(session);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Batched cleanup - only runs every N steps */
|
|
210
|
+
private batchedCleanup(force = false): void {
|
|
211
|
+
this.stepsSinceCleanup++;
|
|
212
|
+
if (!force && this.stepsSinceCleanup < this.config.cleanup_batch_size) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.stepsSinceCleanup = 0;
|
|
216
|
+
this.cleanup();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private createSession(sessionId: string): Session {
|
|
220
|
+
return {
|
|
221
|
+
id: sessionId,
|
|
222
|
+
created_at: Date.now(),
|
|
223
|
+
updated_at: Date.now(),
|
|
224
|
+
thoughts: [],
|
|
225
|
+
branches: new Map([
|
|
226
|
+
["main", { id: "main", name: "Main", from_step: 0, depth: 0, created_at: Date.now() }],
|
|
227
|
+
]),
|
|
228
|
+
metadata: {},
|
|
229
|
+
stepIndex: new Map(),
|
|
230
|
+
stepNumbers: new Set(),
|
|
231
|
+
stepToBranchMap: new Map(),
|
|
232
|
+
toolsUsedSet: new Set(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getOrCreate(sessionId: string): Session {
|
|
237
|
+
let session = this.sessions.get(sessionId);
|
|
238
|
+
|
|
239
|
+
if (!session) {
|
|
240
|
+
// Enforce max sessions limit
|
|
241
|
+
if (this.sessions.size >= this.config.max_sessions) {
|
|
242
|
+
let oldest: [string, Session] | null = null;
|
|
243
|
+
for (const entry of this.sessions) {
|
|
244
|
+
if (!oldest || entry[1].updated_at < oldest[1].updated_at) {
|
|
245
|
+
oldest = entry;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (oldest) {
|
|
249
|
+
const oldSession = this.sessions.get(oldest[0]);
|
|
250
|
+
if (oldSession) {
|
|
251
|
+
this.recycleSession(oldSession);
|
|
252
|
+
}
|
|
253
|
+
this.sessions.delete(oldest[0]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
session = this.getPooledSession(sessionId);
|
|
258
|
+
this.sessions.set(sessionId, session);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return session;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get(sessionId: string): Session | undefined {
|
|
265
|
+
const session = this.sessions.get(sessionId);
|
|
266
|
+
if (session) {
|
|
267
|
+
session.updated_at = Date.now();
|
|
268
|
+
}
|
|
269
|
+
return session;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** O(1) step lookup */
|
|
273
|
+
getStep(sessionId: string, stepNumber: number): ThoughtRecord | undefined {
|
|
274
|
+
const session = this.get(sessionId);
|
|
275
|
+
return session?.stepIndex.get(stepNumber);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** O(1) step existence check */
|
|
279
|
+
hasStep(sessionId: string, stepNumber: number): boolean {
|
|
280
|
+
const session = this.get(sessionId);
|
|
281
|
+
return session?.stepNumbers.has(stepNumber) ?? false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Calculate branch depth for a step */
|
|
285
|
+
calculateBranchDepth(session: Session, fromStep: number): number {
|
|
286
|
+
const branchId = session.stepToBranchMap.get(fromStep);
|
|
287
|
+
if (branchId) {
|
|
288
|
+
const branch = session.branches.get(branchId);
|
|
289
|
+
return branch ? branch.depth + 1 : 1;
|
|
290
|
+
}
|
|
291
|
+
return 1; // Branching from main history
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Add thought with O(1) index updates */
|
|
295
|
+
addThought(sessionId: string, thought: ThoughtRecord): { success: boolean; error?: string } {
|
|
296
|
+
const session = this.getOrCreate(sessionId);
|
|
297
|
+
|
|
298
|
+
// Batched cleanup
|
|
299
|
+
this.batchedCleanup();
|
|
300
|
+
|
|
301
|
+
// Validate revision
|
|
302
|
+
if (thought.revises_step !== undefined) {
|
|
303
|
+
if (thought.revises_step >= thought.step_number) {
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: `Cannot revise step ${thought.revises_step} from step ${thought.step_number}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Mark original step as revised
|
|
310
|
+
const original = session.stepIndex.get(thought.revises_step);
|
|
311
|
+
if (original) {
|
|
312
|
+
original.revised_by = thought.step_number;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Handle branching
|
|
317
|
+
if (thought.branch_from !== undefined) {
|
|
318
|
+
if (thought.branch_from >= thought.step_number) {
|
|
319
|
+
return { success: false, error: `Cannot branch from future step ${thought.branch_from}` };
|
|
320
|
+
}
|
|
321
|
+
if (!session.stepNumbers.has(thought.branch_from)) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
error: `Cannot branch from non-existent step ${thought.branch_from}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const depth = this.calculateBranchDepth(session, thought.branch_from);
|
|
329
|
+
if (depth > this.config.max_branch_depth) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: `Branch depth ${depth} exceeds max ${this.config.max_branch_depth}`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const branchId = thought.branch_id || `branch-${Date.now()}`;
|
|
337
|
+
if (!session.branches.has(branchId)) {
|
|
338
|
+
session.branches.set(branchId, {
|
|
339
|
+
id: branchId,
|
|
340
|
+
name: thought.branch_name || `Alternative ${session.branches.size}`,
|
|
341
|
+
from_step: thought.branch_from,
|
|
342
|
+
depth,
|
|
343
|
+
created_at: Date.now(),
|
|
344
|
+
hypothesis: thought.hypothesis,
|
|
345
|
+
success_criteria: thought.success_criteria,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
thought.branch_id = branchId;
|
|
349
|
+
thought.branch_depth = depth;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Validate dependencies
|
|
353
|
+
if (thought.dependencies?.length) {
|
|
354
|
+
const missing = thought.dependencies.filter((d) => !session.stepNumbers.has(d));
|
|
355
|
+
if (missing.length > 0) {
|
|
356
|
+
// Warn but don't fail
|
|
357
|
+
console.error(`Warning: Missing dependencies: steps ${missing.join(", ")}`);
|
|
358
|
+
}
|
|
359
|
+
// Check for circular/future dependencies
|
|
360
|
+
const invalid = thought.dependencies.filter((d) => d >= thought.step_number);
|
|
361
|
+
if (invalid.length > 0) {
|
|
362
|
+
return { success: false, error: `Cannot depend on future steps: ${invalid.join(", ")}` };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Track tools used
|
|
367
|
+
if (thought.tools_used?.length) {
|
|
368
|
+
for (const tool of thought.tools_used) {
|
|
369
|
+
session.toolsUsedSet.add(tool);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Add to history and update indexes
|
|
374
|
+
session.thoughts.push(thought);
|
|
375
|
+
session.stepIndex.set(thought.step_number, thought);
|
|
376
|
+
session.stepNumbers.add(thought.step_number);
|
|
377
|
+
|
|
378
|
+
const branchId = thought.branch_id || "main";
|
|
379
|
+
session.stepToBranchMap.set(thought.step_number, branchId);
|
|
380
|
+
|
|
381
|
+
// Ensure branch exists
|
|
382
|
+
if (!session.branches.has(branchId)) {
|
|
383
|
+
session.branches.set(branchId, {
|
|
384
|
+
id: branchId,
|
|
385
|
+
name: branchId === "main" ? "Main" : branchId,
|
|
386
|
+
from_step: 0,
|
|
387
|
+
depth: 0,
|
|
388
|
+
created_at: Date.now(),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
session.updated_at = Date.now();
|
|
393
|
+
|
|
394
|
+
// Trim history if needed
|
|
395
|
+
this.trimHistory(session);
|
|
396
|
+
|
|
397
|
+
return { success: true };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Trim history and clean up orphaned references */
|
|
401
|
+
private trimHistory(session: Session): void {
|
|
402
|
+
if (session.thoughts.length <= this.config.max_history_size) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const toRemove = session.thoughts.length - this.config.max_history_size;
|
|
407
|
+
const removed = session.thoughts.splice(0, toRemove);
|
|
408
|
+
|
|
409
|
+
// Clean up indexes
|
|
410
|
+
for (const thought of removed) {
|
|
411
|
+
session.stepIndex.delete(thought.step_number);
|
|
412
|
+
session.stepNumbers.delete(thought.step_number);
|
|
413
|
+
session.stepToBranchMap.delete(thought.step_number);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Clean up branches that reference removed steps
|
|
417
|
+
const removedStepNumbers = new Set(removed.map((t) => t.step_number));
|
|
418
|
+
for (const [branchId, branch] of session.branches) {
|
|
419
|
+
if (removedStepNumbers.has(branch.from_step) && branchId !== "main") {
|
|
420
|
+
session.branches.delete(branchId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
getThoughts(sessionId: string, branchId?: string): ThoughtRecord[] {
|
|
426
|
+
const session = this.get(sessionId);
|
|
427
|
+
if (!session) return [];
|
|
428
|
+
|
|
429
|
+
if (branchId) {
|
|
430
|
+
return session.thoughts.filter((t) => t.branch_id === branchId);
|
|
431
|
+
}
|
|
432
|
+
return session.thoughts;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
getBranches(sessionId: string): Branch[] {
|
|
436
|
+
const session = this.get(sessionId);
|
|
437
|
+
return session ? Array.from(session.branches.values()) : [];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
list(): { id: string; thought_count: number; branches: string[]; age_ms: number }[] {
|
|
441
|
+
const now = Date.now();
|
|
442
|
+
return Array.from(this.sessions.values()).map((s) => ({
|
|
443
|
+
id: s.id,
|
|
444
|
+
thought_count: s.thoughts.length,
|
|
445
|
+
branches: Array.from(s.branches.keys()),
|
|
446
|
+
age_ms: now - s.created_at,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
clear(sessionId: string): boolean {
|
|
451
|
+
// Clear token tracking when session is explicitly cleared
|
|
452
|
+
clearSessionTokens(sessionId);
|
|
453
|
+
return this.sessions.delete(sessionId);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
clearAll(): number {
|
|
457
|
+
const count = this.sessions.size;
|
|
458
|
+
// Clear token tracking for all sessions
|
|
459
|
+
for (const id of this.sessions.keys()) {
|
|
460
|
+
clearSessionTokens(id);
|
|
461
|
+
}
|
|
462
|
+
this.sessions.clear();
|
|
463
|
+
return count;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getSummary(sessionId: string): string | null {
|
|
467
|
+
const session = this.get(sessionId);
|
|
468
|
+
if (!session) return null;
|
|
469
|
+
|
|
470
|
+
// Aggregate compression stats
|
|
471
|
+
let totalInputSaved = 0;
|
|
472
|
+
let totalOutputSaved = 0;
|
|
473
|
+
let totalContextSaved = 0;
|
|
474
|
+
let compressedSteps = 0;
|
|
475
|
+
|
|
476
|
+
for (const thought of session.thoughts) {
|
|
477
|
+
if (thought.compression) {
|
|
478
|
+
compressedSteps++;
|
|
479
|
+
totalInputSaved += thought.compression.input_bytes_saved;
|
|
480
|
+
totalOutputSaved += thought.compression.output_bytes_saved;
|
|
481
|
+
totalContextSaved += thought.compression.context_bytes_saved;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const totalBytesSaved = totalInputSaved + totalOutputSaved + totalContextSaved;
|
|
486
|
+
|
|
487
|
+
const lines: string[] = [
|
|
488
|
+
`Session: ${sessionId}`,
|
|
489
|
+
`Thoughts: ${session.thoughts.length}`,
|
|
490
|
+
`Branches: ${Array.from(session.branches.keys()).join(", ")}`,
|
|
491
|
+
`Tools Used: ${Array.from(session.toolsUsedSet).join(", ") || "none"}`,
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
// Add compression summary if any compression occurred
|
|
495
|
+
if (compressedSteps > 0) {
|
|
496
|
+
lines.push(
|
|
497
|
+
`Compression: ${compressedSteps} steps, ${totalBytesSaved} bytes saved (input: ${totalInputSaved}, output: ${totalOutputSaved}, context: ${totalContextSaved})`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
lines.push("");
|
|
502
|
+
|
|
503
|
+
for (const thought of session.thoughts) {
|
|
504
|
+
const v = thought.verification;
|
|
505
|
+
const status = v ? (v.passed ? "+" : "x") : "?";
|
|
506
|
+
const revised = thought.revised_by ? ` [revised by ${thought.revised_by}]` : "";
|
|
507
|
+
const revising = thought.revises_step ? ` [revises ${thought.revises_step}]` : "";
|
|
508
|
+
lines.push(
|
|
509
|
+
`[${status}] Step ${thought.step_number} (${thought.branch_id})${revised}${revising}: ${thought.thought.slice(0, 60)}...`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return lines.join("\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
getCompressed(sessionId: string): string | null {
|
|
517
|
+
const session = this.get(sessionId);
|
|
518
|
+
if (!session) return null;
|
|
519
|
+
|
|
520
|
+
// Return only key thoughts (verified, final, or not revised)
|
|
521
|
+
const key = session.thoughts.filter(
|
|
522
|
+
(t) =>
|
|
523
|
+
!t.revised_by && // Not superseded
|
|
524
|
+
(t.verification?.passed ||
|
|
525
|
+
t.step_number === Math.max(...session.thoughts.map((x) => x.step_number))),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
return key.map((t) => t.thought).join(" -> ");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Get revision chain for a step */
|
|
532
|
+
getRevisionChain(sessionId: string, stepNumber: number): ThoughtRecord[] {
|
|
533
|
+
const session = this.get(sessionId);
|
|
534
|
+
if (!session) return [];
|
|
535
|
+
|
|
536
|
+
const chain: ThoughtRecord[] = [];
|
|
537
|
+
let current = session.stepIndex.get(stepNumber);
|
|
538
|
+
|
|
539
|
+
while (current) {
|
|
540
|
+
chain.push(current);
|
|
541
|
+
if (current.revised_by) {
|
|
542
|
+
current = session.stepIndex.get(current.revised_by);
|
|
543
|
+
} else {
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return chain;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Get compression stats for a session */
|
|
552
|
+
getCompressionStats(sessionId: string): {
|
|
553
|
+
totalBytesSaved: number;
|
|
554
|
+
stepCount: number;
|
|
555
|
+
breakdown: { input: number; output: number; context: number };
|
|
556
|
+
tokens: { original: number; compressed: number; saved: number };
|
|
557
|
+
} | null {
|
|
558
|
+
const session = this.get(sessionId);
|
|
559
|
+
if (!session) return null;
|
|
560
|
+
|
|
561
|
+
let totalInput = 0;
|
|
562
|
+
let totalOutput = 0;
|
|
563
|
+
let totalContext = 0;
|
|
564
|
+
let totalOriginalTokens = 0;
|
|
565
|
+
let totalCompressedTokens = 0;
|
|
566
|
+
let stepCount = 0;
|
|
567
|
+
|
|
568
|
+
for (const thought of session.thoughts) {
|
|
569
|
+
if (thought.compression) {
|
|
570
|
+
stepCount++;
|
|
571
|
+
totalInput += thought.compression.input_bytes_saved;
|
|
572
|
+
totalOutput += thought.compression.output_bytes_saved;
|
|
573
|
+
totalContext += thought.compression.context_bytes_saved;
|
|
574
|
+
totalOriginalTokens += thought.compression.original_tokens || 0;
|
|
575
|
+
totalCompressedTokens += thought.compression.compressed_tokens || 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
totalBytesSaved: totalInput + totalOutput + totalContext,
|
|
581
|
+
stepCount,
|
|
582
|
+
breakdown: { input: totalInput, output: totalOutput, context: totalContext },
|
|
583
|
+
tokens: {
|
|
584
|
+
original: totalOriginalTokens,
|
|
585
|
+
compressed: totalCompressedTokens,
|
|
586
|
+
saved: totalOriginalTokens - totalCompressedTokens,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Get path from root to a step (ancestors) - O(n) where n = path length */
|
|
592
|
+
getPath(sessionId: string, stepNumber: number): ThoughtRecord[] {
|
|
593
|
+
const session = this.get(sessionId);
|
|
594
|
+
if (!session) return [];
|
|
595
|
+
|
|
596
|
+
const path: ThoughtRecord[] = [];
|
|
597
|
+
let current = session.stepIndex.get(stepNumber);
|
|
598
|
+
|
|
599
|
+
while (current) {
|
|
600
|
+
path.unshift(current); // Add to front to get root-first order
|
|
601
|
+
|
|
602
|
+
// Walk back via branch_from or revision chain
|
|
603
|
+
if (current.branch_from !== undefined) {
|
|
604
|
+
current = session.stepIndex.get(current.branch_from);
|
|
605
|
+
} else if (current.revises_step !== undefined) {
|
|
606
|
+
// Skip to the original step being revised
|
|
607
|
+
current = session.stepIndex.get(current.revises_step);
|
|
608
|
+
} else if (current.step_number > 1) {
|
|
609
|
+
// Linear predecessor in same branch
|
|
610
|
+
const prevStep = current.step_number - 1;
|
|
611
|
+
const prev = session.stepIndex.get(prevStep);
|
|
612
|
+
// Only follow if same branch
|
|
613
|
+
if (prev && prev.branch_id === current.branch_id) {
|
|
614
|
+
current = prev;
|
|
615
|
+
} else {
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return path;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Get current step number for a session/branch */
|
|
627
|
+
getCurrentStep(sessionId: string, branchId = "main"): number {
|
|
628
|
+
const session = this.get(sessionId);
|
|
629
|
+
if (!session) return 0;
|
|
630
|
+
|
|
631
|
+
const branchThoughts = session.thoughts.filter((t) => t.branch_id === branchId);
|
|
632
|
+
if (branchThoughts.length === 0) return 0;
|
|
633
|
+
|
|
634
|
+
return Math.max(...branchThoughts.map((t) => t.step_number));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** Get next step number for a session/branch */
|
|
638
|
+
getNextStep(sessionId: string, branchId = "main"): number {
|
|
639
|
+
return this.getCurrentStep(sessionId, branchId) + 1;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Calculate average confidence across session */
|
|
643
|
+
getAverageConfidence(sessionId: string, branchId?: string): number {
|
|
644
|
+
const session = this.get(sessionId);
|
|
645
|
+
if (!session) return 0;
|
|
646
|
+
|
|
647
|
+
const thoughts = branchId
|
|
648
|
+
? session.thoughts.filter((t) => t.branch_id === branchId)
|
|
649
|
+
: session.thoughts;
|
|
650
|
+
|
|
651
|
+
if (thoughts.length === 0) return 0;
|
|
652
|
+
|
|
653
|
+
const confidences = thoughts
|
|
654
|
+
.filter((t) => t.verification?.confidence !== undefined)
|
|
655
|
+
.map((t) => t.verification!.confidence);
|
|
656
|
+
|
|
657
|
+
if (confidences.length === 0) return 0;
|
|
658
|
+
|
|
659
|
+
return confidences.reduce((a, b) => a + b, 0) / confidences.length;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Get total token usage for a session (estimated from thought lengths) */
|
|
663
|
+
getTokenUsage(sessionId: string): {
|
|
664
|
+
total: number;
|
|
665
|
+
compressed: number;
|
|
666
|
+
uncompressed: number;
|
|
667
|
+
} {
|
|
668
|
+
const session = this.get(sessionId);
|
|
669
|
+
if (!session) return { total: 0, compressed: 0, uncompressed: 0 };
|
|
670
|
+
|
|
671
|
+
let compressed = 0;
|
|
672
|
+
let uncompressed = 0;
|
|
673
|
+
|
|
674
|
+
for (const thought of session.thoughts) {
|
|
675
|
+
// Estimate tokens: ~4 chars per token
|
|
676
|
+
const thoughtTokens = Math.ceil(thought.thought.length / 4);
|
|
677
|
+
|
|
678
|
+
if (thought.compression?.compressed_tokens !== undefined) {
|
|
679
|
+
compressed += thought.compression.compressed_tokens;
|
|
680
|
+
} else {
|
|
681
|
+
uncompressed += thoughtTokens;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
total: compressed + uncompressed,
|
|
687
|
+
compressed,
|
|
688
|
+
uncompressed,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Store a pending thought that failed verification */
|
|
693
|
+
setPendingThought(
|
|
694
|
+
sessionId: string,
|
|
695
|
+
thought: ThoughtRecord,
|
|
696
|
+
verificationError: {
|
|
697
|
+
issue: string;
|
|
698
|
+
evidence: string;
|
|
699
|
+
suggestions: string[];
|
|
700
|
+
confidence: number;
|
|
701
|
+
domain: string;
|
|
702
|
+
},
|
|
703
|
+
): void {
|
|
704
|
+
const session = this.getOrCreate(sessionId);
|
|
705
|
+
session.pendingThought = { thought, verificationError };
|
|
706
|
+
session.updated_at = Date.now();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Get pending thought for a session */
|
|
710
|
+
getPendingThought(sessionId: string): Session["pendingThought"] {
|
|
711
|
+
const session = this.get(sessionId);
|
|
712
|
+
return session?.pendingThought;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Clear pending thought (after override or when replaced by revision) */
|
|
716
|
+
clearPendingThought(sessionId: string): boolean {
|
|
717
|
+
const session = this.get(sessionId);
|
|
718
|
+
if (!session?.pendingThought) return false;
|
|
719
|
+
session.pendingThought = undefined;
|
|
720
|
+
session.updated_at = Date.now();
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Commit pending thought (used by override operation)
|
|
725
|
+
* Atomic: only clears pending if addThought succeeds
|
|
726
|
+
*/
|
|
727
|
+
commitPendingThought(sessionId: string): { success: boolean; error?: string } {
|
|
728
|
+
const session = this.get(sessionId);
|
|
729
|
+
if (!session?.pendingThought) {
|
|
730
|
+
return { success: false, error: "No pending thought to commit" };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Clone the pending thought before attempting to add
|
|
734
|
+
// This ensures we can restore if addThought fails
|
|
735
|
+
const pendingCopy = session.pendingThought;
|
|
736
|
+
|
|
737
|
+
const result = this.addThought(sessionId, pendingCopy.thought);
|
|
738
|
+
if (result.success) {
|
|
739
|
+
// Only clear pending if addThought succeeded
|
|
740
|
+
session.pendingThought = undefined;
|
|
741
|
+
}
|
|
742
|
+
// If addThought failed, pending is preserved for retry/alternate recovery
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Store hint state for progressive reveals */
|
|
747
|
+
setHintState(
|
|
748
|
+
sessionId: string,
|
|
749
|
+
state: {
|
|
750
|
+
expression: string;
|
|
751
|
+
revealCount: number;
|
|
752
|
+
totalSteps: number;
|
|
753
|
+
simplified: string;
|
|
754
|
+
},
|
|
755
|
+
): void {
|
|
756
|
+
const session = this.getOrCreate(sessionId);
|
|
757
|
+
session.metadata.hintState = state;
|
|
758
|
+
session.updated_at = Date.now();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Get hint state for a session */
|
|
762
|
+
getHintState(sessionId: string): {
|
|
763
|
+
expression: string;
|
|
764
|
+
revealCount: number;
|
|
765
|
+
totalSteps: number;
|
|
766
|
+
simplified: string;
|
|
767
|
+
} | null {
|
|
768
|
+
const session = this.get(sessionId);
|
|
769
|
+
const state = session?.metadata?.hintState;
|
|
770
|
+
if (
|
|
771
|
+
state &&
|
|
772
|
+
typeof state === "object" &&
|
|
773
|
+
"expression" in state &&
|
|
774
|
+
"revealCount" in state &&
|
|
775
|
+
"totalSteps" in state &&
|
|
776
|
+
"simplified" in state
|
|
777
|
+
) {
|
|
778
|
+
return state as {
|
|
779
|
+
expression: string;
|
|
780
|
+
revealCount: number;
|
|
781
|
+
totalSteps: number;
|
|
782
|
+
simplified: string;
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Clear hint state */
|
|
789
|
+
clearHintState(sessionId: string): void {
|
|
790
|
+
const session = this.get(sessionId);
|
|
791
|
+
if (session?.metadata) {
|
|
792
|
+
delete session.metadata.hintState;
|
|
793
|
+
session.updated_at = Date.now();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Store the original question for a session (for auto spot-check at complete) */
|
|
798
|
+
setQuestion(sessionId: string, question: string): void {
|
|
799
|
+
const session = this.getOrCreate(sessionId);
|
|
800
|
+
// First-write-wins: don't overwrite existing question (prevents race condition)
|
|
801
|
+
if (session.question) return;
|
|
802
|
+
session.question = question;
|
|
803
|
+
session.updated_at = Date.now();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Get the stored question for a session */
|
|
807
|
+
getQuestion(sessionId: string): string | undefined {
|
|
808
|
+
const session = this.get(sessionId);
|
|
809
|
+
return session?.question;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
destroy(): void {
|
|
813
|
+
if (this.cleanupTimer) {
|
|
814
|
+
clearInterval(this.cleanupTimer);
|
|
815
|
+
this.cleanupTimer = null;
|
|
816
|
+
}
|
|
817
|
+
this.sessions.clear();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Export class for testing
|
|
822
|
+
export { SessionManagerImpl };
|
|
823
|
+
|
|
824
|
+
// Singleton instance
|
|
825
|
+
export const SessionManager = new SessionManagerImpl();
|