tracelattice 1.3.2 â 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/README.md +25 -25
- package/dist/ServerConfig.d.ts +16 -23
- 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__/eval/fixtures/scenarios.d.ts.map +1 -1
- package/dist/__tests__/helpers/factories.d.ts +20 -1
- 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/cache/DiscoveryCache.d.ts +1 -1
- package/dist/cache/DiscoveryCache.d.ts.map +1 -1
- package/dist/cache/DiscoveryCache.js.map +1 -1
- package/dist/cli.js +3602 -8
- package/dist/config/ConfigLoader.d.ts +9 -2
- package/dist/config/ConfigLoader.d.ts.map +1 -1
- package/dist/config/ConfigLoader.js +12 -5
- 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/PersistenceBackend.d.ts.map +1 -0
- package/dist/contracts/features.d.ts +39 -0
- package/dist/contracts/features.d.ts.map +1 -0
- package/dist/contracts/features.js +15 -0
- package/dist/contracts/features.js.map +1 -0
- package/dist/contracts/ids.d.ts +58 -0
- package/dist/contracts/ids.d.ts.map +1 -0
- package/dist/contracts/ids.js +31 -0
- package/dist/contracts/ids.js.map +1 -0
- package/dist/contracts/interfaces.d.ts +48 -3
- package/dist/contracts/interfaces.d.ts.map +1 -1
- package/dist/contracts/strategy.d.ts +2 -2
- package/dist/contracts/strategy.d.ts.map +1 -1
- package/dist/contracts/suspension.d.ts +3 -2
- package/dist/contracts/suspension.d.ts.map +1 -1
- package/dist/contracts/transport.d.ts +25 -0
- package/dist/contracts/transport.d.ts.map +1 -0
- package/dist/core/HistoryManager.d.ts +15 -4
- 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/IHistoryManager.d.ts +10 -0
- package/dist/core/IHistoryManager.d.ts.map +1 -1
- package/dist/core/IThoughtFormatter.d.ts +51 -0
- package/dist/core/IThoughtFormatter.d.ts.map +1 -0
- package/dist/core/IThoughtFormatter.js +1 -0
- package/dist/core/InputNormalizer.d.ts.map +1 -1
- package/dist/core/InputNormalizer.js +9 -5
- package/dist/core/InputNormalizer.js.map +1 -1
- package/dist/core/PersistenceBuffer.d.ts +1 -1
- package/dist/core/PersistenceBuffer.d.ts.map +1 -1
- package/dist/core/PersistenceBuffer.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/ThoughtFormatter.d.ts +2 -1
- package/dist/core/ThoughtFormatter.d.ts.map +1 -1
- package/dist/core/ThoughtFormatter.js +3 -0
- package/dist/core/ThoughtFormatter.js.map +1 -1
- package/dist/core/ThoughtProcessor.d.ts +22 -3
- package/dist/core/ThoughtProcessor.d.ts.map +1 -1
- package/dist/core/ThoughtProcessor.js +41 -16
- package/dist/core/ThoughtProcessor.js.map +1 -1
- package/dist/core/compression/CompressionService.js +3 -3
- package/dist/core/compression/CompressionService.js.map +1 -1
- package/dist/core/compression/Summary.d.ts +4 -3
- package/dist/core/compression/Summary.d.ts.map +1 -1
- package/dist/core/graph/Edge.d.ts +11 -4
- package/dist/core/graph/Edge.d.ts.map +1 -1
- package/dist/core/graph/EdgeEmitter.js +5 -5
- package/dist/core/graph/EdgeEmitter.js.map +1 -1
- package/dist/core/reasoning/strategies/StrategyFactory.d.ts +1 -1
- package/dist/core/reasoning/strategies/StrategyFactory.d.ts.map +1 -1
- package/dist/core/reasoning/strategies/StrategyFactory.js.map +1 -1
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.d.ts.map +1 -1
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js +5 -0
- package/dist/core/reasoning/strategies/TreeOfThoughtStrategy.js.map +1 -1
- package/dist/core/reasoning.d.ts +8 -1
- package/dist/core/reasoning.d.ts.map +1 -1
- package/dist/core/step.d.ts +5 -0
- package/dist/core/step.d.ts.map +1 -1
- package/dist/core/thought.d.ts +4 -3
- package/dist/core/thought.d.ts.map +1 -1
- package/dist/core/tools/InMemorySuspensionStore.d.ts +3 -1
- package/dist/core/tools/InMemorySuspensionStore.d.ts.map +1 -1
- package/dist/core/tools/InMemorySuspensionStore.js +2 -2
- package/dist/core/tools/InMemorySuspensionStore.js.map +1 -1
- package/dist/di/Container.d.ts +6 -3
- package/dist/di/Container.d.ts.map +1 -1
- package/dist/di/Container.js.map +1 -1
- package/dist/di/ServiceRegistry.d.ts +6 -6
- package/dist/di/ServiceRegistry.d.ts.map +1 -1
- package/dist/errors.d.ts +84 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +85 -22
- package/dist/errors.js.map +1 -1
- package/dist/health/HealthChecker.d.ts +1 -1
- package/dist/health/HealthChecker.d.ts.map +1 -1
- package/dist/health/HealthChecker.js.map +1 -1
- package/dist/lib.d.ts +60 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +9 -3
- package/dist/lib.js.map +1 -1
- package/dist/persistence/FilePersistence.d.ts +2 -2
- package/dist/persistence/FilePersistence.d.ts.map +1 -1
- package/dist/persistence/FilePersistence.js.map +1 -1
- package/dist/persistence/MemoryPersistence.d.ts +1 -1
- package/dist/persistence/MemoryPersistence.d.ts.map +1 -1
- package/dist/persistence/MemoryPersistence.js.map +1 -1
- package/dist/persistence/PersistenceFactory.d.ts +1 -1
- package/dist/persistence/PersistenceFactory.d.ts.map +1 -1
- package/dist/persistence/PersistenceFactory.js.map +1 -1
- package/dist/persistence/SqlitePersistence.d.ts +1 -1
- package/dist/persistence/SqlitePersistence.d.ts.map +1 -1
- package/dist/persistence/SqlitePersistence.js.map +1 -1
- package/dist/pool/ConnectionPool.d.ts +11 -13
- package/dist/pool/ConnectionPool.d.ts.map +1 -1
- package/dist/pool/ConnectionPool.js.map +1 -1
- package/dist/pool/IConnectionPool.d.ts +100 -0
- package/dist/pool/IConnectionPool.d.ts.map +1 -0
- package/dist/pool/IConnectionPool.js +1 -0
- package/dist/registry/BaseRegistry.d.ts +1 -1
- package/dist/registry/BaseRegistry.d.ts.map +1 -1
- package/dist/registry/BaseRegistry.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/BaseTransport.d.ts +3 -2
- package/dist/transport/BaseTransport.d.ts.map +1 -1
- package/dist/transport/BaseTransport.js +1 -1
- package/dist/transport/BaseTransport.js.map +1 -1
- package/dist/transport/HttpTransport.d.ts +4 -2
- package/dist/transport/HttpTransport.d.ts.map +1 -1
- package/dist/transport/HttpTransport.js +13 -4
- package/dist/transport/HttpTransport.js.map +1 -1
- package/dist/transport/SseTransport.d.ts +4 -2
- package/dist/transport/SseTransport.d.ts.map +1 -1
- package/dist/transport/SseTransport.js +13 -3
- package/dist/transport/SseTransport.js.map +1 -1
- package/dist/transport/StreamableHttpTransport.d.ts +4 -2
- package/dist/transport/StreamableHttpTransport.d.ts.map +1 -1
- package/dist/transport/StreamableHttpTransport.js +12 -4
- package/dist/transport/StreamableHttpTransport.js.map +1 -1
- package/dist/types/skill.d.ts +5 -0
- package/dist/types/skill.d.ts.map +1 -1
- package/dist/types/tool.d.ts +6 -1
- package/dist/types/tool.d.ts.map +1 -1
- package/package.json +12 -11
- package/dist/__tests__/helpers/index.d.ts +0 -3
- package/dist/__tests__/helpers/index.d.ts.map +0 -1
- package/dist/contracts/index.d.ts +0 -14
- package/dist/contracts/index.d.ts.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1
- package/dist/persistence/PersistenceBackend.d.ts.map +0 -1
- /package/dist/{persistence â contracts}/PersistenceBackend.d.ts +0 -0
- /package/dist/{persistence â contracts}/PersistenceBackend.js +0 -0
- /package/dist/contracts/{index.js â transport.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core/PersistenceBuffer.js","sources":["../../src/core/PersistenceBuffer.ts"],"sourcesContent":["/**\n * PersistenceBuffer â owns the periodic flush timer, retry/backoff logic, and\n * concurrent-flush guarding for HistoryManager's persistence write buffer.\n *\n * Extracted from HistoryManager. The session Map and write buffers remain\n * owned by HistoryManager (passed by reference) so public access patterns\n * (e.g. `manager._sessions`) are preserved.\n *\n * @module PersistenceBuffer\n */\n\nimport type { IEdgeStore } from '../contracts/interfaces.js';\nimport { getErrorMessage } from '../errors.js';\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\nimport type { PersistenceBackend } from '../persistence/PersistenceBackend.js';\nimport type { ThoughtData } from './thought.js';\n\n/** Minimal session view: anything that owns a `writeBuffer`. */\nexport interface BufferedSession {\n\twriteBuffer: ThoughtData[];\n}\n\n/** Event emitter contract for persistence error events. */\nexport interface PersistenceEventEmitter {\n\temit(event: 'persistenceError', payload: { operation: string; error: Error }): boolean;\n}\n\n/** Configuration options for PersistenceBuffer. */\nexport interface PersistenceBufferConfig<S extends BufferedSession> {\n\tpersistence: PersistenceBackend;\n\tbufferSize: number;\n\tflushInterval: number;\n\tmaxRetries: number;\n\tdefaultSessionId: string;\n\t/** Returns the live session map (called on each flush). */\n\tgetSessions: () => Map<string, S>;\n\t/** Returns the requeue target session (default-session buffer). */\n\tgetDefaultSession: () => S;\n\t/** Optional EdgeStore for flushing edges alongside thoughts. */\n\tedgeStore?: IEdgeStore;\n\t/** Optional emitter for `persistenceError` events. */\n\teventEmitter?: PersistenceEventEmitter | null;\n\tlogger?: Logger;\n}\n\n/**\n * Manages buffered persistence writes with periodic flushing, exponential\n * backoff retries, concurrent-flush guarding, and edge store integration.\n */\nexport class PersistenceBuffer<S extends BufferedSession> {\n\tprivate readonly _persistence: PersistenceBackend;\n\tprivate readonly _bufferSize: number;\n\tprivate readonly _flushInterval: number;\n\tprivate readonly _maxRetries: number;\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _getSessions: () => Map<string, S>;\n\tprivate readonly _getDefaultSession: () => S;\n\tprivate readonly _edgeStore?: IEdgeStore;\n\tprivate _eventEmitter: PersistenceEventEmitter | null;\n\tprivate readonly _logger: Logger;\n\n\tprivate _flushTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate _isFlushing = false;\n\tprivate _flushRetryCount = 0;\n\n\tconstructor(config: PersistenceBufferConfig<S>) {\n\t\tthis._persistence = config.persistence;\n\t\tthis._bufferSize = config.bufferSize;\n\t\tthis._flushInterval = config.flushInterval;\n\t\tthis._maxRetries = config.maxRetries;\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._getSessions = config.getSessions;\n\t\tthis._getDefaultSession = config.getDefaultSession;\n\t\tthis._edgeStore = config.edgeStore;\n\t\tthis._eventEmitter = config.eventEmitter ?? null;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying flush timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._flushTimer;\n\t}\n\n\t/** Returns true when a flush is in progress. */\n\tpublic get isFlushing(): boolean {\n\t\treturn this._isFlushing;\n\t}\n\n\t/** Sets / replaces the persistence error event emitter. */\n\tpublic setEventEmitter(emitter: PersistenceEventEmitter | null): void {\n\t\tthis._eventEmitter = emitter;\n\t}\n\n\t/**\n\t * Buffers a thought into the given session's write buffer. Triggers an\n\t * immediate flush when the buffer reaches `bufferSize`.\n\t */\n\tpublic bufferThought(session: BufferedSession, thought: ThoughtData): void {\n\t\t// Backpressure: if buffer is full and flush is in progress, log warning\n\t\tif (session.writeBuffer.length >= this._bufferSize && this._isFlushing) {\n\t\t\tthis._logger.info('Write buffer full and flush in progress, applying backpressure', {\n\t\t\t\tbufferSize: session.writeBuffer.length,\n\t\t\t\tmaxSize: this._bufferSize,\n\t\t\t});\n\t\t}\n\n\t\tsession.writeBuffer.push(thought);\n\n\t\tif (session.writeBuffer.length >= this._bufferSize) {\n\t\t\tvoid this.flush();\n\t\t}\n\t}\n\n\t/**\n\t * Starts the periodic flush timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startFlushTimer(): void {\n\t\tif (this._flushTimer !== null) return;\n\t\tthis._flushTimer = setInterval(() => {\n\t\t\tvoid this.flush();\n\t\t}, this._flushInterval);\n\t\tif (this._flushTimer && typeof this._flushTimer === 'object' && 'unref' in this._flushTimer) {\n\t\t\tthis._flushTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic flush timer. */\n\tpublic stopFlushTimer(): void {\n\t\tif (this._flushTimer !== null) {\n\t\t\tclearInterval(this._flushTimer);\n\t\t\tthis._flushTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes the write buffer to the persistence backend.\n\t *\n\t * Collects all buffered thoughts across all sessions and saves them\n\t * individually with retry logic. On persistent failure (all retries\n\t * exhausted), emits a `persistenceError` event and re-queues failed items\n\t * into the default session's buffer.\n\t *\n\t * Safe to call concurrently â duplicate calls are skipped.\n\t */\n\tpublic async flush(): Promise<void> {\n\t\tif (this._isFlushing) return;\n\n\t\t// Collect all pending writes from all sessions\n\t\tconst allPending: ThoughtData[] = [];\n\t\tfor (const session of this._getSessions().values()) {\n\t\t\tif (session.writeBuffer.length > 0) {\n\t\t\t\tallPending.push(...session.writeBuffer.splice(0));\n\t\t\t}\n\t\t}\n\n\t\tif (allPending.length === 0) return;\n\n\t\tthis._isFlushing = true;\n\t\tconst failedItems: ThoughtData[] = [];\n\n\t\ttry {\n\t\t\tfor (const thought of allPending) {\n\t\t\t\tconst saved = await this._flushSingleThought(thought);\n\t\t\t\tif (!saved) {\n\t\t\t\t\tfailedItems.push(thought);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis._handleFlushResult(failedItems, allPending.length);\n\n\t\t\tif (this._edgeStore) {\n\t\t\t\tawait this._flushEdges();\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._isFlushing = false;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes edges for all known sessions to the persistence backend.\n\t * No-op when EdgeStore is unavailable.\n\t */\n\tprivate async _flushEdges(): Promise<void> {\n\t\tif (!this._edgeStore) return;\n\t\tconst sessionKeys = new Set<string>(this._getSessions().keys());\n\t\tsessionKeys.add(this._defaultSessionId);\n\t\tfor (const sessionId of sessionKeys) {\n\t\t\tconst edges = this._edgeStore.edgesForSession(sessionId);\n\t\t\tif (edges.length === 0) continue;\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveEdges(sessionId, edges);\n\t\t\t} catch (err) {\n\t\t\t\tthis._logger.info('Failed to persist edges for session', {\n\t\t\t\t\tsessionId,\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Flushes a single thought to persistence with exponential backoff retry.\n\t * @returns true if saved successfully, false otherwise\n\t */\n\tprivate async _flushSingleThought(thought: ThoughtData): Promise<boolean> {\n\t\tconst backoffDelays = [100, 500, 2000];\n\n\t\tfor (let attempt = 0; attempt <= this._maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveThought(thought);\n\t\t\t\treturn true;\n\t\t\t} catch (err) {\n\t\t\t\tif (attempt < this._maxRetries) {\n\t\t\t\t\tconst delay = backoffDelays[attempt] ?? backoffDelays[backoffDelays.length - 1]!;\n\t\t\t\t\tthis._logger.info(`Persistence retry ${attempt + 1}/${this._maxRetries}`, {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\tdelay,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t\tawait this._delay(delay);\n\t\t\t\t} else {\n\t\t\t\t\tthis._logger.info('All persistence retries exhausted for thought', {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Handles the result of a flush operation, re-queuing failures into the\n\t * default session's buffer and emitting a `persistenceError` event when\n\t * any items failed.\n\t */\n\tprivate _handleFlushResult(failedItems: ThoughtData[], totalCount: number): void {\n\t\tif (failedItems.length > 0) {\n\t\t\tconst defaultSession = this._getDefaultSession();\n\t\t\tdefaultSession.writeBuffer.unshift(...failedItems);\n\t\t\tthis._flushRetryCount++;\n\n\t\t\tconst error = new Error(\n\t\t\t\t`Failed to persist ${failedItems.length} thoughts after ${this._maxRetries} retries`\n\t\t\t);\n\t\t\tthis._eventEmitter?.emit('persistenceError', {\n\t\t\t\toperation: 'flushBuffer',\n\t\t\t\terror,\n\t\t\t});\n\n\t\t\tthis._logger.info('Flush completed with failures', {\n\t\t\t\tfailed: failedItems.length,\n\t\t\t\ttotal: totalCount,\n\t\t\t\tconsecutiveFailures: this._flushRetryCount,\n\t\t\t});\n\t\t} else {\n\t\t\tthis._flushRetryCount = 0;\n\t\t}\n\t}\n\n\t/** Returns a promise that resolves after the specified delay. */\n\tprivate _delay(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n"],"names":["PersistenceBuffer","config","NullLogger","emitter","session","thought","setInterval","clearInterval","allPending","failedItems","saved","sessionKeys","Set","sessionId","edges","err","getErrorMessage","backoffDelays","attempt","delay","totalCount","defaultSession","error","Error","ms","Promise","resolve","setTimeout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core/PersistenceBuffer.js","sources":["../../src/core/PersistenceBuffer.ts"],"sourcesContent":["/**\n * PersistenceBuffer â owns the periodic flush timer, retry/backoff logic, and\n * concurrent-flush guarding for HistoryManager's persistence write buffer.\n *\n * Extracted from HistoryManager. The session Map and write buffers remain\n * owned by HistoryManager (passed by reference) so public access patterns\n * (e.g. `manager._sessions`) are preserved.\n *\n * @module PersistenceBuffer\n */\n\nimport type { IEdgeStore } from '../contracts/interfaces.js';\nimport { getErrorMessage } from '../errors.js';\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\nimport type { PersistenceBackend } from '../contracts/PersistenceBackend.js';\nimport type { ThoughtData } from './thought.js';\n\n/** Minimal session view: anything that owns a `writeBuffer`. */\nexport interface BufferedSession {\n\twriteBuffer: ThoughtData[];\n}\n\n/** Event emitter contract for persistence error events. */\nexport interface PersistenceEventEmitter {\n\temit(event: 'persistenceError', payload: { operation: string; error: Error }): boolean;\n}\n\n/** Configuration options for PersistenceBuffer. */\nexport interface PersistenceBufferConfig<S extends BufferedSession> {\n\tpersistence: PersistenceBackend;\n\tbufferSize: number;\n\tflushInterval: number;\n\tmaxRetries: number;\n\tdefaultSessionId: string;\n\t/** Returns the live session map (called on each flush). */\n\tgetSessions: () => Map<string, S>;\n\t/** Returns the requeue target session (default-session buffer). */\n\tgetDefaultSession: () => S;\n\t/** Optional EdgeStore for flushing edges alongside thoughts. */\n\tedgeStore?: IEdgeStore;\n\t/** Optional emitter for `persistenceError` events. */\n\teventEmitter?: PersistenceEventEmitter | null;\n\tlogger?: Logger;\n}\n\n/**\n * Manages buffered persistence writes with periodic flushing, exponential\n * backoff retries, concurrent-flush guarding, and edge store integration.\n */\nexport class PersistenceBuffer<S extends BufferedSession> {\n\tprivate readonly _persistence: PersistenceBackend;\n\tprivate readonly _bufferSize: number;\n\tprivate readonly _flushInterval: number;\n\tprivate readonly _maxRetries: number;\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _getSessions: () => Map<string, S>;\n\tprivate readonly _getDefaultSession: () => S;\n\tprivate readonly _edgeStore?: IEdgeStore;\n\tprivate _eventEmitter: PersistenceEventEmitter | null;\n\tprivate readonly _logger: Logger;\n\n\tprivate _flushTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate _isFlushing = false;\n\tprivate _flushRetryCount = 0;\n\n\tconstructor(config: PersistenceBufferConfig<S>) {\n\t\tthis._persistence = config.persistence;\n\t\tthis._bufferSize = config.bufferSize;\n\t\tthis._flushInterval = config.flushInterval;\n\t\tthis._maxRetries = config.maxRetries;\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._getSessions = config.getSessions;\n\t\tthis._getDefaultSession = config.getDefaultSession;\n\t\tthis._edgeStore = config.edgeStore;\n\t\tthis._eventEmitter = config.eventEmitter ?? null;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying flush timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._flushTimer;\n\t}\n\n\t/** Returns true when a flush is in progress. */\n\tpublic get isFlushing(): boolean {\n\t\treturn this._isFlushing;\n\t}\n\n\t/** Sets / replaces the persistence error event emitter. */\n\tpublic setEventEmitter(emitter: PersistenceEventEmitter | null): void {\n\t\tthis._eventEmitter = emitter;\n\t}\n\n\t/**\n\t * Buffers a thought into the given session's write buffer. Triggers an\n\t * immediate flush when the buffer reaches `bufferSize`.\n\t */\n\tpublic bufferThought(session: BufferedSession, thought: ThoughtData): void {\n\t\t// Backpressure: if buffer is full and flush is in progress, log warning\n\t\tif (session.writeBuffer.length >= this._bufferSize && this._isFlushing) {\n\t\t\tthis._logger.info('Write buffer full and flush in progress, applying backpressure', {\n\t\t\t\tbufferSize: session.writeBuffer.length,\n\t\t\t\tmaxSize: this._bufferSize,\n\t\t\t});\n\t\t}\n\n\t\tsession.writeBuffer.push(thought);\n\n\t\tif (session.writeBuffer.length >= this._bufferSize) {\n\t\t\tvoid this.flush();\n\t\t}\n\t}\n\n\t/**\n\t * Starts the periodic flush timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startFlushTimer(): void {\n\t\tif (this._flushTimer !== null) return;\n\t\tthis._flushTimer = setInterval(() => {\n\t\t\tvoid this.flush();\n\t\t}, this._flushInterval);\n\t\tif (this._flushTimer && typeof this._flushTimer === 'object' && 'unref' in this._flushTimer) {\n\t\t\tthis._flushTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic flush timer. */\n\tpublic stopFlushTimer(): void {\n\t\tif (this._flushTimer !== null) {\n\t\t\tclearInterval(this._flushTimer);\n\t\t\tthis._flushTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes the write buffer to the persistence backend.\n\t *\n\t * Collects all buffered thoughts across all sessions and saves them\n\t * individually with retry logic. On persistent failure (all retries\n\t * exhausted), emits a `persistenceError` event and re-queues failed items\n\t * into the default session's buffer.\n\t *\n\t * Safe to call concurrently â duplicate calls are skipped.\n\t */\n\tpublic async flush(): Promise<void> {\n\t\tif (this._isFlushing) return;\n\n\t\t// Collect all pending writes from all sessions\n\t\tconst allPending: ThoughtData[] = [];\n\t\tfor (const session of this._getSessions().values()) {\n\t\t\tif (session.writeBuffer.length > 0) {\n\t\t\t\tallPending.push(...session.writeBuffer.splice(0));\n\t\t\t}\n\t\t}\n\n\t\tif (allPending.length === 0) return;\n\n\t\tthis._isFlushing = true;\n\t\tconst failedItems: ThoughtData[] = [];\n\n\t\ttry {\n\t\t\tfor (const thought of allPending) {\n\t\t\t\tconst saved = await this._flushSingleThought(thought);\n\t\t\t\tif (!saved) {\n\t\t\t\t\tfailedItems.push(thought);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis._handleFlushResult(failedItems, allPending.length);\n\n\t\t\tif (this._edgeStore) {\n\t\t\t\tawait this._flushEdges();\n\t\t\t}\n\t\t} finally {\n\t\t\tthis._isFlushing = false;\n\t\t}\n\t}\n\n\t/**\n\t * Flushes edges for all known sessions to the persistence backend.\n\t * No-op when EdgeStore is unavailable.\n\t */\n\tprivate async _flushEdges(): Promise<void> {\n\t\tif (!this._edgeStore) return;\n\t\tconst sessionKeys = new Set<string>(this._getSessions().keys());\n\t\tsessionKeys.add(this._defaultSessionId);\n\t\tfor (const sessionId of sessionKeys) {\n\t\t\tconst edges = this._edgeStore.edgesForSession(sessionId);\n\t\t\tif (edges.length === 0) continue;\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveEdges(sessionId, edges);\n\t\t\t} catch (err) {\n\t\t\t\tthis._logger.info('Failed to persist edges for session', {\n\t\t\t\t\tsessionId,\n\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Flushes a single thought to persistence with exponential backoff retry.\n\t * @returns true if saved successfully, false otherwise\n\t */\n\tprivate async _flushSingleThought(thought: ThoughtData): Promise<boolean> {\n\t\tconst backoffDelays = [100, 500, 2000];\n\n\t\tfor (let attempt = 0; attempt <= this._maxRetries; attempt++) {\n\t\t\ttry {\n\t\t\t\tawait this._persistence.saveThought(thought);\n\t\t\t\treturn true;\n\t\t\t} catch (err) {\n\t\t\t\tif (attempt < this._maxRetries) {\n\t\t\t\t\tconst delay = backoffDelays[attempt] ?? backoffDelays[backoffDelays.length - 1]!;\n\t\t\t\t\tthis._logger.info(`Persistence retry ${attempt + 1}/${this._maxRetries}`, {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\tdelay,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t\tawait this._delay(delay);\n\t\t\t\t} else {\n\t\t\t\t\tthis._logger.info('All persistence retries exhausted for thought', {\n\t\t\t\t\t\tthoughtNumber: thought.thought_number,\n\t\t\t\t\t\terror: getErrorMessage(err),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Handles the result of a flush operation, re-queuing failures into the\n\t * default session's buffer and emitting a `persistenceError` event when\n\t * any items failed.\n\t */\n\tprivate _handleFlushResult(failedItems: ThoughtData[], totalCount: number): void {\n\t\tif (failedItems.length > 0) {\n\t\t\tconst defaultSession = this._getDefaultSession();\n\t\t\tdefaultSession.writeBuffer.unshift(...failedItems);\n\t\t\tthis._flushRetryCount++;\n\n\t\t\tconst error = new Error(\n\t\t\t\t`Failed to persist ${failedItems.length} thoughts after ${this._maxRetries} retries`\n\t\t\t);\n\t\t\tthis._eventEmitter?.emit('persistenceError', {\n\t\t\t\toperation: 'flushBuffer',\n\t\t\t\terror,\n\t\t\t});\n\n\t\t\tthis._logger.info('Flush completed with failures', {\n\t\t\t\tfailed: failedItems.length,\n\t\t\t\ttotal: totalCount,\n\t\t\t\tconsecutiveFailures: this._flushRetryCount,\n\t\t\t});\n\t\t} else {\n\t\t\tthis._flushRetryCount = 0;\n\t\t}\n\t}\n\n\t/** Returns a promise that resolves after the specified delay. */\n\tprivate _delay(ms: number): Promise<void> {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n"],"names":["PersistenceBuffer","config","NullLogger","emitter","session","thought","setInterval","clearInterval","allPending","failedItems","saved","sessionKeys","Set","sessionId","edges","err","getErrorMessage","backoffDelays","attempt","delay","totalCount","defaultSession","error","Error","ms","Promise","resolve","setTimeout"],"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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core/SessionLock.js","sources":["../../src/core/SessionLock.ts"],"sourcesContent":["/**\n * Per-session async lock to prevent concurrent state mutations.\n *\n * Uses chained promises: each lock acquisition waits for the previous\n * holder's promise to resolve. The lock map entry is purged after release\n * (when no waiters chained on top), preventing unbounded memory growth.\n *\n * Different sessions are fully independent: locks for distinct session\n * ids never block each other. `undefined`/empty session ids share a\n * single global slot.\n *\n * @module session-lock\n */\n\nimport type { ISessionLock } from '../contracts/interfaces.js';\nimport { LockTimeoutError } from '../errors.js';\n\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n\n/**\n * Normalize a session id for keying the internal lock map.\n * Treats `undefined`, `null`, and empty strings as the global session.\n *\n * @internal\n */\nfunction lockKey(sessionId: string | undefined): string {\n\treturn sessionId && sessionId.length > 0 ? sessionId : '__global__';\n}\n\n/**\n * In-memory implementation of {@link ISessionLock}.\n *\n * Maintains a `Map<string, Promise<void>>` where each value is the tail\n * of the lock chain for that session. New acquisitions chain onto the\n * tail and replace it; once they release, the entry is purged unless a\n * later acquisition chained on top.\n *\n * Lock chain integrity is preserved across timeouts and `fn` errors: a\n * waiter's slot is only released after the previous holder actually\n * finishes, even if the waiter aborted via {@link LockTimeoutError}.\n *\n * @example\n * ```typescript\n * const lock = new SessionLock();\n * await lock.withLock('session-a', async () => {\n * // critical section â concurrent calls for 'session-a' wait\n * });\n * ```\n */\nexport class SessionLock implements ISessionLock {\n\tprivate readonly _locks = new Map<string, Promise<void>>();\n\n\t/**\n\t * Number of currently held lock chains. Useful for diagnostics\n\t * and leak assertions in tests.\n\t */\n\tpublic get size(): number {\n\t\treturn this._locks.size;\n\t}\n\n\t/**\n\t * Execute `fn` while holding the lock for the given session.\n\t *\n\t * Concurrent calls for the same session are serialized. Calls for\n\t * different sessions run in parallel. The lock is always released\n\t * (via `finally`) even if `fn` throws.\n\t *\n\t * @param sessionId - Session to lock. Falsy values share a global slot.\n\t * @param fn - Critical section to run while holding the lock.\n\t * @param timeoutMs - Maximum time to wait for the lock (default 5000ms).\n\t * @throws {LockTimeoutError} When the lock cannot be acquired within `timeoutMs`.\n\t */\n\tpublic async withLock<T>(\n\t\tsessionId: string | undefined,\n\t\tfn: () => Promise<T>,\n\t\ttimeoutMs: number = DEFAULT_LOCK_TIMEOUT_MS,\n\t): Promise<T> {\n\t\tconst key = lockKey(sessionId);\n\t\tconst previous = this._locks.get(key) ?? Promise.resolve();\n\n\t\t// `next` is the tail this acquirer publishes to the chain. It only\n\t\t// resolves after `previous` settles, guaranteeing serialization even\n\t\t// when the current acquirer times out before holding the lock.\n\t\tlet release!: () => void;\n\t\tconst next = previous.then(\n\t\t\t() => new Promise<void>((resolve) => {\n\t\t\t\trelease = resolve;\n\t\t\t}),\n\t\t\t() => new Promise<void>((resolve) => {\n\t\t\t\trelease = resolve;\n\t\t\t}),\n\t\t);\n\t\tthis._locks.set(key, next);\n\n\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\n\t\ttry {\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\ttimeoutId = setTimeout(\n\t\t\t\t\t() => reject(new LockTimeoutError(key, timeoutMs)),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t);\n\t\t\t\tprevious.then(\n\t\t\t\t\t() => resolve(),\n\t\t\t\t\t() => resolve(), // previous holder's failure must not poison the chain\n\t\t\t\t);\n\t\t\t});\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\tif (timeoutId !== undefined) {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t}\n\t\t\t// Only purge the chain tail if no later acquisition chained on top.\n\t\t\tif (this._locks.get(key) === next) {\n\t\t\t\tthis._locks.delete(key);\n\t\t\t}\n\t\t\t// `release` is assigned inside the `previous.then(...)` callback.\n\t\t\t// If the timeout fired before `previous` resolved, `release` may\n\t\t\t// not yet exist â wait for `previous` to settle, then release.\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t} else {\n\t\t\t\tconst safeRelease = (): void => {\n\t\t\t\t\tif (release) release();\n\t\t\t\t};\n\t\t\t\tprevious.then(safeRelease, safeRelease);\n\t\t\t}\n\t\t}\n\t}\n}\n"],"names":["DEFAULT_LOCK_TIMEOUT_MS","lockKey","sessionId","SessionLock","Map","fn","timeoutMs","key","previous","Promise","release","next","resolve","timeoutId","reject","setTimeout","LockTimeoutError","undefined","clearTimeout","safeRelease"],"mappings":";AAiBA,MAAMA,0BAA0B;AAQhC,SAASC,QAAQC,SAA6B;IAC7C,OAAOA,aAAaA,UAAU,MAAM,GAAG,IAAIA,YAAY;AACxD;AAsBO,MAAMC;IACK,SAAS,IAAIC,MAA6B;IAM3D,IAAW,OAAe;QACzB,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI;IACxB;IAcA,MAAa,SACZF,SAA6B,EAC7BG,EAAoB,EACpBC,YAAoBN,uBAAuB,EAC9B;QACb,MAAMO,MAAMN,QAAQC;QACpB,MAAMM,WAAW,IAAI,CAAC,MAAM,CAAC,GAAG,CAACD,QAAQE,QAAQ,OAAO;QAKxD,IAAIC;QACJ,MAAMC,OAAOH,SAAS,IAAI,CACzB,IAAM,IAAIC,QAAc,CAACG;gBACxBF,UAAUE;YACX,IACA,IAAM,IAAIH,QAAc,CAACG;gBACxBF,UAAUE;YACX;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAACL,KAAKI;QAErB,IAAIE;QACJ,IAAI;YACH,MAAM,IAAIJ,QAAc,CAACG,SAASE;gBACjCD,YAAYE,WACX,IAAMD,OAAO,IAAIE,iBAAiBT,KAAKD,aACvCA;gBAEDE,SAAS,IAAI,CACZ,IAAMI,WACN,IAAMA;YAER;YACA,OAAO,MAAMP;QACd,SAAU;YACT,IAAIQ,AAAcI,WAAdJ,WACHK,aAAaL;YAGd,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAACN,SAASI,MAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,CAACJ;YAKpB,IAAIG,SACHA;iBACM;gBACN,MAAMS,cAAc;oBACnB,IAAIT,SAASA;gBACd;gBACAF,SAAS,IAAI,CAACW,aAAaA;YAC5B;QACD;IACD;AACD"}
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* @module SessionManager
|
|
8
8
|
*/
|
|
9
9
|
import type { Logger } from '../logger/StructuredLogger.js';
|
|
10
|
-
/** Minimal session contract â anything with a `lastAccessedAt` timestamp. */
|
|
10
|
+
/** Minimal session contract â anything with a `lastAccessedAt` timestamp and optional owner. */
|
|
11
11
|
export interface SessionLike {
|
|
12
12
|
lastAccessedAt: number;
|
|
13
|
+
/** Owner identifier for per-owner LRU quota. Undefined for stdio/global sessions. */
|
|
14
|
+
owner?: string;
|
|
13
15
|
}
|
|
14
16
|
/** Configuration options for SessionManager. */
|
|
15
17
|
export interface SessionManagerConfig {
|
|
@@ -21,6 +23,8 @@ export interface SessionManagerConfig {
|
|
|
21
23
|
cleanupIntervalMs: number;
|
|
22
24
|
/** Returns the current MAX_SESSIONS limit (callable so tests can mutate). */
|
|
23
25
|
getMaxSessions: () => number;
|
|
26
|
+
/** Maximum sessions per owner (per-owner LRU bucket). @default 50 */
|
|
27
|
+
maxSessionsPerOwner?: number;
|
|
24
28
|
logger?: Logger;
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
@@ -32,6 +36,7 @@ export declare class SessionManager<S extends SessionLike> {
|
|
|
32
36
|
private readonly _sessionTtlMs;
|
|
33
37
|
private readonly _cleanupIntervalMs;
|
|
34
38
|
private readonly _getMaxSessions;
|
|
39
|
+
private readonly _maxSessionsPerOwner;
|
|
35
40
|
private readonly _logger;
|
|
36
41
|
private _cleanupTimer;
|
|
37
42
|
constructor(config: SessionManagerConfig);
|
|
@@ -50,9 +55,19 @@ export declare class SessionManager<S extends SessionLike> {
|
|
|
50
55
|
*/
|
|
51
56
|
cleanupStaleSessions(sessions: Map<string, S>): void;
|
|
52
57
|
/**
|
|
53
|
-
* Evicts oldest sessions when the configured
|
|
54
|
-
*
|
|
58
|
+
* Evicts oldest sessions when the configured maximums are exceeded.
|
|
59
|
+
*
|
|
60
|
+
* Two-stage policy:
|
|
61
|
+
* 1. Per-owner LRU: each owner is capped at `maxSessionsPerOwner`. Sessions
|
|
62
|
+
* without an owner (stdio path) are exempt from per-owner quota.
|
|
63
|
+
* 2. Global LRU cap: enforces overall `getMaxSessions()` limit.
|
|
64
|
+
*
|
|
65
|
+
* The default session is never evicted at either stage.
|
|
55
66
|
*/
|
|
56
67
|
evictExcessSessions(sessions: Map<string, S>): void;
|
|
68
|
+
/** Per-owner quota enforcement: oldest sessions per owner bucket are evicted. */
|
|
69
|
+
private _evictPerOwnerOverflow;
|
|
70
|
+
/** Global LRU cap enforcement: evicts oldest until under `getMaxSessions()`. */
|
|
71
|
+
private _evictGlobalOverflow;
|
|
57
72
|
}
|
|
58
73
|
//# sourceMappingURL=SessionManager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../src/core/SessionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,
|
|
1
|
+
{"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../src/core/SessionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;AAG5D,gGAAgG;AAChG,MAAM,WAAW,WAAW;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,oBAAoB;IACpC,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,qEAAqE;IACrE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,qBAAa,cAAc,CAAC,CAAC,SAAS,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAe;IAC/C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,aAAa,CAA+C;gBAExD,MAAM,EAAE,oBAAoB;IASxC,qEAAqE;IACrE,IAAW,KAAK,IAAI,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAExD;IAED;;;OAGG;IACI,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;IAcxD,gDAAgD;IACzC,gBAAgB,IAAI,IAAI;IAO/B;;;OAGG;IACI,oBAAoB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;IAW3D;;;;;;;;;OASG;IACI,mBAAmB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,IAAI;IAK1D,iFAAiF;IACjF,OAAO,CAAC,sBAAsB;IA8B9B,gFAAgF;IAChF,OAAO,CAAC,oBAAoB;CAoB5B"}
|
|
@@ -4,6 +4,7 @@ class SessionManager {
|
|
|
4
4
|
_sessionTtlMs;
|
|
5
5
|
_cleanupIntervalMs;
|
|
6
6
|
_getMaxSessions;
|
|
7
|
+
_maxSessionsPerOwner;
|
|
7
8
|
_logger;
|
|
8
9
|
_cleanupTimer = null;
|
|
9
10
|
constructor(config){
|
|
@@ -11,6 +12,7 @@ class SessionManager {
|
|
|
11
12
|
this._sessionTtlMs = config.sessionTtlMs;
|
|
12
13
|
this._cleanupIntervalMs = config.cleanupIntervalMs;
|
|
13
14
|
this._getMaxSessions = config.getMaxSessions;
|
|
15
|
+
this._maxSessionsPerOwner = config.maxSessionsPerOwner ?? 50;
|
|
14
16
|
this._logger = config.logger ?? new NullLogger();
|
|
15
17
|
}
|
|
16
18
|
get timer() {
|
|
@@ -41,6 +43,37 @@ class SessionManager {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
evictExcessSessions(sessions) {
|
|
46
|
+
this._evictPerOwnerOverflow(sessions);
|
|
47
|
+
this._evictGlobalOverflow(sessions);
|
|
48
|
+
}
|
|
49
|
+
_evictPerOwnerOverflow(sessions) {
|
|
50
|
+
const ownerCounts = new Map();
|
|
51
|
+
for (const [key, session] of sessions)if (key !== this._defaultSessionId) {
|
|
52
|
+
if (void 0 !== session.owner) ownerCounts.set(session.owner, (ownerCounts.get(session.owner) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
const maxPerOwner = this._maxSessionsPerOwner;
|
|
55
|
+
for (const [owner, count] of ownerCounts){
|
|
56
|
+
if (count <= maxPerOwner) continue;
|
|
57
|
+
const ownerSessions = [];
|
|
58
|
+
for (const [key, session] of sessions)if (key !== this._defaultSessionId) {
|
|
59
|
+
if (session.owner === owner) ownerSessions.push([
|
|
60
|
+
key,
|
|
61
|
+
session
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
ownerSessions.sort(([, a], [, b])=>a.lastAccessedAt - b.lastAccessedAt);
|
|
65
|
+
const toEvict = count - maxPerOwner;
|
|
66
|
+
for(let i = 0; i < toEvict && i < ownerSessions.length; i++){
|
|
67
|
+
const [evictKey] = ownerSessions[i];
|
|
68
|
+
sessions.delete(evictKey);
|
|
69
|
+
this._logger.info('Evicted oldest session (per-owner LRU)', {
|
|
70
|
+
sessionId: evictKey,
|
|
71
|
+
owner
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
_evictGlobalOverflow(sessions) {
|
|
44
77
|
const max = this._getMaxSessions();
|
|
45
78
|
while(sessions.size > max){
|
|
46
79
|
let oldestKey = null;
|
|
@@ -53,7 +86,7 @@ class SessionManager {
|
|
|
53
86
|
}
|
|
54
87
|
if (null !== oldestKey) {
|
|
55
88
|
sessions.delete(oldestKey);
|
|
56
|
-
this._logger.info('Evicted oldest session (LRU)', {
|
|
89
|
+
this._logger.info('Evicted oldest session (global LRU)', {
|
|
57
90
|
sessionId: oldestKey
|
|
58
91
|
});
|
|
59
92
|
} else break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core/SessionManager.js","sources":["../../src/core/SessionManager.ts"],"sourcesContent":["/**\n * SessionManager â owns periodic session cleanup timer and TTL/LRU eviction logic.\n *\n * Extracted from HistoryManager. The session Map remains owned by HistoryManager\n * (passed by reference) so that public access patterns are preserved.\n *\n * @module SessionManager\n */\n\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\n\n/** Minimal session contract â anything with a `lastAccessedAt` timestamp. */\nexport interface SessionLike {\n\tlastAccessedAt: number;\n}\n\n/** Configuration options for SessionManager. */\nexport interface SessionManagerConfig {\n\t/** Default session key that must never be evicted. */\n\tdefaultSessionId: string;\n\t/** TTL for inactive sessions in milliseconds. */\n\tsessionTtlMs: number;\n\t/** Periodic cleanup interval in milliseconds. */\n\tcleanupIntervalMs: number;\n\t/** Returns the current MAX_SESSIONS limit (callable so tests can mutate). */\n\tgetMaxSessions: () => number;\n\tlogger?: Logger;\n}\n\n/**\n * Manages session lifecycle: periodic stale-session cleanup and LRU eviction\n * when the session count exceeds the configured maximum.\n */\nexport class SessionManager<S extends SessionLike> {\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _sessionTtlMs: number;\n\tprivate readonly _cleanupIntervalMs: number;\n\tprivate readonly _getMaxSessions: () => number;\n\tprivate readonly _logger: Logger;\n\tprivate _cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(config: SessionManagerConfig) {\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._sessionTtlMs = config.sessionTtlMs;\n\t\tthis._cleanupIntervalMs = config.cleanupIntervalMs;\n\t\tthis._getMaxSessions = config.getMaxSessions;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying cleanup timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._cleanupTimer;\n\t}\n\n\t/**\n\t * Starts the periodic session cleanup timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startCleanupTimer(sessions: Map<string, S>): void {\n\t\tif (this._cleanupTimer !== null) return;\n\t\tthis._cleanupTimer = setInterval(() => {\n\t\t\tthis.cleanupStaleSessions(sessions);\n\t\t}, this._cleanupIntervalMs);\n\t\tif (\n\t\t\tthis._cleanupTimer &&\n\t\t\ttypeof this._cleanupTimer === 'object' &&\n\t\t\t'unref' in this._cleanupTimer\n\t\t) {\n\t\t\tthis._cleanupTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic session cleanup timer. */\n\tpublic stopCleanupTimer(): void {\n\t\tif (this._cleanupTimer !== null) {\n\t\t\tclearInterval(this._cleanupTimer);\n\t\t\tthis._cleanupTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Evicts sessions that have been inactive longer than `sessionTtlMs`.\n\t * The default session is never evicted.\n\t */\n\tpublic cleanupStaleSessions(sessions: Map<string, S>): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, session] of sessions) {\n\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\tif (now - session.lastAccessedAt > this._sessionTtlMs) {\n\t\t\t\tsessions.delete(key);\n\t\t\t\tthis._logger.info('Evicted stale session', { sessionId: key });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Evicts oldest sessions when the configured
|
|
1
|
+
{"version":3,"file":"core/SessionManager.js","sources":["../../src/core/SessionManager.ts"],"sourcesContent":["/**\n * SessionManager â owns periodic session cleanup timer and TTL/LRU eviction logic.\n *\n * Extracted from HistoryManager. The session Map remains owned by HistoryManager\n * (passed by reference) so that public access patterns are preserved.\n *\n * @module SessionManager\n */\n\nimport type { Logger } from '../logger/StructuredLogger.js';\nimport { NullLogger } from '../logger/NullLogger.js';\n\n/** Minimal session contract â anything with a `lastAccessedAt` timestamp and optional owner. */\nexport interface SessionLike {\n\tlastAccessedAt: number;\n\t/** Owner identifier for per-owner LRU quota. Undefined for stdio/global sessions. */\n\towner?: string;\n}\n\n/** Configuration options for SessionManager. */\nexport interface SessionManagerConfig {\n\t/** Default session key that must never be evicted. */\n\tdefaultSessionId: string;\n\t/** TTL for inactive sessions in milliseconds. */\n\tsessionTtlMs: number;\n\t/** Periodic cleanup interval in milliseconds. */\n\tcleanupIntervalMs: number;\n\t/** Returns the current MAX_SESSIONS limit (callable so tests can mutate). */\n\tgetMaxSessions: () => number;\n\t/** Maximum sessions per owner (per-owner LRU bucket). @default 50 */\n\tmaxSessionsPerOwner?: number;\n\tlogger?: Logger;\n}\n\n/**\n * Manages session lifecycle: periodic stale-session cleanup and LRU eviction\n * when the session count exceeds the configured maximum.\n */\nexport class SessionManager<S extends SessionLike> {\n\tprivate readonly _defaultSessionId: string;\n\tprivate readonly _sessionTtlMs: number;\n\tprivate readonly _cleanupIntervalMs: number;\n\tprivate readonly _getMaxSessions: () => number;\n\tprivate readonly _maxSessionsPerOwner: number;\n\tprivate readonly _logger: Logger;\n\tprivate _cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n\tconstructor(config: SessionManagerConfig) {\n\t\tthis._defaultSessionId = config.defaultSessionId;\n\t\tthis._sessionTtlMs = config.sessionTtlMs;\n\t\tthis._cleanupIntervalMs = config.cleanupIntervalMs;\n\t\tthis._getMaxSessions = config.getMaxSessions;\n\t\tthis._maxSessionsPerOwner = config.maxSessionsPerOwner ?? 50;\n\t\tthis._logger = config.logger ?? new NullLogger();\n\t}\n\n\t/** Returns the underlying cleanup timer (for test introspection). */\n\tpublic get timer(): ReturnType<typeof setInterval> | null {\n\t\treturn this._cleanupTimer;\n\t}\n\n\t/**\n\t * Starts the periodic session cleanup timer. No-op if already started.\n\t * The timer is unref'd so it does not block process exit.\n\t */\n\tpublic startCleanupTimer(sessions: Map<string, S>): void {\n\t\tif (this._cleanupTimer !== null) return;\n\t\tthis._cleanupTimer = setInterval(() => {\n\t\t\tthis.cleanupStaleSessions(sessions);\n\t\t}, this._cleanupIntervalMs);\n\t\tif (\n\t\t\tthis._cleanupTimer &&\n\t\t\ttypeof this._cleanupTimer === 'object' &&\n\t\t\t'unref' in this._cleanupTimer\n\t\t) {\n\t\t\tthis._cleanupTimer.unref();\n\t\t}\n\t}\n\n\t/** Stops the periodic session cleanup timer. */\n\tpublic stopCleanupTimer(): void {\n\t\tif (this._cleanupTimer !== null) {\n\t\t\tclearInterval(this._cleanupTimer);\n\t\t\tthis._cleanupTimer = null;\n\t\t}\n\t}\n\n\t/**\n\t * Evicts sessions that have been inactive longer than `sessionTtlMs`.\n\t * The default session is never evicted.\n\t */\n\tpublic cleanupStaleSessions(sessions: Map<string, S>): void {\n\t\tconst now = Date.now();\n\t\tfor (const [key, session] of sessions) {\n\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\tif (now - session.lastAccessedAt > this._sessionTtlMs) {\n\t\t\t\tsessions.delete(key);\n\t\t\t\tthis._logger.info('Evicted stale session', { sessionId: key });\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Evicts oldest sessions when the configured maximums are exceeded.\n\t *\n\t * Two-stage policy:\n\t * 1. Per-owner LRU: each owner is capped at `maxSessionsPerOwner`. Sessions\n\t * without an owner (stdio path) are exempt from per-owner quota.\n\t * 2. Global LRU cap: enforces overall `getMaxSessions()` limit.\n\t *\n\t * The default session is never evicted at either stage.\n\t */\n\tpublic evictExcessSessions(sessions: Map<string, S>): void {\n\t\tthis._evictPerOwnerOverflow(sessions);\n\t\tthis._evictGlobalOverflow(sessions);\n\t}\n\n\t/** Per-owner quota enforcement: oldest sessions per owner bucket are evicted. */\n\tprivate _evictPerOwnerOverflow(sessions: Map<string, S>): void {\n\t\tconst ownerCounts = new Map<string, number>();\n\t\tfor (const [key, session] of sessions) {\n\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\tif (session.owner === undefined) continue;\n\t\t\townerCounts.set(session.owner, (ownerCounts.get(session.owner) ?? 0) + 1);\n\t\t}\n\n\t\tconst maxPerOwner = this._maxSessionsPerOwner;\n\t\tfor (const [owner, count] of ownerCounts) {\n\t\t\tif (count <= maxPerOwner) continue;\n\t\t\tconst ownerSessions: Array<[string, S]> = [];\n\t\t\tfor (const [key, session] of sessions) {\n\t\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\t\tif (session.owner !== owner) continue;\n\t\t\t\townerSessions.push([key, session]);\n\t\t\t}\n\t\t\townerSessions.sort(([, a], [, b]) => a.lastAccessedAt - b.lastAccessedAt);\n\t\t\tconst toEvict = count - maxPerOwner;\n\t\t\tfor (let i = 0; i < toEvict && i < ownerSessions.length; i++) {\n\t\t\t\tconst [evictKey] = ownerSessions[i]!;\n\t\t\t\tsessions.delete(evictKey);\n\t\t\t\tthis._logger.info('Evicted oldest session (per-owner LRU)', {\n\t\t\t\t\tsessionId: evictKey,\n\t\t\t\t\towner,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Global LRU cap enforcement: evicts oldest until under `getMaxSessions()`. */\n\tprivate _evictGlobalOverflow(sessions: Map<string, S>): void {\n\t\tconst max = this._getMaxSessions();\n\t\twhile (sessions.size > max) {\n\t\t\tlet oldestKey: string | null = null;\n\t\t\tlet oldestTime = Infinity;\n\t\t\tfor (const [key, session] of sessions) {\n\t\t\t\tif (key === this._defaultSessionId) continue;\n\t\t\t\tif (session.lastAccessedAt < oldestTime) {\n\t\t\t\t\toldestTime = session.lastAccessedAt;\n\t\t\t\t\toldestKey = key;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (oldestKey !== null) {\n\t\t\t\tsessions.delete(oldestKey);\n\t\t\t\tthis._logger.info('Evicted oldest session (global LRU)', { sessionId: oldestKey });\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n"],"names":["SessionManager","config","NullLogger","sessions","setInterval","clearInterval","now","Date","key","session","ownerCounts","Map","undefined","maxPerOwner","owner","count","ownerSessions","a","b","toEvict","i","evictKey","max","oldestKey","oldestTime","Infinity"],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module formatter
|
|
9
9
|
*/
|
|
10
|
+
import type { IThoughtFormatter } from './IThoughtFormatter.js';
|
|
10
11
|
import type { StepRecommendation } from './step.js';
|
|
11
12
|
import type { ThoughtData } from './thought.js';
|
|
12
13
|
/**
|
|
@@ -52,7 +53,7 @@ import type { ThoughtData } from './thought.js';
|
|
|
52
53
|
* console.log(output);
|
|
53
54
|
* ```
|
|
54
55
|
*/
|
|
55
|
-
export declare class ThoughtFormatter {
|
|
56
|
+
export declare class ThoughtFormatter implements IThoughtFormatter {
|
|
56
57
|
/**
|
|
57
58
|
* Formats a step recommendation into a readable string.
|
|
58
59
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThoughtFormatter.d.ts","sourceRoot":"","sources":["../../src/core/ThoughtFormatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"ThoughtFormatter.d.ts","sourceRoot":"","sources":["../../src/core/ThoughtFormatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IACzD;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACI,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM;IAuB7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACI,aAAa,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM;CA0GtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core/ThoughtFormatter.js","sources":["../../src/core/ThoughtFormatter.ts"],"sourcesContent":["/**\n * Display formatting for thoughts and recommendations.\n *\n * This module provides the `ThoughtFormatter` class which handles all\n * presentation logic for thought data, including clean, simple output\n * formatting and structured display of tool/skill recommendations.\n *\n * @module formatter\n */\n\nimport chalk from 'chalk';\nimport type { ThoughtType } from './reasoning.js';\nimport type { StepRecommendation } from './step.js';\nimport type { ThoughtData } from './thought.js';\n\n/**\n * Formatter for thought data and step recommendations.\n *\n * This class separates presentation concerns from business logic, providing\n * clean, readable output for thoughts with structured display of\n * tool and skill recommendations.\n *\n * @remarks\n * Output Format is clean and simple:\n * - đ Thought - Regular thought (blue)\n * - đŦ Hypothesis - Proposed explanation (magenta)\n * - â
Verification - Testing a hypothesis (green)\n * - đ Critique - Self-critique of reasoning (red)\n * - đ§Ŧ Synthesis - Combining thoughts/branches (cyan)\n * - đ§ Meta - Metacognitive observation (gray)\n * - đ Revision - Thought that revises a previous thought (yellow)\n * - đŋ Branch - Thought that creates a new branch (green)\n * @example\n * ```typescript\n * const formatter = new ThoughtFormatter();\n *\n * // Format a thought with recommendations\n * const output = formatter.formatThought({\n * thought: 'I need to analyze the data structure',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Analyze data structure',\n * recommended_tools: [{\n * tool_name: 'Read',\n * confidence: 0.95,\n * rationale: 'Direct file reading',\n * priority: 1,\n * suggested_inputs: { file_path: './data/schema.json' }\n * }],\n * expected_outcome: 'Understanding of data schema'\n * }\n * });\n *\n * console.log(output);\n * ```\n */\nexport class ThoughtFormatter {\n\t/**\n\t * Formats a step recommendation into a readable string.\n\t *\n\t * Creates a structured display of the step description, recommended tools,\n\t * recommended skills, expected outcome, and conditions for the next step.\n\t *\n\t * @param step - The step recommendation to format\n\t * @returns A formatted string representation of the recommendation\n\t *\n\t * @example\n\t * ```typescript\n\t * const step: StepRecommendation = {\n\t * step_description: 'Search for API endpoints',\n\t * recommended_tools: [{\n\t * tool_name: 'Grep',\n\t * confidence: 0.9,\n\t * rationale: 'Best for searching code patterns',\n\t * priority: 1,\n\t * suggested_inputs: { pattern: 'export.*function' }\n\t * }],\n\t * expected_outcome: 'List of all exported API functions',\n\t * next_step_conditions: ['If no results, try broader pattern']\n\t * };\n\t *\n\t * const formatted = formatter.formatRecommendation(step);\n\t * console.log(formatted);\n\t * ```\n\t */\n\tpublic formatRecommendation(step: StepRecommendation): string {\n\t\tconst parts: string[] = [];\n\n\t\t// Add tools if present\n\t\tif (step.recommended_tools?.length) {\n\t\t\tconst toolNames = step.recommended_tools.map((t) => t.tool_name).join(', ');\n\t\t\tparts.push(chalk.cyan(`Tools: ${toolNames}`));\n\t\t}\n\n\t\t// Add skills if present\n\t\tif (step.recommended_skills?.length) {\n\t\t\tconst skillNames = step.recommended_skills.map((s) => s.skill_name).join(', ');\n\t\t\tparts.push(chalk.green(`Skills: ${skillNames}`));\n\t\t}\n\n\t\t// Add expected outcome\n\t\tif (step.expected_outcome) {\n\t\t\tparts.push(chalk.gray(`â ${step.expected_outcome}`));\n\t\t}\n\n\t\treturn parts.join(' | ');\n\t}\n\n\t/**\n\t * Formats a thought into a clean, simple display.\n\t *\n\t * Creates a clean output containing the thought content with an appropriate\n\t * header indicating the thought type. Priority order for icon selection:\n\t * `is_revision` > `branch_from_thought` > `thought_type`.\n\t *\n\t * Supported `thought_type` icons:\n\t * - `'regular'` (or undefined): đ blue \"Thought\" (default)\n\t * - `'hypothesis'`: đŦ magenta \"Hypothesis\"\n\t * - `'verification'`: â
green \"Verification\"\n\t * - `'critique'`: đ red \"Critique\"\n\t * - `'synthesis'`: đ§Ŧ cyan \"Synthesis\"\n\t * - `'meta'`: đ§ gray \"Meta\"\n\t *\n\t * @param thoughtData - The thought data to format\n\t * @returns A formatted string with thought and recommendations\n\t *\n\t * @example\n\t * ```typescript\n\t * // Regular thought\n\t * const regular = formatter.formatThought({\n\t * thought: 'I should read the configuration file',\n\t * thought_number: 1,\n\t * total_thoughts: 3,\n\t * next_thought_needed: true\n\t * });\n\t * // Output: đ Thought 1/3: I should read the configuration file\n\t *\n\t * // With recommendation\n\t * const withRec = formatter.formatThought({\n\t * thought: 'I need to search the codebase',\n\t * thought_number: 1,\n\t * total_thoughts: 3,\n\t * next_thought_needed: true,\n\t * current_step: {\n\t * step_description: 'Search for files',\n\t * recommended_tools: [{ tool_name: 'Grep', priority: 1 }],\n\t * expected_outcome: 'List of matching files'\n\t * }\n\t * });\n\t * // Output:\n\t * // đ Thought 1/3: I need to search the codebase\n\t * // â Tools: Grep | List of matching files\n\t * ```\n\t */\n\tpublic formatThought(thoughtData: ThoughtData): string {\n\t\tconst {\n\t\t\tthought_number,\n\t\t\ttotal_thoughts,\n\t\t\tthought,\n\t\t\tis_revision,\n\t\t\trevises_thought,\n\t\t\tbranch_from_thought,\n\t\t\tcurrent_step,\n\t\t} = thoughtData;\n\n\t\tlet icon: string;\n\t\tlet label = 'Thought';\n\t\tlet suffix = '';\n\n\t\tif (is_revision) {\n\t\t\ticon = chalk.yellow('đ');\n\t\t\tlabel = 'Revision';\n\t\t\tsuffix = chalk.gray(` (revise #${revises_thought})`);\n\t\t} else if (branch_from_thought) {\n\t\t\ticon = chalk.green('đŋ');\n\t\t\tlabel = 'Branch';\n\t\t\tsuffix = chalk.gray(` (from #${branch_from_thought})`);\n\t\t} else {\n\t\t\tconst thoughtType: ThoughtType = thoughtData.thought_type ?? 'regular';\n\t\t\tswitch (thoughtType) {\n\t\t\t\tcase 'hypothesis':\n\t\t\t\t\ticon = chalk.magenta('đŦ');\n\t\t\t\t\tlabel = 'Hypothesis';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'verification':\n\t\t\t\t\ticon = chalk.green('â
');\n\t\t\t\t\tlabel = 'Verification';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'critique':\n\t\t\t\t\ticon = chalk.red('đ');\n\t\t\t\t\tlabel = 'Critique';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'synthesis':\n\t\t\t\t\ticon = chalk.cyan('đ§Ŧ');\n\t\t\t\t\tlabel = 'Synthesis';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'meta':\n\t\t\t\t\ticon = chalk.gray('đ§ ');\n\t\t\t\t\tlabel = 'Meta';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'tool_call':\n\t\t\t\t\ticon = chalk.yellow('đ§');\n\t\t\t\t\tlabel = 'Tool Call';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'tool_observation':\n\t\t\t\t\ticon = chalk.yellow('đī¸');\n\t\t\t\t\tlabel = 'Tool Observation';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'assumption':\n\t\t\t\t\ticon = chalk.yellow('đĄ');\n\t\t\t\t\tlabel = 'Assumption';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'decomposition':\n\t\t\t\t\ticon = chalk.cyan('đ§Š');\n\t\t\t\t\tlabel = 'Decomposition';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'backtrack':\n\t\t\t\t\ticon = chalk.red('âŠī¸');\n\t\t\t\t\tlabel = 'Backtrack';\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\ticon = chalk.blue('đ');\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// Build header: \"đ Thought 1/3: \"\n\t\tconst retractedTag = thoughtData.retracted ? chalk.red.strikethrough('[RETRACTED] ') : '';\n\t\tconst header = `${icon} ${label} ${thought_number}/${total_thoughts}${suffix}: ${retractedTag}`;\n\n\t\t// Build content lines\n\t\tconst lines: string[] = [];\n\n\t\t// Add the thought content\n\t\tlines.push(`${header}${thought}`);\n\n\t\t// Add recommendation if present\n\t\tif (current_step) {\n\t\t\tconst recommendation = this.formatRecommendation(current_step);\n\t\t\tlines.push(` ${recommendation}`);\n\t\t}\n\n\t\t// Add id if present (for DAG debugging)\n\t\tif (thoughtData.id) {\n\t\t\tlines.push(` ${chalk.gray(`đ ${thoughtData.id}`)}`);\n\t\t}\n\n\t\t// Add meta observation if present\n\t\tif (thoughtData.meta_observation) {\n\t\t\tlines.push(` ${chalk.gray(`đ ${thoughtData.meta_observation}`)}`);\n\t\t}\n\n\t\treturn lines.join('\\n');\n\t}\n}\n"],"names":["ThoughtFormatter","step","parts","toolNames","t","chalk","skillNames","s","thoughtData","thought_number","total_thoughts","thought","is_revision","revises_thought","branch_from_thought","current_step","icon","label","suffix","thoughtType","retractedTag","header","lines","recommendation"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core/ThoughtFormatter.js","sources":["../../src/core/ThoughtFormatter.ts"],"sourcesContent":["/**\n * Display formatting for thoughts and recommendations.\n *\n * This module provides the `ThoughtFormatter` class which handles all\n * presentation logic for thought data, including clean, simple output\n * formatting and structured display of tool/skill recommendations.\n *\n * @module formatter\n */\n\nimport chalk from 'chalk';\nimport type { IThoughtFormatter } from './IThoughtFormatter.js';\nimport type { ThoughtType } from './reasoning.js';\nimport type { StepRecommendation } from './step.js';\nimport type { ThoughtData } from './thought.js';\n\n/**\n * Formatter for thought data and step recommendations.\n *\n * This class separates presentation concerns from business logic, providing\n * clean, readable output for thoughts with structured display of\n * tool and skill recommendations.\n *\n * @remarks\n * Output Format is clean and simple:\n * - đ Thought - Regular thought (blue)\n * - đŦ Hypothesis - Proposed explanation (magenta)\n * - â
Verification - Testing a hypothesis (green)\n * - đ Critique - Self-critique of reasoning (red)\n * - đ§Ŧ Synthesis - Combining thoughts/branches (cyan)\n * - đ§ Meta - Metacognitive observation (gray)\n * - đ Revision - Thought that revises a previous thought (yellow)\n * - đŋ Branch - Thought that creates a new branch (green)\n * @example\n * ```typescript\n * const formatter = new ThoughtFormatter();\n *\n * // Format a thought with recommendations\n * const output = formatter.formatThought({\n * thought: 'I need to analyze the data structure',\n * thought_number: 1,\n * total_thoughts: 3,\n * next_thought_needed: true,\n * current_step: {\n * step_description: 'Analyze data structure',\n * recommended_tools: [{\n * tool_name: 'Read',\n * confidence: 0.95,\n * rationale: 'Direct file reading',\n * priority: 1,\n * suggested_inputs: { file_path: './data/schema.json' }\n * }],\n * expected_outcome: 'Understanding of data schema'\n * }\n * });\n *\n * console.log(output);\n * ```\n */\nexport class ThoughtFormatter implements IThoughtFormatter {\n\t/**\n\t * Formats a step recommendation into a readable string.\n\t *\n\t * Creates a structured display of the step description, recommended tools,\n\t * recommended skills, expected outcome, and conditions for the next step.\n\t *\n\t * @param step - The step recommendation to format\n\t * @returns A formatted string representation of the recommendation\n\t *\n\t * @example\n\t * ```typescript\n\t * const step: StepRecommendation = {\n\t * step_description: 'Search for API endpoints',\n\t * recommended_tools: [{\n\t * tool_name: 'Grep',\n\t * confidence: 0.9,\n\t * rationale: 'Best for searching code patterns',\n\t * priority: 1,\n\t * suggested_inputs: { pattern: 'export.*function' }\n\t * }],\n\t * expected_outcome: 'List of all exported API functions',\n\t * next_step_conditions: ['If no results, try broader pattern']\n\t * };\n\t *\n\t * const formatted = formatter.formatRecommendation(step);\n\t * console.log(formatted);\n\t * ```\n\t */\n\tpublic formatRecommendation(step: StepRecommendation): string {\n\t\tconst parts: string[] = [];\n\n\t\t// Add tools if present\n\t\tif (step.recommended_tools?.length) {\n\t\t\tconst toolNames = step.recommended_tools.map((t) => t.tool_name).join(', ');\n\t\t\tparts.push(chalk.cyan(`Tools: ${toolNames}`));\n\t\t}\n\n\t\t// Add skills if present\n\t\tif (step.recommended_skills?.length) {\n\t\t\tconst skillNames = step.recommended_skills.map((s) => s.skill_name).join(', ');\n\t\t\tparts.push(chalk.green(`Skills: ${skillNames}`));\n\t\t}\n\n\t\t// Add expected outcome\n\t\tif (step.expected_outcome) {\n\t\t\tparts.push(chalk.gray(`â ${step.expected_outcome}`));\n\t\t}\n\n\t\treturn parts.join(' | ');\n\t}\n\n\t/**\n\t * Formats a thought into a clean, simple display.\n\t *\n\t * Creates a clean output containing the thought content with an appropriate\n\t * header indicating the thought type. Priority order for icon selection:\n\t * `is_revision` > `branch_from_thought` > `thought_type`.\n\t *\n\t * Supported `thought_type` icons:\n\t * - `'regular'` (or undefined): đ blue \"Thought\" (default)\n\t * - `'hypothesis'`: đŦ magenta \"Hypothesis\"\n\t * - `'verification'`: â
green \"Verification\"\n\t * - `'critique'`: đ red \"Critique\"\n\t * - `'synthesis'`: đ§Ŧ cyan \"Synthesis\"\n\t * - `'meta'`: đ§ gray \"Meta\"\n\t *\n\t * @param thoughtData - The thought data to format\n\t * @returns A formatted string with thought and recommendations\n\t *\n\t * @example\n\t * ```typescript\n\t * // Regular thought\n\t * const regular = formatter.formatThought({\n\t * thought: 'I should read the configuration file',\n\t * thought_number: 1,\n\t * total_thoughts: 3,\n\t * next_thought_needed: true\n\t * });\n\t * // Output: đ Thought 1/3: I should read the configuration file\n\t *\n\t * // With recommendation\n\t * const withRec = formatter.formatThought({\n\t * thought: 'I need to search the codebase',\n\t * thought_number: 1,\n\t * total_thoughts: 3,\n\t * next_thought_needed: true,\n\t * current_step: {\n\t * step_description: 'Search for files',\n\t * recommended_tools: [{ tool_name: 'Grep', priority: 1 }],\n\t * expected_outcome: 'List of matching files'\n\t * }\n\t * });\n\t * // Output:\n\t * // đ Thought 1/3: I need to search the codebase\n\t * // â Tools: Grep | List of matching files\n\t * ```\n\t */\n\tpublic formatThought(thoughtData: ThoughtData): string {\n\t\tconst {\n\t\t\tthought_number,\n\t\t\ttotal_thoughts,\n\t\t\tthought,\n\t\t\tis_revision,\n\t\t\trevises_thought,\n\t\t\tbranch_from_thought,\n\t\t\tcurrent_step,\n\t\t} = thoughtData;\n\n\t\tlet icon: string;\n\t\tlet label = 'Thought';\n\t\tlet suffix = '';\n\n\t\tif (is_revision) {\n\t\t\ticon = chalk.yellow('đ');\n\t\t\tlabel = 'Revision';\n\t\t\tsuffix = chalk.gray(` (revise #${revises_thought})`);\n\t\t} else if (branch_from_thought) {\n\t\t\ticon = chalk.green('đŋ');\n\t\t\tlabel = 'Branch';\n\t\t\tsuffix = chalk.gray(` (from #${branch_from_thought})`);\n\t\t} else {\n\t\t\tconst thoughtType: ThoughtType = thoughtData.thought_type ?? 'regular';\n\t\t\tswitch (thoughtType) {\n\t\t\t\tcase 'hypothesis':\n\t\t\t\t\ticon = chalk.magenta('đŦ');\n\t\t\t\t\tlabel = 'Hypothesis';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'verification':\n\t\t\t\t\ticon = chalk.green('â
');\n\t\t\t\t\tlabel = 'Verification';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'critique':\n\t\t\t\t\ticon = chalk.red('đ');\n\t\t\t\t\tlabel = 'Critique';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'synthesis':\n\t\t\t\t\ticon = chalk.cyan('đ§Ŧ');\n\t\t\t\t\tlabel = 'Synthesis';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'meta':\n\t\t\t\t\ticon = chalk.gray('đ§ ');\n\t\t\t\t\tlabel = 'Meta';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'tool_call':\n\t\t\t\t\ticon = chalk.yellow('đ§');\n\t\t\t\t\tlabel = 'Tool Call';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'tool_observation':\n\t\t\t\t\ticon = chalk.yellow('đī¸');\n\t\t\t\t\tlabel = 'Tool Observation';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'assumption':\n\t\t\t\t\ticon = chalk.yellow('đĄ');\n\t\t\t\t\tlabel = 'Assumption';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'decomposition':\n\t\t\t\t\ticon = chalk.cyan('đ§Š');\n\t\t\t\t\tlabel = 'Decomposition';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'backtrack':\n\t\t\t\t\ticon = chalk.red('âŠī¸');\n\t\t\t\t\tlabel = 'Backtrack';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'regular':\n\t\t\t\t\ticon = chalk.blue('đ');\n\t\t\t\t\tbreak;\n\t\t\t\tdefault: {\n\t\t\t\t\tconst _exhaust: never = thoughtType;\n\t\t\t\t\tvoid _exhaust;\n\t\t\t\t\ticon = chalk.blue('đ');\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Build header: \"đ Thought 1/3: \"\n\t\tconst retractedTag = thoughtData.retracted ? chalk.red.strikethrough('[RETRACTED] ') : '';\n\t\tconst header = `${icon} ${label} ${thought_number}/${total_thoughts}${suffix}: ${retractedTag}`;\n\n\t\t// Build content lines\n\t\tconst lines: string[] = [];\n\n\t\t// Add the thought content\n\t\tlines.push(`${header}${thought}`);\n\n\t\t// Add recommendation if present\n\t\tif (current_step) {\n\t\t\tconst recommendation = this.formatRecommendation(current_step);\n\t\t\tlines.push(` ${recommendation}`);\n\t\t}\n\n\t\t// Add id if present (for DAG debugging)\n\t\tif (thoughtData.id) {\n\t\t\tlines.push(` ${chalk.gray(`đ ${thoughtData.id}`)}`);\n\t\t}\n\n\t\t// Add meta observation if present\n\t\tif (thoughtData.meta_observation) {\n\t\t\tlines.push(` ${chalk.gray(`đ ${thoughtData.meta_observation}`)}`);\n\t\t}\n\n\t\treturn lines.join('\\n');\n\t}\n}\n"],"names":["ThoughtFormatter","step","parts","toolNames","t","chalk","skillNames","s","thoughtData","thought_number","total_thoughts","thought","is_revision","revises_thought","branch_from_thought","current_step","icon","label","suffix","thoughtType","retractedTag","header","lines","recommendation"],"mappings":"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"}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import type { Logger } from '../logger/StructuredLogger.js';
|
|
11
11
|
import type { ISuspensionStore } from '../contracts/suspension.js';
|
|
12
12
|
import type { IReasoningStrategy } from '../contracts/strategy.js';
|
|
13
|
-
import type
|
|
13
|
+
import { type FeatureFlags } from '../contracts/features.js';
|
|
14
|
+
import type { ISessionLock, IToolRegistry } from '../contracts/interfaces.js';
|
|
14
15
|
import type { IHistoryManager } from './IHistoryManager.js';
|
|
15
16
|
import type { ThoughtData } from './thought.js';
|
|
16
17
|
import type { ThoughtEvaluator } from './ThoughtEvaluator.js';
|
|
@@ -64,7 +65,9 @@ export declare class ThoughtProcessor {
|
|
|
64
65
|
private readonly strategy;
|
|
65
66
|
private readonly _compressionService?;
|
|
66
67
|
private readonly _suspensionStore?;
|
|
68
|
+
private readonly _toolRegistry?;
|
|
67
69
|
private readonly _features;
|
|
70
|
+
private readonly _sessionLock?;
|
|
68
71
|
/** Logger for debugging and monitoring. */
|
|
69
72
|
private _logger;
|
|
70
73
|
/** Evaluator for quality signal computation. */
|
|
@@ -83,8 +86,12 @@ export declare class ThoughtProcessor {
|
|
|
83
86
|
* @param logger - Optional logger for diagnostics (defaults to NullLogger)
|
|
84
87
|
* @param strategy - Reasoning strategy controlling next-action decisions (defaults to SequentialStrategy)
|
|
85
88
|
* @param compressionService - Optional compression service for auto-compression on terminate
|
|
89
|
+
* @param suspensionStore - Optional suspension store enabling tool interleave
|
|
90
|
+
* @param toolRegistry - Optional tool registry for tool_name allowlist validation (required when toolInterleave is enabled)
|
|
91
|
+
* @param features - Optional feature flags (defaults to DEFAULT_FLAGS â all opt-in flags off)
|
|
92
|
+
* @param sessionLock - Optional per-session async lock; when provided, `process()` runs under it
|
|
86
93
|
*/
|
|
87
|
-
constructor(historyManager: IHistoryManager, thoughtFormatter: ThoughtFormatter, thoughtEvaluator: ThoughtEvaluator, logger?: Logger, strategy?: IReasoningStrategy, _compressionService?: CompressionService | undefined, _suspensionStore?: ISuspensionStore | undefined, _features?: FeatureFlags);
|
|
94
|
+
constructor(historyManager: IHistoryManager, thoughtFormatter: ThoughtFormatter, thoughtEvaluator: ThoughtEvaluator, logger?: Logger, strategy?: IReasoningStrategy, _compressionService?: CompressionService | undefined, _suspensionStore?: ISuspensionStore | undefined, _toolRegistry?: IToolRegistry | undefined, _features?: FeatureFlags, _sessionLock?: ISessionLock | undefined);
|
|
88
95
|
/**
|
|
89
96
|
* Internal logging method.
|
|
90
97
|
* @param message - The message to log
|
|
@@ -152,6 +159,7 @@ export declare class ThoughtProcessor {
|
|
|
152
159
|
* ```
|
|
153
160
|
*/
|
|
154
161
|
process(input: ThoughtData): Promise<CallToolResult>;
|
|
162
|
+
private _processInner;
|
|
155
163
|
/**
|
|
156
164
|
* Run the configured reasoning strategy and return its decision.
|
|
157
165
|
* Strategy errors degrade to `{ action: 'continue' }`.
|
|
@@ -165,7 +173,7 @@ export declare class ThoughtProcessor {
|
|
|
165
173
|
* @private
|
|
166
174
|
*/
|
|
167
175
|
private _findBranchRoot;
|
|
168
|
-
/**
|
|
176
|
+
/** Access the EdgeStore via IHistoryManager. @private */
|
|
169
177
|
private _getEdgeStore;
|
|
170
178
|
/**
|
|
171
179
|
* Validates and normalizes thought input.
|
|
@@ -210,6 +218,17 @@ export declare class ThoughtProcessor {
|
|
|
210
218
|
* @private
|
|
211
219
|
*/
|
|
212
220
|
private _validateNewTypes;
|
|
221
|
+
/**
|
|
222
|
+
* Validate a tool_call's tool_name against the configured allowlist.
|
|
223
|
+
*
|
|
224
|
+
* Fails closed: if no tool registry was wired, all tool_call invocations are
|
|
225
|
+
* rejected. This prevents arbitrary tool name injection through the protocol.
|
|
226
|
+
*
|
|
227
|
+
* @param toolName - The tool name from the tool_call thought
|
|
228
|
+
* @throws {UnknownToolError} When no registry is wired or the tool is not registered
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
private _validateToolName;
|
|
213
232
|
/**
|
|
214
233
|
* Checks whether a given thought_number exists in the session history or any branch.
|
|
215
234
|
* @private
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThoughtProcessor.d.ts","sourceRoot":"","sources":["../../src/core/ThoughtProcessor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"ThoughtProcessor.d.ts","sourceRoot":"","sources":["../../src/core/ThoughtProcessor.ts"],"names":[],"mappings":"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"}
|