tracelattice 1.3.3 → 1.3.4
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/ServerConfig.d.ts +14 -0
- package/dist/ServerConfig.d.ts.map +1 -1
- package/dist/ServerConfig.js +12 -1
- package/dist/ServerConfig.js.map +1 -1
- package/dist/__tests__/core/HistoryManager.ownership.test.d.ts +2 -0
- package/dist/__tests__/core/HistoryManager.ownership.test.d.ts.map +1 -0
- package/dist/__tests__/core/SessionLock.test.d.ts +6 -0
- package/dist/__tests__/core/SessionLock.test.d.ts.map +1 -0
- package/dist/__tests__/core/SessionManager.test.d.ts +8 -0
- package/dist/__tests__/core/SessionManager.test.d.ts.map +1 -0
- package/dist/__tests__/core/ThoughtProcessor.toolAllowlist.test.d.ts +2 -0
- package/dist/__tests__/core/ThoughtProcessor.toolAllowlist.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/factories.d.ts +7 -0
- package/dist/__tests__/helpers/factories.d.ts.map +1 -1
- package/dist/__tests__/sanitize.enforceJsonShape.test.d.ts +2 -0
- package/dist/__tests__/sanitize.enforceJsonShape.test.d.ts.map +1 -0
- package/dist/__tests__/transport-owner-context.test.d.ts +8 -0
- package/dist/__tests__/transport-owner-context.test.d.ts.map +1 -0
- package/dist/cli.js +176 -57
- package/dist/config/ConfigLoader.d.ts +7 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +6 -1
- package/dist/config/ConfigLoader.js.map +1 -1
- package/dist/context/RequestContext.d.ts +26 -0
- package/dist/context/RequestContext.d.ts.map +1 -1
- package/dist/context/RequestContext.js +7 -1
- package/dist/context/RequestContext.js.map +1 -1
- package/dist/contracts/features.d.ts +2 -2
- package/dist/contracts/features.js.map +1 -1
- package/dist/contracts/interfaces.d.ts +42 -0
- package/dist/contracts/interfaces.d.ts.map +1 -1
- package/dist/core/HistoryManager.d.ts +13 -1
- package/dist/core/HistoryManager.d.ts.map +1 -1
- package/dist/core/HistoryManager.js +25 -14
- package/dist/core/HistoryManager.js.map +1 -1
- package/dist/core/InputNormalizer.d.ts.map +1 -1
- package/dist/core/InputNormalizer.js +5 -2
- package/dist/core/InputNormalizer.js.map +1 -1
- package/dist/core/SessionLock.d.ts +56 -0
- package/dist/core/SessionLock.d.ts.map +1 -0
- package/dist/core/SessionLock.js +43 -0
- package/dist/core/SessionLock.js.map +1 -0
- package/dist/core/SessionManager.d.ts +18 -3
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +34 -1
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/ThoughtProcessor.d.ts +21 -2
- package/dist/core/ThoughtProcessor.d.ts.map +1 -1
- package/dist/core/ThoughtProcessor.js +33 -13
- package/dist/core/ThoughtProcessor.js.map +1 -1
- package/dist/di/ServiceRegistry.d.ts +3 -3
- package/dist/di/ServiceRegistry.d.ts.map +1 -1
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +38 -2
- package/dist/errors.js.map +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +9 -3
- package/dist/lib.js.map +1 -1
- package/dist/registry/ToolRegistry.d.ts +1 -0
- package/dist/registry/ToolRegistry.d.ts.map +1 -1
- package/dist/registry/ToolRegistry.js +3 -0
- package/dist/registry/ToolRegistry.js.map +1 -1
- package/dist/sanitize.d.ts +70 -0
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +77 -1
- package/dist/sanitize.js.map +1 -1
- package/dist/schema.d.ts +35 -35
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +15 -5
- package/dist/schema.js.map +1 -1
- package/dist/transport/HttpTransport.d.ts.map +1 -1
- package/dist/transport/HttpTransport.js +9 -3
- package/dist/transport/HttpTransport.js.map +1 -1
- package/dist/transport/SseTransport.d.ts.map +1 -1
- package/dist/transport/SseTransport.js +10 -3
- package/dist/transport/SseTransport.js.map +1 -1
- package/dist/transport/StreamableHttpTransport.d.ts.map +1 -1
- package/dist/transport/StreamableHttpTransport.js +8 -3
- package/dist/transport/StreamableHttpTransport.js.map +1 -1
- package/dist/types/tool.d.ts +1 -1
- package/dist/types/tool.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -186,4 +186,46 @@ export interface IEdgeStore {
|
|
|
186
186
|
*/
|
|
187
187
|
size(sessionId?: string): number;
|
|
188
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Tool registry interface used for tool_name allowlisting in ThoughtProcessor.
|
|
191
|
+
*
|
|
192
|
+
* Provides the minimal read-only contract needed to validate tool_call thoughts.
|
|
193
|
+
* Implementations include the concrete `ToolRegistry` class.
|
|
194
|
+
*/
|
|
195
|
+
export interface IToolRegistry {
|
|
196
|
+
/**
|
|
197
|
+
* Returns true if a tool with the given name is registered.
|
|
198
|
+
*/
|
|
199
|
+
has(name: string): boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Returns the names of all registered tools.
|
|
202
|
+
*/
|
|
203
|
+
list(): string[];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Per-session async lock used to serialize state-mutating operations.
|
|
207
|
+
*
|
|
208
|
+
* Implementations must guarantee:
|
|
209
|
+
* - Concurrent calls for the same session id are serialized in arrival order.
|
|
210
|
+
* - Calls for different session ids never block each other.
|
|
211
|
+
* - The lock is always released, even when the critical section throws.
|
|
212
|
+
* - A waiter that times out does not corrupt the chain — later waiters
|
|
213
|
+
* still observe correct serialization.
|
|
214
|
+
*
|
|
215
|
+
* Used by `ThoughtProcessor.process()` to prevent `reset_state` from
|
|
216
|
+
* interleaving with concurrent `addThought` calls for the same session.
|
|
217
|
+
*/
|
|
218
|
+
export interface ISessionLock {
|
|
219
|
+
/**
|
|
220
|
+
* Execute `fn` while holding the lock for the given session.
|
|
221
|
+
*
|
|
222
|
+
* @param sessionId - Session to lock (`undefined` shares a global slot).
|
|
223
|
+
* @param fn - Critical section to run while holding the lock.
|
|
224
|
+
* @param timeoutMs - Max wait for lock acquisition (default 5000ms).
|
|
225
|
+
* @throws {LockTimeoutError} If the lock is not acquired before `timeoutMs`.
|
|
226
|
+
*/
|
|
227
|
+
withLock<T>(sessionId: string | undefined, fn: () => Promise<T>, timeoutMs?: number): Promise<T>;
|
|
228
|
+
/** Number of currently held lock chains (diagnostics). */
|
|
229
|
+
readonly size: number;
|
|
230
|
+
}
|
|
189
231
|
//# sourceMappingURL=interfaces.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/contracts/interfaces.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/contracts/interfaces.ts"],"names":[],"mappings":"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"}
|
|
@@ -43,6 +43,8 @@ export interface HistoryManagerConfig {
|
|
|
43
43
|
summaryStore?: ISummaryStore;
|
|
44
44
|
/** Whether to emit DAG edges (gated independently of edgeStore). @default false */
|
|
45
45
|
dagEdges?: boolean;
|
|
46
|
+
/** Maximum sessions per owner (per-owner LRU bucket). @default 50 */
|
|
47
|
+
maxSessionsPerOwner?: number;
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* Manages thought history and branching for sequential thinking.
|
|
@@ -80,7 +82,17 @@ export declare class HistoryManager implements IHistoryManager {
|
|
|
80
82
|
/** EdgeStore instance, if configured. Used by ThoughtProcessor for StrategyContext. */
|
|
81
83
|
getEdgeStore(): IEdgeStore | undefined;
|
|
82
84
|
private log;
|
|
83
|
-
/**
|
|
85
|
+
/** Reads owner from RequestContext (AsyncLocalStorage). Stdio path returns undefined. */
|
|
86
|
+
private _getCurrentOwner;
|
|
87
|
+
/**
|
|
88
|
+
* Gets or creates session state; updates lastAccessedAt.
|
|
89
|
+
*
|
|
90
|
+
* Ownership semantics:
|
|
91
|
+
* - `owner === undefined` (stdio path): never rejects, never sets owner.
|
|
92
|
+
* - `owner !== undefined`: if session has a different owner, throws
|
|
93
|
+
* `SessionAccessDeniedError`. If session was created without an owner
|
|
94
|
+
* (e.g. by stdio), the owner is set on first owner-aware access.
|
|
95
|
+
*/
|
|
84
96
|
private _getSession;
|
|
85
97
|
/**
|
|
86
98
|
* Adds a thought to the history. Routes per-session, applies retraction for backtrack,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HistoryManager.d.ts","sourceRoot":"","sources":["../../src/core/HistoryManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,EAEN,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"HistoryManager.d.ts","sourceRoot":"","sources":["../../src/core/HistoryManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { ValidationError, getErrorMessage } from "../errors.js";
|
|
1
|
+
import { SessionAccessDeniedError, ValidationError, getErrorMessage } from "../errors.js";
|
|
2
2
|
import { NullLogger } from "../logger/NullLogger.js";
|
|
3
3
|
import { DehydrationPolicy } from "./compression/DehydrationPolicy.js";
|
|
4
4
|
import { EdgeEmitter } from "./graph/EdgeEmitter.js";
|
|
5
5
|
import { PersistenceBuffer } from "./PersistenceBuffer.js";
|
|
6
6
|
import { SessionManager } from "./SessionManager.js";
|
|
7
|
+
import { getOwner } from "../context/RequestContext.js";
|
|
7
8
|
const ABSOLUTE_MAX_HISTORY_SIZE = 10000;
|
|
8
9
|
class HistoryManager {
|
|
9
10
|
static DEFAULT_SESSION = '__global__';
|
|
@@ -52,6 +53,7 @@ class HistoryManager {
|
|
|
52
53
|
sessionTtlMs: HistoryManager.SESSION_TTL_MS,
|
|
53
54
|
cleanupIntervalMs: 300000,
|
|
54
55
|
getMaxSessions: ()=>HistoryManager.MAX_SESSIONS,
|
|
56
|
+
maxSessionsPerOwner: config.maxSessionsPerOwner ?? 50,
|
|
55
57
|
logger: this._logger
|
|
56
58
|
});
|
|
57
59
|
this._persistenceBuffer = null;
|
|
@@ -91,10 +93,18 @@ class HistoryManager {
|
|
|
91
93
|
log(message, meta) {
|
|
92
94
|
this._logger.info(message, meta);
|
|
93
95
|
}
|
|
94
|
-
|
|
96
|
+
_getCurrentOwner() {
|
|
97
|
+
return getOwner();
|
|
98
|
+
}
|
|
99
|
+
_getSession(sessionId, owner) {
|
|
95
100
|
const key = sessionId ?? HistoryManager.DEFAULT_SESSION;
|
|
96
101
|
let session = this._sessions.get(key);
|
|
97
|
-
if (
|
|
102
|
+
if (session) {
|
|
103
|
+
if (void 0 !== owner) {
|
|
104
|
+
if (void 0 !== session.owner && session.owner !== owner) throw new SessionAccessDeniedError(key, session.owner, owner);
|
|
105
|
+
if (void 0 === session.owner) session.owner = owner;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
98
108
|
session = {
|
|
99
109
|
thought_history: [],
|
|
100
110
|
branches: {},
|
|
@@ -102,7 +112,8 @@ class HistoryManager {
|
|
|
102
112
|
availableSkills: void 0,
|
|
103
113
|
writeBuffer: [],
|
|
104
114
|
lastAccessedAt: Date.now(),
|
|
105
|
-
registeredBranches: new Set()
|
|
115
|
+
registeredBranches: new Set(),
|
|
116
|
+
owner
|
|
106
117
|
};
|
|
107
118
|
this._sessions.set(key, session);
|
|
108
119
|
this._sessionManager.evictExcessSessions(this._sessions);
|
|
@@ -111,7 +122,7 @@ class HistoryManager {
|
|
|
111
122
|
return session;
|
|
112
123
|
}
|
|
113
124
|
addThought(thought) {
|
|
114
|
-
const session = this._getSession(thought.session_id);
|
|
125
|
+
const session = this._getSession(thought.session_id, this._getCurrentOwner());
|
|
115
126
|
this._metrics?.counter('thought_requests_total', 1, {}, 'Total thought requests added to history');
|
|
116
127
|
session.thought_history.push(thought);
|
|
117
128
|
if ('backtrack' === thought.thought_type && void 0 !== thought.backtrack_target) this._applyRetraction(session, thought.backtrack_target);
|
|
@@ -173,7 +184,7 @@ class HistoryManager {
|
|
|
173
184
|
}
|
|
174
185
|
}
|
|
175
186
|
getHistory(sessionId) {
|
|
176
|
-
return this._getSession(sessionId).thought_history;
|
|
187
|
+
return this._getSession(sessionId, this._getCurrentOwner()).thought_history;
|
|
177
188
|
}
|
|
178
189
|
getHistoryHydrated(sessionId, opts) {
|
|
179
190
|
const history = this.getHistory(sessionId);
|
|
@@ -183,20 +194,20 @@ class HistoryManager {
|
|
|
183
194
|
return policy.apply(history, sid, opts);
|
|
184
195
|
}
|
|
185
196
|
getHistoryLength(sessionId) {
|
|
186
|
-
return this._getSession(sessionId).thought_history.length;
|
|
197
|
+
return this._getSession(sessionId, this._getCurrentOwner()).thought_history.length;
|
|
187
198
|
}
|
|
188
199
|
getBranches(sessionId) {
|
|
189
|
-
return this._getSession(sessionId).branches;
|
|
200
|
+
return this._getSession(sessionId, this._getCurrentOwner()).branches;
|
|
190
201
|
}
|
|
191
202
|
getBranchIds(sessionId) {
|
|
192
|
-
const session = this._getSession(sessionId);
|
|
203
|
+
const session = this._getSession(sessionId, this._getCurrentOwner());
|
|
193
204
|
const ids = new Set(Object.keys(session.branches));
|
|
194
205
|
for (const id of session.registeredBranches)ids.add(id);
|
|
195
206
|
return Array.from(ids);
|
|
196
207
|
}
|
|
197
208
|
registerBranch(sessionId, branchId) {
|
|
198
209
|
if ('string' != typeof branchId || 0 === branchId.length) throw new ValidationError('branch_id', 'branch_id must be a non-empty string');
|
|
199
|
-
const session = this._getSession(sessionId);
|
|
210
|
+
const session = this._getSession(sessionId, this._getCurrentOwner());
|
|
200
211
|
if (branchId in session.branches || session.registeredBranches.has(branchId)) throw new ValidationError('branch_id', `Branch already exists: ${branchId}`);
|
|
201
212
|
session.registeredBranches.add(branchId);
|
|
202
213
|
this.log('Registered branch', {
|
|
@@ -205,17 +216,17 @@ class HistoryManager {
|
|
|
205
216
|
});
|
|
206
217
|
}
|
|
207
218
|
branchExists(sessionId, branchId) {
|
|
208
|
-
const session = this._getSession(sessionId);
|
|
219
|
+
const session = this._getSession(sessionId, this._getCurrentOwner());
|
|
209
220
|
return branchId in session.branches || session.registeredBranches.has(branchId);
|
|
210
221
|
}
|
|
211
222
|
getAvailableMcpTools(sessionId) {
|
|
212
|
-
return this._getSession(sessionId).availableMcpTools;
|
|
223
|
+
return this._getSession(sessionId, this._getCurrentOwner()).availableMcpTools;
|
|
213
224
|
}
|
|
214
225
|
getAvailableSkills(sessionId) {
|
|
215
|
-
return this._getSession(sessionId).availableSkills;
|
|
226
|
+
return this._getSession(sessionId, this._getCurrentOwner()).availableSkills;
|
|
216
227
|
}
|
|
217
228
|
getBranch(branchId, sessionId) {
|
|
218
|
-
return this._getSession(sessionId).branches[branchId];
|
|
229
|
+
return this._getSession(sessionId, this._getCurrentOwner()).branches[branchId];
|
|
219
230
|
}
|
|
220
231
|
clear(sessionId) {
|
|
221
232
|
if (this._edgeStore) if (void 0 !== sessionId) this._edgeStore.clearSession(sessionId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core/HistoryManager.js","sources":["../../src/core/HistoryManager.ts"],"sourcesContent":["/**\n * History and branch management for sequential thinking.\n *\n * This module provides the `HistoryManager` class which manages thought history,\n * branching, and optional persistence with per-session state isolation.\n *\n * Internally delegates to three focused collaborators:\n * - `EdgeEmitter` — DAG edge emission\n * - `PersistenceBuffer` — buffered persistence + retry/backoff\n * - `SessionManager` — session lifecycle (TTL/LRU eviction)\n *\n * @module HistoryManager\n */\n\nimport type { IMetrics } from '../contracts/interfaces.js';\nimport type { IEdgeStore } from '../contracts/interfaces.js';\nimport type { ISummaryStore } from '../contracts/summary.js';\nimport { ValidationError, getErrorMessage } from '../errors.js';\nimport { NullLogger } from '../logger/NullLogger.js';\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport type { PersistenceBackend } from '../contracts/PersistenceBackend.js';\nimport {\n\tDehydrationPolicy,\n\ttype DehydrationOptions,\n\ttype HydratedEntry,\n} from './compression/DehydrationPolicy.js';\nimport { EdgeEmitter } from './graph/EdgeEmitter.js';\nimport type { IHistoryManager } from './IHistoryManager.js';\nimport { PersistenceBuffer, type PersistenceEventEmitter } from './PersistenceBuffer.js';\nimport { SessionManager } from './SessionManager.js';\nimport type { ThoughtData } from './thought.js';\n\n\n/** Absolute maximum history size (~20MB at 2KB/thought). Cannot be overridden. */\nexport const ABSOLUTE_MAX_HISTORY_SIZE = 10_000;\n\ninterface SessionState {\n\tthought_history: ThoughtData[];\n\tbranches: Record<string, ThoughtData[]>;\n\tavailableMcpTools: string[] | undefined;\n\tavailableSkills: string[] | undefined;\n\twriteBuffer: ThoughtData[];\n\tlastAccessedAt: number;\n\tregisteredBranches: Set<string>;\n}\n\nexport interface HistoryManagerConfig {\n\t/** Maximum number of thoughts to keep in main history. @default 1000 */\n\tmaxHistorySize?: number;\n\t/** Maximum number of branches to maintain. @default 50 */\n\tmaxBranches?: number;\n\t/** Maximum size of each branch. @default 100 */\n\tmaxBranchSize?: number;\n\tlogger?: Logger;\n\tpersistence?: PersistenceBackend | null;\n\tmetrics?: IMetrics;\n\t/** Maximum number of thoughts to buffer before flushing. @default 100 */\n\tpersistenceBufferSize?: number;\n\t/** Periodic flush interval in ms. @default 1000 */\n\tpersistenceFlushInterval?: number;\n\t/** Max retries for failed persistence flushes. @default 3 */\n\tpersistenceMaxRetries?: number;\n\teventEmitter?: PersistenceEventEmitter;\n\tedgeStore?: IEdgeStore;\n\tsummaryStore?: ISummaryStore;\n\t/** Whether to emit DAG edges (gated independently of edgeStore). @default false */\n\tdagEdges?: boolean;\n}\n\n/**\n * Manages thought history and branching for sequential thinking.\n *\n * Owns the per-session `Map<string, SessionState>`. Delegates DAG edge emission,\n * buffered persistence, and session TTL/LRU eviction to focused collaborators while\n * preserving test-coupled private member names (`_flushTimer`, `_startFlushTimer`,\n * `_flushBuffer`, `_sessions`).\n */\nexport class HistoryManager implements IHistoryManager {\n\tprivate static readonly DEFAULT_SESSION = '__global__';\n\tprivate static readonly SESSION_TTL_MS = 30 * 60 * 1000;\n\tprivate static readonly MAX_SESSIONS = 100;\n\tprivate _sessions: Map<string, SessionState> = new Map();\n\tprivate _maxHistorySize: number;\n\tprivate _maxBranches: number;\n\tprivate _maxBranchSize: number;\n\tprivate _logger: Logger;\n\tprivate _persistence: PersistenceBackend | null;\n\tprivate _persistenceEnabled: boolean;\n\tprivate _metrics?: IMetrics;\n\n\tprivate _edgeStore?: IEdgeStore;\n\tprivate _summaryStore?: ISummaryStore;\n\tprivate _dagEdges: boolean;\n\n\tprivate _eventEmitter: PersistenceEventEmitter | null;\n\n\tprivate readonly _edgeEmitter: EdgeEmitter;\n\tprivate _persistenceBuffer: PersistenceBuffer<SessionState> | null;\n\tprivate readonly _sessionManager: SessionManager<SessionState>;\n\n\tconstructor(config: HistoryManagerConfig = {}) {\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t\tconst requestedMaxSize = config.maxHistorySize ?? 10000;\n\t\tthis._maxHistorySize = Math.min(requestedMaxSize, ABSOLUTE_MAX_HISTORY_SIZE);\n\t\tif (requestedMaxSize > ABSOLUTE_MAX_HISTORY_SIZE) {\n\t\t\tthis._logger.warn('maxHistorySize exceeds absolute maximum, capped', {\n\t\t\t\trequested: requestedMaxSize,\n\t\t\t\tapplied: ABSOLUTE_MAX_HISTORY_SIZE,\n\t\t\t});\n\t\t}\n\t\tthis._maxBranches = config.maxBranches || 50;\n\t\tthis._maxBranchSize = config.maxBranchSize || 100;\n\t\tthis._persistence = config.persistence ?? null;\n\t\tthis._persistenceEnabled = this._persistence !== null;\n\t\tthis._metrics = config.metrics;\n\t\tthis._eventEmitter = config.eventEmitter ?? null;\n\t\tthis._edgeStore = config.edgeStore;\n\t\tthis._summaryStore = config.summaryStore;\n\t\tthis._dagEdges = config.dagEdges ?? true;\n\n\t\t// Wire delegates\n\t\tthis._edgeEmitter = new EdgeEmitter({\n\t\t\tedgeStore: this._edgeStore,\n\t\t\tdagEdges: this._dagEdges,\n\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\tlogger: this._logger,\n\t\t});\n\n\t\tthis._sessionManager = new SessionManager<SessionState>({\n\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\tsessionTtlMs: HistoryManager.SESSION_TTL_MS,\n\t\t\tcleanupIntervalMs: 5 * 60 * 1000,\n\t\t\tgetMaxSessions: () => HistoryManager.MAX_SESSIONS,\n\t\t\tlogger: this._logger,\n\t\t});\n\n\t\tthis._persistenceBuffer = null;\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistenceBuffer = new PersistenceBuffer<SessionState>({\n\t\t\t\tpersistence: this._persistence,\n\t\t\t\tbufferSize: config.persistenceBufferSize ?? 100,\n\t\t\t\tflushInterval: config.persistenceFlushInterval ?? 1000,\n\t\t\t\tmaxRetries: config.persistenceMaxRetries ?? 3,\n\t\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\t\tgetSessions: () => this._sessions,\n\t\t\t\tgetDefaultSession: () => this._getSession(),\n\t\t\t\tedgeStore: this._edgeStore,\n\t\t\t\teventEmitter: this._eventEmitter,\n\t\t\t\tlogger: this._logger,\n\t\t\t});\n\t\t\tthis._startFlushTimer();\n\t\t}\n\n\t\tthis._sessionManager.startCleanupTimer(this._sessions);\n\t}\n\n\t// Test-coupled accessors: these private member names must remain reachable\n\t// via `manager as unknown as { _flushTimer; _startFlushTimer }`.\n\tprivate get _flushTimer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._persistenceBuffer?.timer ?? null;\n\t}\n\n\tprivate _startFlushTimer(): void {\n\t\tthis._persistenceBuffer?.startFlushTimer();\n\t}\n\n\tprivate _stopFlushTimer(): void {\n\t\tif (this._flushTimer === null) return;\n\t\tthis._persistenceBuffer?.stopFlushTimer();\n\t}\n\n\t/** @internal Public for backward-compatible test coupling. */\n\tpublic async _flushBuffer(): Promise<void> {\n\t\tawait this._persistenceBuffer?.flush();\n\t}\n\n\t/** EdgeStore instance, if configured. Used by ThoughtProcessor for StrategyContext. */\n\tpublic getEdgeStore(): IEdgeStore | undefined {\n\t\treturn this._edgeStore;\n\t}\n\n\tprivate log(message: string, meta?: Record<string, unknown>): void {\n\t\tthis._logger.info(message, meta);\n\t}\n\n\t/** Gets or creates session state; updates lastAccessedAt. */\n\tprivate _getSession(sessionId?: string): SessionState {\n\t\tconst key = sessionId ?? HistoryManager.DEFAULT_SESSION;\n\t\tlet session = this._sessions.get(key);\n\t\tif (!session) {\n\t\t\tsession = {\n\t\t\t\tthought_history: [],\n\t\t\t\tbranches: {},\n\t\t\t\tavailableMcpTools: undefined,\n\t\t\t\tavailableSkills: undefined,\n\t\t\t\twriteBuffer: [],\n\t\t\t\tlastAccessedAt: Date.now(),\n\t\t\t\tregisteredBranches: new Set<string>(),\n\t\t\t};\n\t\t\tthis._sessions.set(key, session);\n\t\t\tthis._sessionManager.evictExcessSessions(this._sessions);\n\t\t}\n\t\tsession.lastAccessedAt = Date.now();\n\t\treturn session;\n\t}\n\n\t/**\n\t * Adds a thought to the history. Routes per-session, applies retraction for backtrack,\n\t * caches tools/skills, trims, branches, emits DAG edges, and buffers for persistence.\n\t */\n\tpublic addThought(thought: ThoughtData): void {\n\t\tconst session = this._getSession(thought.session_id);\n\t\tthis._metrics?.counter(\n\t\t\t'thought_requests_total',\n\t\t\t1,\n\t\t\t{},\n\t\t\t'Total thought requests added to history'\n\t\t);\n\n\t\tsession.thought_history.push(thought);\n\n\t\t// Logical retraction: when a backtrack thought is added, mark its target\n\t\t// as retracted (append-only — target remains in history).\n\t\tif (thought.thought_type === 'backtrack' && thought.backtrack_target !== undefined) {\n\t\t\tthis._applyRetraction(session, thought.backtrack_target);\n\t\t}\n\n\t\t// Cache available_mcp_tools/available_skills for cross-call persistence\n\t\tif (thought.available_mcp_tools) {\n\t\t\tsession.availableMcpTools = thought.available_mcp_tools;\n\t\t}\n\t\tif (thought.available_skills) {\n\t\t\tsession.availableSkills = thought.available_skills;\n\t\t}\n\n\t\tif (session.thought_history.length > this._maxHistorySize) {\n\t\t\tsession.thought_history = session.thought_history.slice(-this._maxHistorySize);\n\t\t\tthis.log(`History trimmed to ${this._maxHistorySize} items`, {\n\t\t\t\tmaxSize: this._maxHistorySize,\n\t\t\t});\n\t\t}\n\n\t\tif (thought.branch_from_thought && thought.branch_id) {\n\t\t\tthis._addToSessionBranch(session, thought.branch_id, thought);\n\t\t}\n\n\t\t// Track merge operations for analytics\n\t\tif (thought.merge_from_thoughts?.length || thought.merge_branch_ids?.length) {\n\t\t\tthis._metrics?.counter(\n\t\t\t\t'thought_merge_operations_total',\n\t\t\t\t1,\n\t\t\t\t{},\n\t\t\t\t'Total merge operations (graph topology)'\n\t\t\t);\n\t\t}\n\n\t\t// Emit DAG edges (no-op unless edgeStore + dagEdges flag both enabled)\n\t\tthis._edgeEmitter.emitEdgesForThought(session, thought);\n\n\t\t// Buffer thought for persistence (no-op when persistence disabled)\n\t\tif (this._persistenceBuffer) {\n\t\t\tthis._persistenceBuffer.bufferThought(session, thought);\n\t\t}\n\t}\n\n\t/** Marks the thought as retracted within the session (append-only). */\n\tprivate _applyRetraction(session: SessionState, targetNumber: number): void {\n\t\tfor (const t of session.thought_history) {\n\t\t\tif (t.thought_number === targetNumber) {\n\t\t\t\tt.retracted = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tfor (const branchThoughts of Object.values(session.branches)) {\n\t\t\tfor (const t of branchThoughts) {\n\t\t\t\tif (t.thought_number === targetNumber) {\n\t\t\t\t\tt.retracted = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate _addToSessionBranch(session: SessionState, branchId: string, thought: ThoughtData): void {\n\t\tif (!session.branches[branchId]) {\n\t\t\tsession.branches[branchId] = [];\n\t\t}\n\t\tthis._trimSessionBranchSize(session, branchId);\n\t\tsession.branches[branchId].push(thought);\n\n\t\tif (Object.keys(session.branches).length > this._maxBranches) {\n\t\t\tthis._cleanupSessionBranches(session);\n\t\t}\n\n\t\t// Persist branch to backend if enabled\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistence.saveBranch(branchId, session.branches[branchId]).catch((err) => {\n\t\t\t\tthis.log('Failed to persist branch', {\n\t\t\t\t\tbranchId,\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _cleanupSessionBranches(session: SessionState): void {\n\t\tconst branchCount = Object.keys(session.branches).length;\n\t\tif (branchCount > this._maxBranches) {\n\t\t\tconst branchesToRemove = Object.keys(session.branches).slice(\n\t\t\t\t0,\n\t\t\t\tbranchCount - this._maxBranches\n\t\t\t);\n\t\t\tfor (const branchId of branchesToRemove) {\n\t\t\t\tdelete session.branches[branchId];\n\t\t\t\tthis.log(`Removed old branch: ${branchId}`, { branchId });\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate _trimSessionBranchSize(session: SessionState, branchId: string): void {\n\t\tif ((session.branches[branchId] ?? []).length > this._maxBranchSize) {\n\t\t\tconst removed = session.branches[branchId]!.length - this._maxBranchSize;\n\t\t\tsession.branches[branchId] = session.branches[branchId]!.slice(-this._maxBranchSize);\n\t\t\tthis.log(`Trimmed branch '${branchId}': removed ${removed} old thoughts`, {\n\t\t\t\tbranchId,\n\t\t\t\tremoved,\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic getHistory(sessionId?: string): ThoughtData[] {\n\t\treturn this._getSession(sessionId).thought_history;\n\t}\n\n\t/**\n\t * Returns history with optional sliding-window dehydration. Non-mutating: when\n\t * `dagEdges` is off OR no `ISummaryStore` is configured, returns same as getHistory.\n\t */\n\tpublic getHistoryHydrated(\n\t\tsessionId?: string,\n\t\topts?: DehydrationOptions\n\t): HydratedEntry[] {\n\t\tconst history = this.getHistory(sessionId);\n\t\tif (!this._dagEdges || !this._summaryStore) {\n\t\t\treturn history.slice();\n\t\t}\n\t\tconst sid = sessionId ?? HistoryManager.DEFAULT_SESSION;\n\t\tconst policy = new DehydrationPolicy(this._summaryStore);\n\t\treturn policy.apply(history, sid, opts);\n\t}\n\n\tpublic getHistoryLength(sessionId?: string): number {\n\t\treturn this._getSession(sessionId).thought_history.length;\n\t}\n\n\tpublic getBranches(sessionId?: string): Record<string, ThoughtData[]> {\n\t\treturn this._getSession(sessionId).branches;\n\t}\n\n\tpublic getBranchIds(sessionId?: string): string[] {\n\t\tconst session = this._getSession(sessionId);\n\t\tconst ids = new Set<string>(Object.keys(session.branches));\n\t\tfor (const id of session.registeredBranches) ids.add(id);\n\t\treturn Array.from(ids);\n\t}\n\n\t/** @throws {ValidationError} If branchId is empty or already exists. */\n\tpublic registerBranch(sessionId: string | undefined, branchId: string): void {\n\t\tif (typeof branchId !== 'string' || branchId.length === 0) {\n\t\t\tthrow new ValidationError('branch_id', 'branch_id must be a non-empty string');\n\t\t}\n\t\tconst session = this._getSession(sessionId);\n\t\tif (branchId in session.branches || session.registeredBranches.has(branchId)) {\n\t\t\tthrow new ValidationError('branch_id', `Branch already exists: ${branchId}`);\n\t\t}\n\t\tsession.registeredBranches.add(branchId);\n\t\tthis.log('Registered branch', { branchId, sessionId: sessionId ?? null });\n\t}\n\n\tpublic branchExists(sessionId: string | undefined, branchId: string): boolean {\n\t\tconst session = this._getSession(sessionId);\n\t\treturn branchId in session.branches || session.registeredBranches.has(branchId);\n\t}\n\n\tpublic getAvailableMcpTools(sessionId?: string): string[] | undefined {\n\t\treturn this._getSession(sessionId).availableMcpTools;\n\t}\n\n\tpublic getAvailableSkills(sessionId?: string): string[] | undefined {\n\t\treturn this._getSession(sessionId).availableSkills;\n\t}\n\n\tpublic getBranch(branchId: string, sessionId?: string): ThoughtData[] | undefined {\n\t\treturn this._getSession(sessionId).branches[branchId];\n\t}\n\n\t/** Clears history and branches. If sessionId provided, clears only that session. */\n\tpublic clear(sessionId?: string): void {\n\t\t// Clear edges from EdgeStore (before session map mutation so keys are still available)\n\t\tif (this._edgeStore) {\n\t\t\tif (sessionId !== undefined) {\n\t\t\t\tthis._edgeStore.clearSession(sessionId);\n\t\t\t} else {\n\t\t\t\tfor (const sid of this._sessions.keys()) {\n\t\t\t\t\tthis._edgeStore.clearSession(sid);\n\t\t\t\t}\n\t\t\t\t// Also clear the default session in case no session entries exist yet\n\t\t\t\tthis._edgeStore.clearSession(HistoryManager.DEFAULT_SESSION);\n\t\t\t}\n\t\t}\n\n\t\tif (sessionId !== undefined) {\n\t\t\tthis._sessions.delete(sessionId);\n\t\t\tthis.log('Session cleared', { sessionId });\n\t\t} else {\n\t\t\tthis._sessions.clear();\n\t\t\tthis.log('History cleared (all sessions)');\n\t\t}\n\n\t\t// Clear persisted data if enabled\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistence.clear().catch((err) => {\n\t\t\t\tthis.log('Failed to clear persisted data', {\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic clearSession(sessionId: string): void {\n\t\tthis.clear(sessionId);\n\t}\n\n\tpublic getSessionIds(): string[] {\n\t\treturn Array.from(this._sessions.keys());\n\t}\n\n\tpublic getSessionCount(): number {\n\t\treturn this._sessions.size;\n\t}\n\n\t/** Loads history from persistence into the global session. Call at init. */\n\tpublic async loadFromPersistence(): Promise<void> {\n\t\tif (!this._persistenceEnabled || !this._persistence) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst isHealthy = await this._persistence.healthy();\n\t\t\tif (!isHealthy) {\n\t\t\t\tthis.log('Persistence backend not healthy, skipping load');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst globalSession = this._getSession();\n\n\t\t\tconst history = await this._persistence.loadHistory();\n\t\t\tif (history.length > 0) {\n\t\t\t\tglobalSession.thought_history = history.slice(-this._maxHistorySize);\n\t\t\t\tthis.log(`Loaded ${globalSession.thought_history.length} thoughts from persistence`);\n\t\t\t}\n\n\t\t\tconst branchIds = await this._persistence.listBranches();\n\t\t\tfor (const branchId of branchIds) {\n\t\t\t\tconst branchData = await this._persistence.loadBranch(branchId);\n\t\t\t\tif (branchData) {\n\t\t\t\t\tglobalSession.branches[branchId] = branchData.slice(-this._maxBranchSize);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.log(`Loaded ${Object.keys(globalSession.branches).length} branches from persistence`);\n\n\t\t\t// Load edges if EdgeStore is configured — restore for ALL persisted sessions\n\t\t\tif (this._edgeStore) {\n\t\t\t\ttry {\n\t\t\t\t\tconst edgeSessions = await this._persistence.listEdgeSessions();\n\t\t\t\t\tlet totalEdges = 0;\n\t\t\t\t\tfor (const sessionId of edgeSessions) {\n\t\t\t\t\t\tconst edges = await this._persistence.loadEdges(sessionId);\n\t\t\t\t\t\tfor (const edge of edges) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis._edgeStore.addEdge(edge);\n\t\t\t\t\t\t\t\ttotalEdges++;\n\t\t\t\t\t\t\t} catch (edgeErr) {\n\t\t\t\t\t\t\t\tthis.log('Failed to restore edge', {\n\t\t\t\t\t\t\t\t\tedgeId: edge.id,\n\t\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\t\terror: getErrorMessage(edgeErr),\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.log(\n\t\t\t\t\t\t`Loaded ${totalEdges} edges across ${edgeSessions.length} sessions from persistence`,\n\t\t\t\t\t);\n\t\t\t\t} catch (edgeError) {\n\t\t\t\t\tthis.log('Failed to load edges from persistence', {\n\t\t\t\t\t\terror: getErrorMessage(edgeError),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log('Failed to load from persistence', {\n\t\t\t\terror: getErrorMessage(error),\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic isPersistenceEnabled(): boolean {\n\t\treturn this._persistenceEnabled;\n\t}\n\n\tpublic getPersistenceBackend(): PersistenceBackend | null {\n\t\treturn this._persistence;\n\t}\n\n\t/** Sets the event emitter for persistence error events (post-construction wiring). */\n\tpublic setEventEmitter(emitter: PersistenceEventEmitter): void {\n\t\tthis._eventEmitter = emitter;\n\t\tthis._persistenceBuffer?.setEventEmitter(emitter);\n\t}\n\n\t/** Stops timers and flushes any remaining buffered writes. */\n\tpublic async shutdown(): Promise<void> {\n\t\tthis._stopFlushTimer();\n\t\tthis._sessionManager.stopCleanupTimer();\n\t\tawait this._flushBuffer();\n\t}\n\n\t/** Total write buffer length across all sessions. */\n\tpublic getWriteBufferLength(): number {\n\t\tlet total = 0;\n\t\tfor (const session of this._sessions.values()) {\n\t\t\ttotal += session.writeBuffer.length;\n\t\t}\n\t\treturn total;\n\t}\n}\n"],"names":["ABSOLUTE_MAX_HISTORY_SIZE","HistoryManager","Map","config","NullLogger","requestedMaxSize","Math","EdgeEmitter","SessionManager","PersistenceBuffer","message","meta","sessionId","key","session","undefined","Date","Set","thought","targetNumber","t","branchThoughts","Object","branchId","err","getErrorMessage","branchCount","branchesToRemove","removed","opts","history","sid","policy","DehydrationPolicy","ids","id","Array","ValidationError","isHealthy","globalSession","branchIds","branchData","edgeSessions","totalEdges","edges","edge","edgeErr","edgeError","error","emitter","total"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core/HistoryManager.js","sources":["../../src/core/HistoryManager.ts"],"sourcesContent":["/**\n * History and branch management for sequential thinking.\n *\n * This module provides the `HistoryManager` class which manages thought history,\n * branching, and optional persistence with per-session state isolation.\n *\n * Internally delegates to three focused collaborators:\n * - `EdgeEmitter` — DAG edge emission\n * - `PersistenceBuffer` — buffered persistence + retry/backoff\n * - `SessionManager` — session lifecycle (TTL/LRU eviction)\n *\n * @module HistoryManager\n */\n\nimport type { IMetrics } from '../contracts/interfaces.js';\nimport type { IEdgeStore } from '../contracts/interfaces.js';\nimport type { ISummaryStore } from '../contracts/summary.js';\nimport { ValidationError, SessionAccessDeniedError, getErrorMessage } from '../errors.js';\nimport { NullLogger } from '../logger/NullLogger.js';\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport type { PersistenceBackend } from '../contracts/PersistenceBackend.js';\nimport {\n\tDehydrationPolicy,\n\ttype DehydrationOptions,\n\ttype HydratedEntry,\n} from './compression/DehydrationPolicy.js';\nimport { EdgeEmitter } from './graph/EdgeEmitter.js';\nimport type { IHistoryManager } from './IHistoryManager.js';\nimport { PersistenceBuffer, type PersistenceEventEmitter } from './PersistenceBuffer.js';\nimport { SessionManager } from './SessionManager.js';\nimport type { ThoughtData } from './thought.js';\nimport { getOwner } from '../context/RequestContext.js';\n\n\n/** Absolute maximum history size (~20MB at 2KB/thought). Cannot be overridden. */\nexport const ABSOLUTE_MAX_HISTORY_SIZE = 10_000;\n\ninterface SessionState {\n\tthought_history: ThoughtData[];\n\tbranches: Record<string, ThoughtData[]>;\n\tavailableMcpTools: string[] | undefined;\n\tavailableSkills: string[] | undefined;\n\twriteBuffer: ThoughtData[];\n\tlastAccessedAt: number;\n\tregisteredBranches: Set<string>;\n\t/** Owner identifier set on first owner-aware access. Immutable thereafter. */\n\towner?: string;\n}\n\nexport interface HistoryManagerConfig {\n\t/** Maximum number of thoughts to keep in main history. @default 1000 */\n\tmaxHistorySize?: number;\n\t/** Maximum number of branches to maintain. @default 50 */\n\tmaxBranches?: number;\n\t/** Maximum size of each branch. @default 100 */\n\tmaxBranchSize?: number;\n\tlogger?: Logger;\n\tpersistence?: PersistenceBackend | null;\n\tmetrics?: IMetrics;\n\t/** Maximum number of thoughts to buffer before flushing. @default 100 */\n\tpersistenceBufferSize?: number;\n\t/** Periodic flush interval in ms. @default 1000 */\n\tpersistenceFlushInterval?: number;\n\t/** Max retries for failed persistence flushes. @default 3 */\n\tpersistenceMaxRetries?: number;\n\teventEmitter?: PersistenceEventEmitter;\n\tedgeStore?: IEdgeStore;\n\tsummaryStore?: ISummaryStore;\n\t/** Whether to emit DAG edges (gated independently of edgeStore). @default false */\n\tdagEdges?: boolean;\n\t/** Maximum sessions per owner (per-owner LRU bucket). @default 50 */\n\tmaxSessionsPerOwner?: number;\n}\n\n/**\n * Manages thought history and branching for sequential thinking.\n *\n * Owns the per-session `Map<string, SessionState>`. Delegates DAG edge emission,\n * buffered persistence, and session TTL/LRU eviction to focused collaborators while\n * preserving test-coupled private member names (`_flushTimer`, `_startFlushTimer`,\n * `_flushBuffer`, `_sessions`).\n */\nexport class HistoryManager implements IHistoryManager {\n\tprivate static readonly DEFAULT_SESSION = '__global__';\n\tprivate static readonly SESSION_TTL_MS = 30 * 60 * 1000;\n\tprivate static readonly MAX_SESSIONS = 100;\n\tprivate _sessions: Map<string, SessionState> = new Map();\n\tprivate _maxHistorySize: number;\n\tprivate _maxBranches: number;\n\tprivate _maxBranchSize: number;\n\tprivate _logger: Logger;\n\tprivate _persistence: PersistenceBackend | null;\n\tprivate _persistenceEnabled: boolean;\n\tprivate _metrics?: IMetrics;\n\n\tprivate _edgeStore?: IEdgeStore;\n\tprivate _summaryStore?: ISummaryStore;\n\tprivate _dagEdges: boolean;\n\n\tprivate _eventEmitter: PersistenceEventEmitter | null;\n\n\tprivate readonly _edgeEmitter: EdgeEmitter;\n\tprivate _persistenceBuffer: PersistenceBuffer<SessionState> | null;\n\tprivate readonly _sessionManager: SessionManager<SessionState>;\n\n\tconstructor(config: HistoryManagerConfig = {}) {\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t\tconst requestedMaxSize = config.maxHistorySize ?? 10000;\n\t\tthis._maxHistorySize = Math.min(requestedMaxSize, ABSOLUTE_MAX_HISTORY_SIZE);\n\t\tif (requestedMaxSize > ABSOLUTE_MAX_HISTORY_SIZE) {\n\t\t\tthis._logger.warn('maxHistorySize exceeds absolute maximum, capped', {\n\t\t\t\trequested: requestedMaxSize,\n\t\t\t\tapplied: ABSOLUTE_MAX_HISTORY_SIZE,\n\t\t\t});\n\t\t}\n\t\tthis._maxBranches = config.maxBranches || 50;\n\t\tthis._maxBranchSize = config.maxBranchSize || 100;\n\t\tthis._persistence = config.persistence ?? null;\n\t\tthis._persistenceEnabled = this._persistence !== null;\n\t\tthis._metrics = config.metrics;\n\t\tthis._eventEmitter = config.eventEmitter ?? null;\n\t\tthis._edgeStore = config.edgeStore;\n\t\tthis._summaryStore = config.summaryStore;\n\t\tthis._dagEdges = config.dagEdges ?? true;\n\n\t\t// Wire delegates\n\t\tthis._edgeEmitter = new EdgeEmitter({\n\t\t\tedgeStore: this._edgeStore,\n\t\t\tdagEdges: this._dagEdges,\n\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\tlogger: this._logger,\n\t\t});\n\n\t\tthis._sessionManager = new SessionManager<SessionState>({\n\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\tsessionTtlMs: HistoryManager.SESSION_TTL_MS,\n\t\t\tcleanupIntervalMs: 5 * 60 * 1000,\n\t\t\tgetMaxSessions: () => HistoryManager.MAX_SESSIONS,\n\t\t\tmaxSessionsPerOwner: config.maxSessionsPerOwner ?? 50,\n\t\t\tlogger: this._logger,\n\t\t});\n\n\t\tthis._persistenceBuffer = null;\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistenceBuffer = new PersistenceBuffer<SessionState>({\n\t\t\t\tpersistence: this._persistence,\n\t\t\t\tbufferSize: config.persistenceBufferSize ?? 100,\n\t\t\t\tflushInterval: config.persistenceFlushInterval ?? 1000,\n\t\t\t\tmaxRetries: config.persistenceMaxRetries ?? 3,\n\t\t\t\tdefaultSessionId: HistoryManager.DEFAULT_SESSION,\n\t\t\t\tgetSessions: () => this._sessions,\n\t\t\t\tgetDefaultSession: () => this._getSession(),\n\t\t\t\tedgeStore: this._edgeStore,\n\t\t\t\teventEmitter: this._eventEmitter,\n\t\t\t\tlogger: this._logger,\n\t\t\t});\n\t\t\tthis._startFlushTimer();\n\t\t}\n\n\t\tthis._sessionManager.startCleanupTimer(this._sessions);\n\t}\n\n\t// Test-coupled accessors: these private member names must remain reachable\n\t// via `manager as unknown as { _flushTimer; _startFlushTimer }`.\n\tprivate get _flushTimer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._persistenceBuffer?.timer ?? null;\n\t}\n\n\tprivate _startFlushTimer(): void {\n\t\tthis._persistenceBuffer?.startFlushTimer();\n\t}\n\n\tprivate _stopFlushTimer(): void {\n\t\tif (this._flushTimer === null) return;\n\t\tthis._persistenceBuffer?.stopFlushTimer();\n\t}\n\n\t/** @internal Public for backward-compatible test coupling. */\n\tpublic async _flushBuffer(): Promise<void> {\n\t\tawait this._persistenceBuffer?.flush();\n\t}\n\n\t/** EdgeStore instance, if configured. Used by ThoughtProcessor for StrategyContext. */\n\tpublic getEdgeStore(): IEdgeStore | undefined {\n\t\treturn this._edgeStore;\n\t}\n\n\tprivate log(message: string, meta?: Record<string, unknown>): void {\n\t\tthis._logger.info(message, meta);\n\t}\n\n\t/** Reads owner from RequestContext (AsyncLocalStorage). Stdio path returns undefined. */\n\tprivate _getCurrentOwner(): string | undefined {\n\t\treturn getOwner();\n\t}\n\n\t/**\n\t * Gets or creates session state; updates lastAccessedAt.\n\t *\n\t * Ownership semantics:\n\t * - `owner === undefined` (stdio path): never rejects, never sets owner.\n\t * - `owner !== undefined`: if session has a different owner, throws\n\t * `SessionAccessDeniedError`. If session was created without an owner\n\t * (e.g. by stdio), the owner is set on first owner-aware access.\n\t */\n\tprivate _getSession(sessionId?: string, owner?: string): SessionState {\n\t\tconst key = sessionId ?? HistoryManager.DEFAULT_SESSION;\n\t\tlet session = this._sessions.get(key);\n\t\tif (!session) {\n\t\t\tsession = {\n\t\t\t\tthought_history: [],\n\t\t\t\tbranches: {},\n\t\t\t\tavailableMcpTools: undefined,\n\t\t\t\tavailableSkills: undefined,\n\t\t\t\twriteBuffer: [],\n\t\t\t\tlastAccessedAt: Date.now(),\n\t\t\t\tregisteredBranches: new Set<string>(),\n\t\t\t\towner,\n\t\t\t};\n\t\t\tthis._sessions.set(key, session);\n\t\t\tthis._sessionManager.evictExcessSessions(this._sessions);\n\t\t} else if (owner !== undefined) {\n\t\t\tif (session.owner !== undefined && session.owner !== owner) {\n\t\t\t\tthrow new SessionAccessDeniedError(key, session.owner, owner);\n\t\t\t}\n\t\t\tif (session.owner === undefined) {\n\t\t\t\t// First owner-aware access: bind owner. Acceptable promotion path\n\t\t\t\t// for sessions created by stdio that later receive an owner-bearing\n\t\t\t\t// access (single-user transition).\n\t\t\t\tsession.owner = owner;\n\t\t\t}\n\t\t}\n\t\tsession.lastAccessedAt = Date.now();\n\t\treturn session;\n\t}\n\n\t/**\n\t * Adds a thought to the history. Routes per-session, applies retraction for backtrack,\n\t * caches tools/skills, trims, branches, emits DAG edges, and buffers for persistence.\n\t */\n\tpublic addThought(thought: ThoughtData): void {\n\t\tconst session = this._getSession(thought.session_id, this._getCurrentOwner());\n\t\tthis._metrics?.counter(\n\t\t\t'thought_requests_total',\n\t\t\t1,\n\t\t\t{},\n\t\t\t'Total thought requests added to history'\n\t\t);\n\n\t\tsession.thought_history.push(thought);\n\n\t\t// Logical retraction: when a backtrack thought is added, mark its target\n\t\t// as retracted (append-only — target remains in history).\n\t\tif (thought.thought_type === 'backtrack' && thought.backtrack_target !== undefined) {\n\t\t\tthis._applyRetraction(session, thought.backtrack_target);\n\t\t}\n\n\t\t// Cache available_mcp_tools/available_skills for cross-call persistence\n\t\tif (thought.available_mcp_tools) {\n\t\t\tsession.availableMcpTools = thought.available_mcp_tools;\n\t\t}\n\t\tif (thought.available_skills) {\n\t\t\tsession.availableSkills = thought.available_skills;\n\t\t}\n\n\t\tif (session.thought_history.length > this._maxHistorySize) {\n\t\t\tsession.thought_history = session.thought_history.slice(-this._maxHistorySize);\n\t\t\tthis.log(`History trimmed to ${this._maxHistorySize} items`, {\n\t\t\t\tmaxSize: this._maxHistorySize,\n\t\t\t});\n\t\t}\n\n\t\tif (thought.branch_from_thought && thought.branch_id) {\n\t\t\tthis._addToSessionBranch(session, thought.branch_id, thought);\n\t\t}\n\n\t\t// Track merge operations for analytics\n\t\tif (thought.merge_from_thoughts?.length || thought.merge_branch_ids?.length) {\n\t\t\tthis._metrics?.counter(\n\t\t\t\t'thought_merge_operations_total',\n\t\t\t\t1,\n\t\t\t\t{},\n\t\t\t\t'Total merge operations (graph topology)'\n\t\t\t);\n\t\t}\n\n\t\t// Emit DAG edges (no-op unless edgeStore + dagEdges flag both enabled)\n\t\tthis._edgeEmitter.emitEdgesForThought(session, thought);\n\n\t\t// Buffer thought for persistence (no-op when persistence disabled)\n\t\tif (this._persistenceBuffer) {\n\t\t\tthis._persistenceBuffer.bufferThought(session, thought);\n\t\t}\n\t}\n\n\t/** Marks the thought as retracted within the session (append-only). */\n\tprivate _applyRetraction(session: SessionState, targetNumber: number): void {\n\t\tfor (const t of session.thought_history) {\n\t\t\tif (t.thought_number === targetNumber) {\n\t\t\t\tt.retracted = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tfor (const branchThoughts of Object.values(session.branches)) {\n\t\t\tfor (const t of branchThoughts) {\n\t\t\t\tif (t.thought_number === targetNumber) {\n\t\t\t\t\tt.retracted = true;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate _addToSessionBranch(session: SessionState, branchId: string, thought: ThoughtData): void {\n\t\tif (!session.branches[branchId]) {\n\t\t\tsession.branches[branchId] = [];\n\t\t}\n\t\tthis._trimSessionBranchSize(session, branchId);\n\t\tsession.branches[branchId].push(thought);\n\n\t\tif (Object.keys(session.branches).length > this._maxBranches) {\n\t\t\tthis._cleanupSessionBranches(session);\n\t\t}\n\n\t\t// Persist branch to backend if enabled\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistence.saveBranch(branchId, session.branches[branchId]).catch((err) => {\n\t\t\t\tthis.log('Failed to persist branch', {\n\t\t\t\t\tbranchId,\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate _cleanupSessionBranches(session: SessionState): void {\n\t\tconst branchCount = Object.keys(session.branches).length;\n\t\tif (branchCount > this._maxBranches) {\n\t\t\tconst branchesToRemove = Object.keys(session.branches).slice(\n\t\t\t\t0,\n\t\t\t\tbranchCount - this._maxBranches\n\t\t\t);\n\t\t\tfor (const branchId of branchesToRemove) {\n\t\t\t\tdelete session.branches[branchId];\n\t\t\t\tthis.log(`Removed old branch: ${branchId}`, { branchId });\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate _trimSessionBranchSize(session: SessionState, branchId: string): void {\n\t\tif ((session.branches[branchId] ?? []).length > this._maxBranchSize) {\n\t\t\tconst removed = session.branches[branchId]!.length - this._maxBranchSize;\n\t\t\tsession.branches[branchId] = session.branches[branchId]!.slice(-this._maxBranchSize);\n\t\t\tthis.log(`Trimmed branch '${branchId}': removed ${removed} old thoughts`, {\n\t\t\t\tbranchId,\n\t\t\t\tremoved,\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic getHistory(sessionId?: string): ThoughtData[] {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).thought_history;\n\t}\n\n\t/**\n\t * Returns history with optional sliding-window dehydration. Non-mutating: when\n\t * `dagEdges` is off OR no `ISummaryStore` is configured, returns same as getHistory.\n\t */\n\tpublic getHistoryHydrated(\n\t\tsessionId?: string,\n\t\topts?: DehydrationOptions\n\t): HydratedEntry[] {\n\t\tconst history = this.getHistory(sessionId);\n\t\tif (!this._dagEdges || !this._summaryStore) {\n\t\t\treturn history.slice();\n\t\t}\n\t\tconst sid = sessionId ?? HistoryManager.DEFAULT_SESSION;\n\t\tconst policy = new DehydrationPolicy(this._summaryStore);\n\t\treturn policy.apply(history, sid, opts);\n\t}\n\n\tpublic getHistoryLength(sessionId?: string): number {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).thought_history.length;\n\t}\n\n\tpublic getBranches(sessionId?: string): Record<string, ThoughtData[]> {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).branches;\n\t}\n\n\tpublic getBranchIds(sessionId?: string): string[] {\n\t\tconst session = this._getSession(sessionId, this._getCurrentOwner());\n\t\tconst ids = new Set<string>(Object.keys(session.branches));\n\t\tfor (const id of session.registeredBranches) ids.add(id);\n\t\treturn Array.from(ids);\n\t}\n\n\t/** @throws {ValidationError} If branchId is empty or already exists. */\n\tpublic registerBranch(sessionId: string | undefined, branchId: string): void {\n\t\tif (typeof branchId !== 'string' || branchId.length === 0) {\n\t\t\tthrow new ValidationError('branch_id', 'branch_id must be a non-empty string');\n\t\t}\n\t\tconst session = this._getSession(sessionId, this._getCurrentOwner());\n\t\tif (branchId in session.branches || session.registeredBranches.has(branchId)) {\n\t\t\tthrow new ValidationError('branch_id', `Branch already exists: ${branchId}`);\n\t\t}\n\t\tsession.registeredBranches.add(branchId);\n\t\tthis.log('Registered branch', { branchId, sessionId: sessionId ?? null });\n\t}\n\n\tpublic branchExists(sessionId: string | undefined, branchId: string): boolean {\n\t\tconst session = this._getSession(sessionId, this._getCurrentOwner());\n\t\treturn branchId in session.branches || session.registeredBranches.has(branchId);\n\t}\n\n\tpublic getAvailableMcpTools(sessionId?: string): string[] | undefined {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).availableMcpTools;\n\t}\n\n\tpublic getAvailableSkills(sessionId?: string): string[] | undefined {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).availableSkills;\n\t}\n\n\tpublic getBranch(branchId: string, sessionId?: string): ThoughtData[] | undefined {\n\t\treturn this._getSession(sessionId, this._getCurrentOwner()).branches[branchId];\n\t}\n\n\t/** Clears history and branches. If sessionId provided, clears only that session. */\n\tpublic clear(sessionId?: string): void {\n\t\t// Clear edges from EdgeStore (before session map mutation so keys are still available)\n\t\tif (this._edgeStore) {\n\t\t\tif (sessionId !== undefined) {\n\t\t\t\tthis._edgeStore.clearSession(sessionId);\n\t\t\t} else {\n\t\t\t\tfor (const sid of this._sessions.keys()) {\n\t\t\t\t\tthis._edgeStore.clearSession(sid);\n\t\t\t\t}\n\t\t\t\t// Also clear the default session in case no session entries exist yet\n\t\t\t\tthis._edgeStore.clearSession(HistoryManager.DEFAULT_SESSION);\n\t\t\t}\n\t\t}\n\n\t\tif (sessionId !== undefined) {\n\t\t\tthis._sessions.delete(sessionId);\n\t\t\tthis.log('Session cleared', { sessionId });\n\t\t} else {\n\t\t\tthis._sessions.clear();\n\t\t\tthis.log('History cleared (all sessions)');\n\t\t}\n\n\t\t// Clear persisted data if enabled\n\t\tif (this._persistenceEnabled && this._persistence) {\n\t\t\tthis._persistence.clear().catch((err) => {\n\t\t\t\tthis.log('Failed to clear persisted data', {\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic clearSession(sessionId: string): void {\n\t\tthis.clear(sessionId);\n\t}\n\n\tpublic getSessionIds(): string[] {\n\t\treturn Array.from(this._sessions.keys());\n\t}\n\n\tpublic getSessionCount(): number {\n\t\treturn this._sessions.size;\n\t}\n\n\t/** Loads history from persistence into the global session. Call at init. */\n\tpublic async loadFromPersistence(): Promise<void> {\n\t\tif (!this._persistenceEnabled || !this._persistence) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst isHealthy = await this._persistence.healthy();\n\t\t\tif (!isHealthy) {\n\t\t\t\tthis.log('Persistence backend not healthy, skipping load');\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst globalSession = this._getSession();\n\n\t\t\tconst history = await this._persistence.loadHistory();\n\t\t\tif (history.length > 0) {\n\t\t\t\tglobalSession.thought_history = history.slice(-this._maxHistorySize);\n\t\t\t\tthis.log(`Loaded ${globalSession.thought_history.length} thoughts from persistence`);\n\t\t\t}\n\n\t\t\tconst branchIds = await this._persistence.listBranches();\n\t\t\tfor (const branchId of branchIds) {\n\t\t\t\tconst branchData = await this._persistence.loadBranch(branchId);\n\t\t\t\tif (branchData) {\n\t\t\t\t\tglobalSession.branches[branchId] = branchData.slice(-this._maxBranchSize);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.log(`Loaded ${Object.keys(globalSession.branches).length} branches from persistence`);\n\n\t\t\t// Load edges if EdgeStore is configured — restore for ALL persisted sessions\n\t\t\tif (this._edgeStore) {\n\t\t\t\ttry {\n\t\t\t\t\tconst edgeSessions = await this._persistence.listEdgeSessions();\n\t\t\t\t\tlet totalEdges = 0;\n\t\t\t\t\tfor (const sessionId of edgeSessions) {\n\t\t\t\t\t\tconst edges = await this._persistence.loadEdges(sessionId);\n\t\t\t\t\t\tfor (const edge of edges) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis._edgeStore.addEdge(edge);\n\t\t\t\t\t\t\t\ttotalEdges++;\n\t\t\t\t\t\t\t} catch (edgeErr) {\n\t\t\t\t\t\t\t\tthis.log('Failed to restore edge', {\n\t\t\t\t\t\t\t\t\tedgeId: edge.id,\n\t\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\t\terror: getErrorMessage(edgeErr),\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis.log(\n\t\t\t\t\t\t`Loaded ${totalEdges} edges across ${edgeSessions.length} sessions from persistence`,\n\t\t\t\t\t);\n\t\t\t\t} catch (edgeError) {\n\t\t\t\t\tthis.log('Failed to load edges from persistence', {\n\t\t\t\t\t\terror: getErrorMessage(edgeError),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.log('Failed to load from persistence', {\n\t\t\t\terror: getErrorMessage(error),\n\t\t\t});\n\t\t}\n\t}\n\n\tpublic isPersistenceEnabled(): boolean {\n\t\treturn this._persistenceEnabled;\n\t}\n\n\tpublic getPersistenceBackend(): PersistenceBackend | null {\n\t\treturn this._persistence;\n\t}\n\n\t/** Sets the event emitter for persistence error events (post-construction wiring). */\n\tpublic setEventEmitter(emitter: PersistenceEventEmitter): void {\n\t\tthis._eventEmitter = emitter;\n\t\tthis._persistenceBuffer?.setEventEmitter(emitter);\n\t}\n\n\t/** Stops timers and flushes any remaining buffered writes. */\n\tpublic async shutdown(): Promise<void> {\n\t\tthis._stopFlushTimer();\n\t\tthis._sessionManager.stopCleanupTimer();\n\t\tawait this._flushBuffer();\n\t}\n\n\t/** Total write buffer length across all sessions. */\n\tpublic getWriteBufferLength(): number {\n\t\tlet total = 0;\n\t\tfor (const session of this._sessions.values()) {\n\t\t\ttotal += session.writeBuffer.length;\n\t\t}\n\t\treturn total;\n\t}\n}\n"],"names":["ABSOLUTE_MAX_HISTORY_SIZE","HistoryManager","Map","config","NullLogger","requestedMaxSize","Math","EdgeEmitter","SessionManager","PersistenceBuffer","message","meta","getOwner","sessionId","owner","key","session","undefined","SessionAccessDeniedError","Date","Set","thought","targetNumber","t","branchThoughts","Object","branchId","err","getErrorMessage","branchCount","branchesToRemove","removed","opts","history","sid","policy","DehydrationPolicy","ids","id","Array","ValidationError","isHealthy","globalSession","branchIds","branchData","edgeSessions","totalEdges","edges","edge","edgeErr","edgeError","error","emitter","total"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InputNormalizer.d.ts","sourceRoot":"","sources":["../../src/core/InputNormalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAchD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAuBzD;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQvE;
|
|
1
|
+
{"version":3,"file":"InputNormalizer.d.ts","sourceRoot":"","sources":["../../src/core/InputNormalizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAchD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAuBzD;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQvE;AAmID;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAsD7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAsD1D"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ValidationError } from "../errors.js";
|
|
2
|
-
import { sanitizeString } from "../sanitize.js";
|
|
2
|
+
import { sanitizeRationale, sanitizeString, sanitizeSuggestedInputs } from "../sanitize.js";
|
|
3
3
|
import { MAX_SESSION_ID_LENGTH, SESSION_ID_PATTERN } from "./ids.js";
|
|
4
4
|
import { asSessionId, generateThoughtId } from "../contracts/ids.js";
|
|
5
5
|
const DEFAULT_RECOMMENDATION_CONFIDENCE = 0.5;
|
|
@@ -35,7 +35,10 @@ function normalizeRecommendation(rec) {
|
|
|
35
35
|
};
|
|
36
36
|
if (!('confidence' in normalized) || void 0 === normalized.confidence) normalized.confidence = DEFAULT_RECOMMENDATION_CONFIDENCE;
|
|
37
37
|
if (!('priority' in normalized) || void 0 === normalized.priority) normalized.priority = DEFAULT_RECOMMENDATION_PRIORITY;
|
|
38
|
-
if (
|
|
38
|
+
if ('rationale' in normalized && void 0 !== normalized.rationale) {
|
|
39
|
+
if ('string' == typeof normalized.rationale) normalized.rationale = sanitizeRationale(normalized.rationale);
|
|
40
|
+
} else normalized.rationale = DEFAULT_RECOMMENDATION_RATIONALE;
|
|
41
|
+
if (normalized.suggested_inputs && 'object' == typeof normalized.suggested_inputs && !Array.isArray(normalized.suggested_inputs)) normalized.suggested_inputs = sanitizeSuggestedInputs(normalized.suggested_inputs);
|
|
39
42
|
return normalized;
|
|
40
43
|
}
|
|
41
44
|
function normalizeStepRecommendation(step, lenient) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core/InputNormalizer.js","sources":["../../src/core/InputNormalizer.ts"],"sourcesContent":["/**\n * Input normalization for common LLM field name mistakes.\n *\n * This module provides normalization logic to handle common mistakes\n * that LLMs make when generating field names, such as using singular\n * instead of plural forms (e.g., `recommended_tool` vs `recommended_tools`).\n *\n * It also fills in sensible defaults for missing fields in `previous_steps`,\n * which LLMs naturally provide as partial/skeletal data (historical context).\n *\n * @module processor\n */\n\nimport { ValidationError } from '../errors.js';\nimport { sanitizeString } from '../sanitize.js';\nimport type { ThoughtData } from './thought.js';\nimport { SESSION_ID_PATTERN, MAX_SESSION_ID_LENGTH } from './ids.js';\nimport { asSessionId, generateThoughtId } from '../contracts/ids.js';\n\n/**\n * Default values for missing partial recommendation fields.\n *\n * Shared between tool and skill recommendations (identical defaults).\n */\nconst DEFAULT_RECOMMENDATION_CONFIDENCE = 0.5;\nconst DEFAULT_RECOMMENDATION_PRIORITY = 999;\nconst DEFAULT_RECOMMENDATION_RATIONALE = '';\nconst DEFAULT_STEP_OUTCOME = '';\n\n/**\n * Recursively sanitizes all string values within an unknown structure.\n * Walks into plain objects and arrays to reach deeply nested strings.\n * Non-plain objects (Date, RegExp, etc.) are returned as-is.\n *\n * @param value - The value to sanitize recursively\n * @returns The sanitized value with all nested strings cleaned\n *\n * @example\n * ```typescript\n * sanitizeRecursive('<script>x</script>'); // 'x'\n * sanitizeRecursive({ a: { b: '<iframe>y' } }); // { a: { b: 'y' } }\n * sanitizeRecursive(['a\\x00b', 42]); // ['ab', 42]\n * ```\n */\nexport function sanitizeRecursive(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn value;\n\t}\n\tif (typeof value === 'string') {\n\t\treturn sanitizeString(value);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((item) => sanitizeRecursive(item));\n\t}\n\tif (typeof value === 'object') {\n\t\t// Only recurse into plain objects — skip Date, RegExp, Map, Set, etc.\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\tif (proto !== Object.prototype && proto !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n\t\t\tresult[key] = sanitizeRecursive(val);\n\t\t}\n\t\treturn result;\n\t}\n\treturn value;\n}\n\n/**\n * Valid branch ID pattern: alphanumeric, hyphens, underscores only.\n * Prevents path traversal attacks by rejecting special characters like / . \\ etc.\n */\nconst BRANCH_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;\n\n\n\n/**\n * Sanitizes and validates a branch ID to prevent path traversal attacks.\n *\n * @param branchId - The branch ID to sanitize\n * @returns The sanitized branch ID\n * @throws ValidationError if the branch ID contains invalid characters\n *\n * @example\n * ```typescript\n * sanitizeBranchId('my-branch_01'); // 'my-branch_01'\n * sanitizeBranchId('../etc/passwd'); // throws ValidationError\n * ```\n */\nexport function sanitizeBranchId(branchId: string): string {\n\t// Validate format\n\tif (!BRANCH_ID_PATTERN.test(branchId)) {\n\t\tthrow new ValidationError(\n\t\t\t'branch_id',\n\t\t\t'Invalid format - must be 1-64 alphanumeric characters, hyphens, or underscores only'\n\t\t);\n\t}\n\treturn branchId;\n}\n\n/**\n * Sanitizes and validates a session ID.\n *\n * @param sessionId - The session ID to sanitize\n * @returns The sanitized session ID, or undefined if invalid after sanitization\n *\n * @example\n * ```typescript\n * sanitizeSessionId('analysis-task-42'); // 'analysis-task-42'\n * sanitizeSessionId('bad session!'); // undefined (stripped)\n * ```\n */\nexport function sanitizeSessionId(sessionId: string): string | undefined {\n\t// First sanitize control characters\n\tconst cleaned = sanitizeString(sessionId);\n\t// Validate format after sanitization\n\tif (cleaned.length > MAX_SESSION_ID_LENGTH || !SESSION_ID_PATTERN.test(cleaned)) {\n\t\treturn undefined;\n\t}\n\treturn cleaned;\n}\n\n\n/**\n * Normalizes a recommendation object (tool or skill) with default values.\n *\n * Fills in sensible defaults for missing optional fields:\n * - `confidence`: 0.5\n * - `priority`: 999\n * - `rationale`: empty string\n *\n * @param rec - The recommendation object to normalize\n * @returns The normalized recommendation with defaults filled in\n *\n * @example\n * ```typescript\n * const input = { tool_name: 'Read', rationale: 'Read the file' };\n * const normalized = normalizeRecommendation(input);\n * // { tool_name: 'Read', rationale: 'Read the file', confidence: 0.5, priority: 999 }\n * ```\n */\nfunction normalizeRecommendation(rec: Record<string, unknown>): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...rec };\n\n\t// Fill in default confidence if missing\n\tif (!('confidence' in normalized) || normalized.confidence === undefined) {\n\t\tnormalized.confidence = DEFAULT_RECOMMENDATION_CONFIDENCE;\n\t}\n\n\t// Fill in default priority if missing\n\tif (!('priority' in normalized) || normalized.priority === undefined) {\n\t\tnormalized.priority = DEFAULT_RECOMMENDATION_PRIORITY;\n\t}\n\n\t// Fill in default rationale if missing\n\tif (!('rationale' in normalized) || normalized.rationale === undefined) {\n\t\tnormalized.rationale = DEFAULT_RECOMMENDATION_RATIONALE;\n\t}\n\n\treturn normalized;\n}\n/**\n * Normalizes step recommendation objects.\n *\n * Handles common field name mistakes:\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n *\n * Also normalizes tool recommendations within the step to fill in defaults.\n *\n * @param step - The step recommendation to normalize\n * @param lenient - Whether to use lenient mode (fill in defaults for missing fields)\n * @returns The normalized step recommendation\n *\n * @example\n * ```typescript\n * // Strict mode (for current_step)\n * const input = {\n * step_description: 'Analyze data',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data analyzed'\n * };\n * const normalized = normalizeStepRecommendation(input, false);\n * // normalized.recommended_tools exists (plural form)\n *\n * // Lenient mode (for previous_steps)\n * const partialInput = {\n * step_description: 'Read file',\n * recommended_tools: [{ tool_name: 'Read', rationale: 'Read file' }]\n * };\n * const normalized = normalizeStepRecommendation(partialInput, true);\n * // confidence: 0.5, priority: 999, expected_outcome: '' filled in\n * ```\n */\nfunction normalizeStepRecommendation(\n\tstep: Record<string, unknown>,\n\tlenient: boolean\n): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...step };\n\n\t// Transform `recommended_tool` (singular) → `recommended_tools` (plural)\n\tif ('recommended_tool' in normalized && !('recommended_tools' in normalized)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tool;\n\t\tdelete normalized.recommended_tool;\n\t}\n\n\t// Transform `recommended_skill` (singular) → `recommended_skills` (plural)\n\tif ('recommended_skill' in normalized && !('recommended_skills' in normalized)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skill;\n\t\tdelete normalized.recommended_skill;\n\t}\n\n\t// Normalize recommended_tools array if present\n\tif (Array.isArray(normalized.recommended_tools)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tools.map((tool) =>\n\t\t\ttypeof tool === 'object' && tool !== null\n\t\t\t\t? normalizeRecommendation(tool as Record<string, unknown>)\n\t\t\t\t: tool\n\t\t);\n\t}\n\n\t// Normalize recommended_skills array if present\n\tif (Array.isArray(normalized.recommended_skills)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skills.map((skill) =>\n\t\t\ttypeof skill === 'object' && skill !== null\n\t\t\t\t? normalizeRecommendation(skill as Record<string, unknown>)\n\t\t\t\t: skill\n\t\t);\n\t}\n\t// In lenient mode, fill in default expected_outcome if missing\n\tif (lenient && !('expected_outcome' in normalized)) {\n\t\tnormalized.expected_outcome = DEFAULT_STEP_OUTCOME;\n\t}\n\n\treturn normalized;\n}\n\n\n/**\n * Normalizes reasoning-specific fields on a thought input object.\n * Always applies reasoning normalization — reasoning is the default pipeline.\n * Applies the following normalization rules:\n * - Defaults `thought_type` to `'regular'` if not provided\n * - Clamps `quality_score` to [0, 1] range\n * - Clamps `confidence` to [0, 1] range\n * - Sanitizes `hypothesis_id` using `sanitizeBranchId` pattern\n * - Filters `synthesis_sources` to positive integers only\n * - Filters `merge_from_thoughts` to positive integers only\n * - Sanitizes each entry in `merge_branch_ids`\n * - Defaults `reasoning_depth` to `'moderate'` for hypothesis/verification types\n *\n * @param input - The mutable normalized input object to apply reasoning defaults to\n *\n * @example\n * ```typescript\n * const input: Record<string, unknown> = { thought_type: 'hypothesis', quality_score: 1.5 };\n * normalizeReasoningFields(input);\n * // input.quality_score === 1, input.reasoning_depth === 'moderate'\n * ```\n */\nexport function normalizeReasoningFields(input: Record<string, unknown>): void {\n\t// Always apply reasoning field normalization — reasoning is the default pipeline\n\t// Default thought_type to 'regular'\n\n\tif (!('thought_type' in input) || input.thought_type === undefined) {\n\t\tinput.thought_type = 'regular';\n\t}\n\n\t// Clamp quality_score to [0, 1]\n\tif (typeof input.quality_score === 'number') {\n\t\tinput.quality_score = Math.max(0, Math.min(1, input.quality_score));\n\t}\n\n\t// Clamp confidence to [0, 1]\n\tif (typeof input.confidence === 'number') {\n\t\tinput.confidence = Math.max(0, Math.min(1, input.confidence));\n\t}\n\n\t// Sanitize hypothesis_id (same rules as branch_id)\n\tif (typeof input.hypothesis_id === 'string') {\n\t\tinput.hypothesis_id = sanitizeBranchId(input.hypothesis_id);\n\t}\n\n\t// Filter synthesis_sources to positive integers only\n\tif (Array.isArray(input.synthesis_sources)) {\n\t\tinput.synthesis_sources = input.synthesis_sources.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Filter merge_from_thoughts to positive integers only\n\tif (Array.isArray(input.merge_from_thoughts)) {\n\t\tinput.merge_from_thoughts = input.merge_from_thoughts.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Sanitize merge_branch_ids entries\n\tif (Array.isArray(input.merge_branch_ids)) {\n\t\tinput.merge_branch_ids = input.merge_branch_ids.map((id: unknown) => {\n\t\t\tif (typeof id === 'string') {\n\t\t\t\treturn sanitizeBranchId(id);\n\t\t\t}\n\t\t\treturn id;\n\t\t});\n\t}\n\n\t// Default reasoning_depth to 'moderate' for hypothesis/verification types\n\tif (\n\t\t(input.thought_type === 'hypothesis' || input.thought_type === 'verification') &&\n\t\t!('reasoning_depth' in input)\n\t) {\n\t\tinput.reasoning_depth = 'moderate';\n\t}\n}\n\n/**\n * Normalizes thought input data by fixing common LLM field name mistakes.\n *\n * This function handles cases where LLMs incorrectly use singular forms\n * of field names that should be plural. It applies normalization to both\n * `current_step` and `previous_steps` fields.\n *\n * The normalization is applied BEFORE schema validation, allowing the\n * strict Valibot schema to remain correct while still being tolerant\n * of common LLM mistakes.\n *\n * @param input - The raw thought input data to normalize\n * @returns Normalized thought data with correct field names\n *\n * @remarks\n * **Normalization Rules:**\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n * - Applied to `current_step` if present (strict mode)\n * - Applied to all items in `previous_steps` if present (lenient mode with defaults)\n *\n * **Design Rationale:**\n * LLMs sometimes use singular field names even when the schema explicitly\n * defines plural forms. Rather than forcing the LLM to be perfect (which\n * leads to cryptic validation errors), we normalize the input to handle\n * these common mistakes gracefully.\n *\n * Additionally, LLMs naturally provide complete data for `current_step`\n * but only partial/skeletal data for `previous_steps` (historical context).\n * The lenient mode for `previous_steps` fills in sensible defaults:\n * - `confidence`: 0.5 for missing tool recommendation confidence\n * - `priority`: 999 for missing tool recommendation priority\n * - `rationale`: empty string for missing tool recommendation rationale\n * - `expected_outcome`: empty string for missing step expected outcome\n *\n * @example\n * ```typescript\n * const input = {\n * thought: 'I need to analyze the data',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Read the data file',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data loaded'\n * },\n * previous_steps: [{\n * step_description: 'Previous step',\n * recommended_tools: [{ tool_name: 'Grep', rationale: 'Search code' }]\n * }]\n * };\n *\n * const normalized = normalizeInput(input);\n * // current_step: recommended_tools exists (plural form)\n * // previous_steps[0]: confidence=0.5, priority=999, expected_outcome='' filled in\n * ```\n */\nexport function normalizeInput(input: unknown): ThoughtData {\n\tif (typeof input !== 'object' || input === null) {\n\t\treturn input as ThoughtData;\n\t}\n\n\tconst normalized = { ...input } as Record<string, unknown>;\n\n\t// Normalize current_step if present (strict mode - no defaults)\n\tif (normalized.current_step && typeof normalized.current_step === 'object') {\n\t\tnormalized.current_step = normalizeStepRecommendation(\n\t\t\tnormalized.current_step as Record<string, unknown>,\n\t\t\tfalse // strict mode\n\t\t);\n\t}\n\n\n\t// Normalize all items in previous_steps if present (lenient mode - with defaults)\n\tif (Array.isArray(normalized.previous_steps) && normalized.previous_steps.length > 0) {\n\t\tnormalized.previous_steps = normalized.previous_steps.map((step) =>\n\t\t\ttypeof step === 'object' && step !== null\n\t\t\t\t? normalizeStepRecommendation(step as Record<string, unknown>, true) // lenient mode\n\t\t\t\t: step\n\t\t);\n\t}\n\n\t// Sanitize branch_id to prevent path traversal attacks\n\tif (typeof normalized.branch_id === 'string') {\n\t\tnormalized.branch_id = sanitizeBranchId(normalized.branch_id);\n\t}\n\n\t// Sanitize session_id (same pattern as branch_id but allows 1-100 chars)\n\tif (typeof normalized.session_id === 'string') {\n\t\tconst sanitized = sanitizeSessionId(normalized.session_id);\n\t\tif (sanitized === undefined) {\n\t\t\tdelete normalized.session_id;\n\t\t} else {\n\t\t\tnormalized.session_id = asSessionId(sanitized);\n\t\t}\n\t}\n\n\t// Auto-generate id if not provided (for DAG node identity)\n\tif (!normalized.id || typeof normalized.id !== 'string') {\n\t\tnormalized.id = generateThoughtId();\n\t}\n\n\t// Normalize reasoning fields\n\tnormalizeReasoningFields(normalized);\n\n\n\t// Sanitize all free-text string fields recursively (dangerous HTML tags + null bytes)\n\t// This was moved from schema transforms because v.transform() cannot be converted to JSON Schema\n\tconst sanitized = sanitizeRecursive(normalized);\n\n\treturn sanitized as ThoughtData;\n}\n"],"names":["DEFAULT_RECOMMENDATION_CONFIDENCE","DEFAULT_RECOMMENDATION_PRIORITY","DEFAULT_RECOMMENDATION_RATIONALE","DEFAULT_STEP_OUTCOME","sanitizeRecursive","value","sanitizeString","Array","item","proto","Object","result","key","val","BRANCH_ID_PATTERN","sanitizeBranchId","branchId","ValidationError","sanitizeSessionId","sessionId","cleaned","MAX_SESSION_ID_LENGTH","SESSION_ID_PATTERN","normalizeRecommendation","rec","normalized","undefined","normalizeStepRecommendation","step","lenient","tool","skill","normalizeReasoningFields","input","Math","v","Number","id","normalizeInput","sanitized","asSessionId","generateThoughtId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core/InputNormalizer.js","sources":["../../src/core/InputNormalizer.ts"],"sourcesContent":["/**\n * Input normalization for common LLM field name mistakes.\n *\n * This module provides normalization logic to handle common mistakes\n * that LLMs make when generating field names, such as using singular\n * instead of plural forms (e.g., `recommended_tool` vs `recommended_tools`).\n *\n * It also fills in sensible defaults for missing fields in `previous_steps`,\n * which LLMs naturally provide as partial/skeletal data (historical context).\n *\n * @module processor\n */\n\nimport { ValidationError } from '../errors.js';\nimport { sanitizeString, sanitizeRationale, sanitizeSuggestedInputs } from '../sanitize.js';\nimport type { ThoughtData } from './thought.js';\nimport { SESSION_ID_PATTERN, MAX_SESSION_ID_LENGTH } from './ids.js';\nimport { asSessionId, generateThoughtId } from '../contracts/ids.js';\n\n/**\n * Default values for missing partial recommendation fields.\n *\n * Shared between tool and skill recommendations (identical defaults).\n */\nconst DEFAULT_RECOMMENDATION_CONFIDENCE = 0.5;\nconst DEFAULT_RECOMMENDATION_PRIORITY = 999;\nconst DEFAULT_RECOMMENDATION_RATIONALE = '';\nconst DEFAULT_STEP_OUTCOME = '';\n\n/**\n * Recursively sanitizes all string values within an unknown structure.\n * Walks into plain objects and arrays to reach deeply nested strings.\n * Non-plain objects (Date, RegExp, etc.) are returned as-is.\n *\n * @param value - The value to sanitize recursively\n * @returns The sanitized value with all nested strings cleaned\n *\n * @example\n * ```typescript\n * sanitizeRecursive('<script>x</script>'); // 'x'\n * sanitizeRecursive({ a: { b: '<iframe>y' } }); // { a: { b: 'y' } }\n * sanitizeRecursive(['a\\x00b', 42]); // ['ab', 42]\n * ```\n */\nexport function sanitizeRecursive(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn value;\n\t}\n\tif (typeof value === 'string') {\n\t\treturn sanitizeString(value);\n\t}\n\tif (Array.isArray(value)) {\n\t\treturn value.map((item) => sanitizeRecursive(item));\n\t}\n\tif (typeof value === 'object') {\n\t\t// Only recurse into plain objects — skip Date, RegExp, Map, Set, etc.\n\t\tconst proto = Object.getPrototypeOf(value);\n\t\tif (proto !== Object.prototype && proto !== null) {\n\t\t\treturn value;\n\t\t}\n\t\tconst result: Record<string, unknown> = {};\n\t\tfor (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n\t\t\tresult[key] = sanitizeRecursive(val);\n\t\t}\n\t\treturn result;\n\t}\n\treturn value;\n}\n\n/**\n * Valid branch ID pattern: alphanumeric, hyphens, underscores only.\n * Prevents path traversal attacks by rejecting special characters like / . \\ etc.\n */\nconst BRANCH_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;\n\n\n\n/**\n * Sanitizes and validates a branch ID to prevent path traversal attacks.\n *\n * @param branchId - The branch ID to sanitize\n * @returns The sanitized branch ID\n * @throws ValidationError if the branch ID contains invalid characters\n *\n * @example\n * ```typescript\n * sanitizeBranchId('my-branch_01'); // 'my-branch_01'\n * sanitizeBranchId('../etc/passwd'); // throws ValidationError\n * ```\n */\nexport function sanitizeBranchId(branchId: string): string {\n\t// Validate format\n\tif (!BRANCH_ID_PATTERN.test(branchId)) {\n\t\tthrow new ValidationError(\n\t\t\t'branch_id',\n\t\t\t'Invalid format - must be 1-64 alphanumeric characters, hyphens, or underscores only'\n\t\t);\n\t}\n\treturn branchId;\n}\n\n/**\n * Sanitizes and validates a session ID.\n *\n * @param sessionId - The session ID to sanitize\n * @returns The sanitized session ID, or undefined if invalid after sanitization\n *\n * @example\n * ```typescript\n * sanitizeSessionId('analysis-task-42'); // 'analysis-task-42'\n * sanitizeSessionId('bad session!'); // undefined (stripped)\n * ```\n */\nexport function sanitizeSessionId(sessionId: string): string | undefined {\n\t// First sanitize control characters\n\tconst cleaned = sanitizeString(sessionId);\n\t// Validate format after sanitization\n\tif (cleaned.length > MAX_SESSION_ID_LENGTH || !SESSION_ID_PATTERN.test(cleaned)) {\n\t\treturn undefined;\n\t}\n\treturn cleaned;\n}\n\n\n/**\n * Normalizes a recommendation object (tool or skill) with default values.\n *\n * Fills in sensible defaults for missing optional fields:\n * - `confidence`: 0.5\n * - `priority`: 999\n * - `rationale`: empty string\n *\n * @param rec - The recommendation object to normalize\n * @returns The normalized recommendation with defaults filled in\n *\n * @example\n * ```typescript\n * const input = { tool_name: 'Read', rationale: 'Read the file' };\n * const normalized = normalizeRecommendation(input);\n * // { tool_name: 'Read', rationale: 'Read the file', confidence: 0.5, priority: 999 }\n * ```\n */\nfunction normalizeRecommendation(rec: Record<string, unknown>): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...rec };\n\n\t// Fill in default confidence if missing\n\tif (!('confidence' in normalized) || normalized.confidence === undefined) {\n\t\tnormalized.confidence = DEFAULT_RECOMMENDATION_CONFIDENCE;\n\t}\n\n\t// Fill in default priority if missing\n\tif (!('priority' in normalized) || normalized.priority === undefined) {\n\t\tnormalized.priority = DEFAULT_RECOMMENDATION_PRIORITY;\n\t}\n\n\t// Fill in default rationale if missing\n\t// Fill in default rationale if missing, otherwise sanitize urgency phrases + cap length\n\tif (!('rationale' in normalized) || normalized.rationale === undefined) {\n\t\tnormalized.rationale = DEFAULT_RECOMMENDATION_RATIONALE;\n\t} else if (typeof normalized.rationale === 'string') {\n\t\tnormalized.rationale = sanitizeRationale(normalized.rationale);\n\t}\n\t// Sanitize suggested_inputs: enforce flat primitives, key cap, value-length cap\n\tif (\n\t\tnormalized.suggested_inputs &&\n\t\ttypeof normalized.suggested_inputs === 'object' &&\n\t\t!Array.isArray(normalized.suggested_inputs)\n\t) {\n\t\tnormalized.suggested_inputs = sanitizeSuggestedInputs(\n\t\t\tnormalized.suggested_inputs as Record<string, unknown>,\n\t\t);\n\t}\n\n\treturn normalized;\n}\n/**\n * Normalizes step recommendation objects.\n *\n * Handles common field name mistakes:\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n *\n * Also normalizes tool recommendations within the step to fill in defaults.\n *\n * @param step - The step recommendation to normalize\n * @param lenient - Whether to use lenient mode (fill in defaults for missing fields)\n * @returns The normalized step recommendation\n *\n * @example\n * ```typescript\n * // Strict mode (for current_step)\n * const input = {\n * step_description: 'Analyze data',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data analyzed'\n * };\n * const normalized = normalizeStepRecommendation(input, false);\n * // normalized.recommended_tools exists (plural form)\n *\n * // Lenient mode (for previous_steps)\n * const partialInput = {\n * step_description: 'Read file',\n * recommended_tools: [{ tool_name: 'Read', rationale: 'Read file' }]\n * };\n * const normalized = normalizeStepRecommendation(partialInput, true);\n * // confidence: 0.5, priority: 999, expected_outcome: '' filled in\n * ```\n */\nfunction normalizeStepRecommendation(\n\tstep: Record<string, unknown>,\n\tlenient: boolean\n): Record<string, unknown> {\n\tconst normalized: Record<string, unknown> = { ...step };\n\n\t// Transform `recommended_tool` (singular) → `recommended_tools` (plural)\n\tif ('recommended_tool' in normalized && !('recommended_tools' in normalized)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tool;\n\t\tdelete normalized.recommended_tool;\n\t}\n\n\t// Transform `recommended_skill` (singular) → `recommended_skills` (plural)\n\tif ('recommended_skill' in normalized && !('recommended_skills' in normalized)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skill;\n\t\tdelete normalized.recommended_skill;\n\t}\n\n\t// Normalize recommended_tools array if present\n\tif (Array.isArray(normalized.recommended_tools)) {\n\t\tnormalized.recommended_tools = normalized.recommended_tools.map((tool) =>\n\t\t\ttypeof tool === 'object' && tool !== null\n\t\t\t\t? normalizeRecommendation(tool as Record<string, unknown>)\n\t\t\t\t: tool\n\t\t);\n\t}\n\n\t// Normalize recommended_skills array if present\n\tif (Array.isArray(normalized.recommended_skills)) {\n\t\tnormalized.recommended_skills = normalized.recommended_skills.map((skill) =>\n\t\t\ttypeof skill === 'object' && skill !== null\n\t\t\t\t? normalizeRecommendation(skill as Record<string, unknown>)\n\t\t\t\t: skill\n\t\t);\n\t}\n\t// In lenient mode, fill in default expected_outcome if missing\n\tif (lenient && !('expected_outcome' in normalized)) {\n\t\tnormalized.expected_outcome = DEFAULT_STEP_OUTCOME;\n\t}\n\n\treturn normalized;\n}\n\n\n/**\n * Normalizes reasoning-specific fields on a thought input object.\n * Always applies reasoning normalization — reasoning is the default pipeline.\n * Applies the following normalization rules:\n * - Defaults `thought_type` to `'regular'` if not provided\n * - Clamps `quality_score` to [0, 1] range\n * - Clamps `confidence` to [0, 1] range\n * - Sanitizes `hypothesis_id` using `sanitizeBranchId` pattern\n * - Filters `synthesis_sources` to positive integers only\n * - Filters `merge_from_thoughts` to positive integers only\n * - Sanitizes each entry in `merge_branch_ids`\n * - Defaults `reasoning_depth` to `'moderate'` for hypothesis/verification types\n *\n * @param input - The mutable normalized input object to apply reasoning defaults to\n *\n * @example\n * ```typescript\n * const input: Record<string, unknown> = { thought_type: 'hypothesis', quality_score: 1.5 };\n * normalizeReasoningFields(input);\n * // input.quality_score === 1, input.reasoning_depth === 'moderate'\n * ```\n */\nexport function normalizeReasoningFields(input: Record<string, unknown>): void {\n\t// Always apply reasoning field normalization — reasoning is the default pipeline\n\t// Default thought_type to 'regular'\n\n\tif (!('thought_type' in input) || input.thought_type === undefined) {\n\t\tinput.thought_type = 'regular';\n\t}\n\n\t// Clamp quality_score to [0, 1]\n\tif (typeof input.quality_score === 'number') {\n\t\tinput.quality_score = Math.max(0, Math.min(1, input.quality_score));\n\t}\n\n\t// Clamp confidence to [0, 1]\n\tif (typeof input.confidence === 'number') {\n\t\tinput.confidence = Math.max(0, Math.min(1, input.confidence));\n\t}\n\n\t// Sanitize hypothesis_id (same rules as branch_id)\n\tif (typeof input.hypothesis_id === 'string') {\n\t\tinput.hypothesis_id = sanitizeBranchId(input.hypothesis_id);\n\t}\n\n\t// Filter synthesis_sources to positive integers only\n\tif (Array.isArray(input.synthesis_sources)) {\n\t\tinput.synthesis_sources = input.synthesis_sources.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Filter merge_from_thoughts to positive integers only\n\tif (Array.isArray(input.merge_from_thoughts)) {\n\t\tinput.merge_from_thoughts = input.merge_from_thoughts.filter(\n\t\t\t(v: unknown) => typeof v === 'number' && Number.isInteger(v) && v > 0\n\t\t);\n\t}\n\n\t// Sanitize merge_branch_ids entries\n\tif (Array.isArray(input.merge_branch_ids)) {\n\t\tinput.merge_branch_ids = input.merge_branch_ids.map((id: unknown) => {\n\t\t\tif (typeof id === 'string') {\n\t\t\t\treturn sanitizeBranchId(id);\n\t\t\t}\n\t\t\treturn id;\n\t\t});\n\t}\n\n\t// Default reasoning_depth to 'moderate' for hypothesis/verification types\n\tif (\n\t\t(input.thought_type === 'hypothesis' || input.thought_type === 'verification') &&\n\t\t!('reasoning_depth' in input)\n\t) {\n\t\tinput.reasoning_depth = 'moderate';\n\t}\n}\n\n/**\n * Normalizes thought input data by fixing common LLM field name mistakes.\n *\n * This function handles cases where LLMs incorrectly use singular forms\n * of field names that should be plural. It applies normalization to both\n * `current_step` and `previous_steps` fields.\n *\n * The normalization is applied BEFORE schema validation, allowing the\n * strict Valibot schema to remain correct while still being tolerant\n * of common LLM mistakes.\n *\n * @param input - The raw thought input data to normalize\n * @returns Normalized thought data with correct field names\n *\n * @remarks\n * **Normalization Rules:**\n * - `recommended_tool` (singular) → `recommended_tools` (plural)\n * - `recommended_skill` (singular) → `recommended_skills` (plural)\n * - Applied to `current_step` if present (strict mode)\n * - Applied to all items in `previous_steps` if present (lenient mode with defaults)\n *\n * **Design Rationale:**\n * LLMs sometimes use singular field names even when the schema explicitly\n * defines plural forms. Rather than forcing the LLM to be perfect (which\n * leads to cryptic validation errors), we normalize the input to handle\n * these common mistakes gracefully.\n *\n * Additionally, LLMs naturally provide complete data for `current_step`\n * but only partial/skeletal data for `previous_steps` (historical context).\n * The lenient mode for `previous_steps` fills in sensible defaults:\n * - `confidence`: 0.5 for missing tool recommendation confidence\n * - `priority`: 999 for missing tool recommendation priority\n * - `rationale`: empty string for missing tool recommendation rationale\n * - `expected_outcome`: empty string for missing step expected outcome\n *\n * @example\n * ```typescript\n * const input = {\n * thought: 'I need to analyze the data',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Read the data file',\n * recommended_tool: [{ tool_name: 'Read', confidence: 0.9, rationale: 'test', priority: 1 }],\n * expected_outcome: 'Data loaded'\n * },\n * previous_steps: [{\n * step_description: 'Previous step',\n * recommended_tools: [{ tool_name: 'Grep', rationale: 'Search code' }]\n * }]\n * };\n *\n * const normalized = normalizeInput(input);\n * // current_step: recommended_tools exists (plural form)\n * // previous_steps[0]: confidence=0.5, priority=999, expected_outcome='' filled in\n * ```\n */\nexport function normalizeInput(input: unknown): ThoughtData {\n\tif (typeof input !== 'object' || input === null) {\n\t\treturn input as ThoughtData;\n\t}\n\n\tconst normalized = { ...input } as Record<string, unknown>;\n\n\t// Normalize current_step if present (strict mode - no defaults)\n\tif (normalized.current_step && typeof normalized.current_step === 'object') {\n\t\tnormalized.current_step = normalizeStepRecommendation(\n\t\t\tnormalized.current_step as Record<string, unknown>,\n\t\t\tfalse // strict mode\n\t\t);\n\t}\n\n\n\t// Normalize all items in previous_steps if present (lenient mode - with defaults)\n\tif (Array.isArray(normalized.previous_steps) && normalized.previous_steps.length > 0) {\n\t\tnormalized.previous_steps = normalized.previous_steps.map((step) =>\n\t\t\ttypeof step === 'object' && step !== null\n\t\t\t\t? normalizeStepRecommendation(step as Record<string, unknown>, true) // lenient mode\n\t\t\t\t: step\n\t\t);\n\t}\n\n\t// Sanitize branch_id to prevent path traversal attacks\n\tif (typeof normalized.branch_id === 'string') {\n\t\tnormalized.branch_id = sanitizeBranchId(normalized.branch_id);\n\t}\n\n\t// Sanitize session_id (same pattern as branch_id but allows 1-100 chars)\n\tif (typeof normalized.session_id === 'string') {\n\t\tconst sanitized = sanitizeSessionId(normalized.session_id);\n\t\tif (sanitized === undefined) {\n\t\t\tdelete normalized.session_id;\n\t\t} else {\n\t\t\tnormalized.session_id = asSessionId(sanitized);\n\t\t}\n\t}\n\n\t// Auto-generate id if not provided (for DAG node identity)\n\tif (!normalized.id || typeof normalized.id !== 'string') {\n\t\tnormalized.id = generateThoughtId();\n\t}\n\n\t// Normalize reasoning fields\n\tnormalizeReasoningFields(normalized);\n\n\n\t// Sanitize all free-text string fields recursively (dangerous HTML tags + null bytes)\n\t// This was moved from schema transforms because v.transform() cannot be converted to JSON Schema\n\tconst sanitized = sanitizeRecursive(normalized);\n\n\treturn sanitized as ThoughtData;\n}\n"],"names":["DEFAULT_RECOMMENDATION_CONFIDENCE","DEFAULT_RECOMMENDATION_PRIORITY","DEFAULT_RECOMMENDATION_RATIONALE","DEFAULT_STEP_OUTCOME","sanitizeRecursive","value","sanitizeString","Array","item","proto","Object","result","key","val","BRANCH_ID_PATTERN","sanitizeBranchId","branchId","ValidationError","sanitizeSessionId","sessionId","cleaned","MAX_SESSION_ID_LENGTH","SESSION_ID_PATTERN","normalizeRecommendation","rec","normalized","undefined","sanitizeRationale","sanitizeSuggestedInputs","normalizeStepRecommendation","step","lenient","tool","skill","normalizeReasoningFields","input","Math","v","Number","id","normalizeInput","sanitized","asSessionId","generateThoughtId"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session async lock to prevent concurrent state mutations.
|
|
3
|
+
*
|
|
4
|
+
* Uses chained promises: each lock acquisition waits for the previous
|
|
5
|
+
* holder's promise to resolve. The lock map entry is purged after release
|
|
6
|
+
* (when no waiters chained on top), preventing unbounded memory growth.
|
|
7
|
+
*
|
|
8
|
+
* Different sessions are fully independent: locks for distinct session
|
|
9
|
+
* ids never block each other. `undefined`/empty session ids share a
|
|
10
|
+
* single global slot.
|
|
11
|
+
*
|
|
12
|
+
* @module session-lock
|
|
13
|
+
*/
|
|
14
|
+
import type { ISessionLock } from '../contracts/interfaces.js';
|
|
15
|
+
/**
|
|
16
|
+
* In-memory implementation of {@link ISessionLock}.
|
|
17
|
+
*
|
|
18
|
+
* Maintains a `Map<string, Promise<void>>` where each value is the tail
|
|
19
|
+
* of the lock chain for that session. New acquisitions chain onto the
|
|
20
|
+
* tail and replace it; once they release, the entry is purged unless a
|
|
21
|
+
* later acquisition chained on top.
|
|
22
|
+
*
|
|
23
|
+
* Lock chain integrity is preserved across timeouts and `fn` errors: a
|
|
24
|
+
* waiter's slot is only released after the previous holder actually
|
|
25
|
+
* finishes, even if the waiter aborted via {@link LockTimeoutError}.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const lock = new SessionLock();
|
|
30
|
+
* await lock.withLock('session-a', async () => {
|
|
31
|
+
* // critical section — concurrent calls for 'session-a' wait
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class SessionLock implements ISessionLock {
|
|
36
|
+
private readonly _locks;
|
|
37
|
+
/**
|
|
38
|
+
* Number of currently held lock chains. Useful for diagnostics
|
|
39
|
+
* and leak assertions in tests.
|
|
40
|
+
*/
|
|
41
|
+
get size(): number;
|
|
42
|
+
/**
|
|
43
|
+
* Execute `fn` while holding the lock for the given session.
|
|
44
|
+
*
|
|
45
|
+
* Concurrent calls for the same session are serialized. Calls for
|
|
46
|
+
* different sessions run in parallel. The lock is always released
|
|
47
|
+
* (via `finally`) even if `fn` throws.
|
|
48
|
+
*
|
|
49
|
+
* @param sessionId - Session to lock. Falsy values share a global slot.
|
|
50
|
+
* @param fn - Critical section to run while holding the lock.
|
|
51
|
+
* @param timeoutMs - Maximum time to wait for the lock (default 5000ms).
|
|
52
|
+
* @throws {LockTimeoutError} When the lock cannot be acquired within `timeoutMs`.
|
|
53
|
+
*/
|
|
54
|
+
withLock<T>(sessionId: string | undefined, fn: () => Promise<T>, timeoutMs?: number): Promise<T>;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=SessionLock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionLock.d.ts","sourceRoot":"","sources":["../../src/core/SessionLock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAe/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAY,YAAW,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAE3D;;;OAGG;IACH,IAAW,IAAI,IAAI,MAAM,CAExB;IAED;;;;;;;;;;;OAWG;IACU,QAAQ,CAAC,CAAC,EACtB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,SAAS,GAAE,MAAgC,GACzC,OAAO,CAAC,CAAC,CAAC;CAoDb"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LockTimeoutError } from "../errors.js";
|
|
2
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 5000;
|
|
3
|
+
function lockKey(sessionId) {
|
|
4
|
+
return sessionId && sessionId.length > 0 ? sessionId : '__global__';
|
|
5
|
+
}
|
|
6
|
+
class SessionLock {
|
|
7
|
+
_locks = new Map();
|
|
8
|
+
get size() {
|
|
9
|
+
return this._locks.size;
|
|
10
|
+
}
|
|
11
|
+
async withLock(sessionId, fn, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
|
|
12
|
+
const key = lockKey(sessionId);
|
|
13
|
+
const previous = this._locks.get(key) ?? Promise.resolve();
|
|
14
|
+
let release;
|
|
15
|
+
const next = previous.then(()=>new Promise((resolve)=>{
|
|
16
|
+
release = resolve;
|
|
17
|
+
}), ()=>new Promise((resolve)=>{
|
|
18
|
+
release = resolve;
|
|
19
|
+
}));
|
|
20
|
+
this._locks.set(key, next);
|
|
21
|
+
let timeoutId;
|
|
22
|
+
try {
|
|
23
|
+
await new Promise((resolve, reject)=>{
|
|
24
|
+
timeoutId = setTimeout(()=>reject(new LockTimeoutError(key, timeoutMs)), timeoutMs);
|
|
25
|
+
previous.then(()=>resolve(), ()=>resolve());
|
|
26
|
+
});
|
|
27
|
+
return await fn();
|
|
28
|
+
} finally{
|
|
29
|
+
if (void 0 !== timeoutId) clearTimeout(timeoutId);
|
|
30
|
+
if (this._locks.get(key) === next) this._locks.delete(key);
|
|
31
|
+
if (release) release();
|
|
32
|
+
else {
|
|
33
|
+
const safeRelease = ()=>{
|
|
34
|
+
if (release) release();
|
|
35
|
+
};
|
|
36
|
+
previous.then(safeRelease, safeRelease);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export { SessionLock };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=SessionLock.js.map
|