@iworkforces/tracelattice 2.0.5 → 2.0.7

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 CHANGED
@@ -204,6 +204,33 @@ Set `TRANSPORT_TYPE` to pick one:
204
204
 
205
205
  The Streamable HTTP endpoint defaults to `POST /mcp` for JSON-RPC requests and supports stateful sessions via the `Mcp-Session-Id` header. In stateful mode, `GET /mcp` may expose `text/event-stream` notifications for that session. The library also exposes `HttpTransport` for stateless JSON-RPC over HTTP, but the CLI does not select it with `TRANSPORT_TYPE`.
206
206
 
207
+ ### Stateless HTTP library API
208
+
209
+ Import the stateless transport from the package root. The package does not expose transport internals or deep import paths.
210
+
211
+ ```typescript
212
+ import {
213
+ createHttpTransport,
214
+ type HttpTransportOptions,
215
+ type ITransport,
216
+ } from '@iworkforces/tracelattice';
217
+ import type { McpServer } from 'tmcp';
218
+
219
+ const options: HttpTransportOptions = {
220
+ host: '127.0.0.1',
221
+ port: 9108,
222
+ path: '/messages',
223
+ };
224
+
225
+ export async function startHttpTransport(mcpServer: McpServer): Promise<ITransport> {
226
+ const transport = createHttpTransport(options);
227
+ await transport.connect(mcpServer);
228
+ return transport;
229
+ }
230
+ ```
231
+
232
+ Call and await `transport.stop()` during application shutdown.
233
+
207
234
  ## Development
208
235
 
209
236
  ```bash
package/dist/cli.js CHANGED
@@ -1993,6 +1993,20 @@ class AcceptedWorkTracker {
1993
1993
 
1994
1994
 
1995
1995
 
1996
+ function parseSessionRetentionOptions(options) {
1997
+ const { maxSessions, sessionIdleTimeoutMs, sessionSweepIntervalMs } = options;
1998
+ if (maxSessions === undefined && sessionIdleTimeoutMs === undefined && sessionSweepIntervalMs === undefined) {
1999
+ return null;
2000
+ }
2001
+ if (maxSessions === undefined || !Number.isFinite(maxSessions) || !Number.isInteger(maxSessions) || maxSessions <= 0 || sessionIdleTimeoutMs === undefined || !Number.isFinite(sessionIdleTimeoutMs) || !Number.isInteger(sessionIdleTimeoutMs) || sessionIdleTimeoutMs <= 0 || sessionSweepIntervalMs === undefined || !Number.isFinite(sessionSweepIntervalMs) || !Number.isInteger(sessionSweepIntervalMs) || sessionSweepIntervalMs <= 0) {
2002
+ throw new TypeError('maxSessions, sessionIdleTimeoutMs, and sessionSweepIntervalMs must be provided together as positive integers');
2003
+ }
2004
+ return {
2005
+ maxSessions,
2006
+ sessionIdleTimeoutMs,
2007
+ sessionSweepIntervalMs
2008
+ };
2009
+ }
1996
2010
  /**
1997
2011
  * Streamable HTTP Transport for MCP server.
1998
2012
  *
@@ -2046,8 +2060,11 @@ class AcceptedWorkTracker {
2046
2060
  _metricsProvider;
2047
2061
  _lifecycleFailureReporter;
2048
2062
  _acceptedWork;
2063
+ _sessionRetention;
2064
+ _sessionSweepIntervalId = null;
2049
2065
  _stopPromise = null;
2050
2066
  constructor(options = {}){
2067
+ const sessionRetention = parseSessionRetentionOptions(options);
2051
2068
  super(options);
2052
2069
  this._path = options.path ?? '/mcp';
2053
2070
  this._stateful = options.stateful ?? true;
@@ -2057,12 +2074,17 @@ class AcceptedWorkTracker {
2057
2074
  this._requestTimeout = options.requestTimeout ?? 30000;
2058
2075
  this._metrics = options.metrics;
2059
2076
  this._metricsProvider = options.metricsProvider ?? null;
2077
+ this._sessionRetention = sessionRetention;
2060
2078
  this._lifecycleFailureReporter = new LifecycleFailureReporter((error)=>{
2061
2079
  this.log('error', 'Streamable HTTP request lifecycle failed', {
2062
2080
  error: (0,errors/* .getErrorMessage */.u1)(error)
2063
2081
  });
2064
2082
  });
2065
2083
  this._acceptedWork = new AcceptedWorkTracker(this._lifecycleFailureReporter);
2084
+ if (this._stateful && sessionRetention) {
2085
+ this._sessionSweepIntervalId = setInterval(()=>this._sweepExpiredSessions(), sessionRetention.sessionSweepIntervalMs);
2086
+ this._sessionSweepIntervalId.unref();
2087
+ }
2066
2088
  }
2067
2089
  _requireMcpServer() {
2068
2090
  if (!this._mcpServer) {
@@ -2234,6 +2256,7 @@ class AcceptedWorkTracker {
2234
2256
  this._requestCount++;
2235
2257
  this._activeRequests++;
2236
2258
  const responseFinalizer = new ResponseFinalizer(res, this._lifecycleFailureReporter);
2259
+ let acceptedSession = null;
2237
2260
  const timeout = setTimeout(()=>{
2238
2261
  responseFinalizer.finalize((response)=>{
2239
2262
  this._sendJsonRpcError(response, 500, -32603, 'Request timeout');
@@ -2270,18 +2293,9 @@ class AcceptedWorkTracker {
2270
2293
  return;
2271
2294
  }
2272
2295
  const jsonRpcRequest = parseResult.output;
2273
- // Session management (stateful mode)
2274
- let sessionId;
2275
- if (this._stateful) {
2276
- const sessionResult = this._resolveSession(req);
2277
- if (typeof sessionResult !== 'string') {
2278
- responseFinalizer.finalize((response)=>{
2279
- this._sendJsonRpcError(response, sessionResult.statusCode, sessionResult.code, sessionResult.message);
2280
- });
2281
- return;
2282
- }
2283
- sessionId = sessionResult;
2284
- }
2296
+ const session = this._acceptPostSession(req, responseFinalizer);
2297
+ if (!session) return;
2298
+ acceptedSession = session.state;
2285
2299
  // Check if MCP server is ready
2286
2300
  if (!this._mcpServer) {
2287
2301
  responseFinalizer.finalize((response)=>{
@@ -2289,7 +2303,7 @@ class AcceptedWorkTracker {
2289
2303
  });
2290
2304
  return;
2291
2305
  }
2292
- await this._receiveAndFinalize(jsonRpcRequest, sessionId, responseFinalizer);
2306
+ await this._receiveAndFinalize(jsonRpcRequest, session.id, responseFinalizer);
2293
2307
  } catch (error) {
2294
2308
  responseFinalizer.finalize((response)=>{
2295
2309
  this._sendJsonRpcError(response, 200, -32603, 'Internal error', null, {
@@ -2297,10 +2311,35 @@ class AcceptedWorkTracker {
2297
2311
  });
2298
2312
  });
2299
2313
  } finally{
2314
+ if (acceptedSession) acceptedSession.acceptedPostCount--;
2300
2315
  clearTimeout(timeout);
2301
2316
  this._activeRequests--;
2302
2317
  }
2303
2318
  }
2319
+ _acceptPostSession(req, responseFinalizer) {
2320
+ if (!this._stateful) return {
2321
+ id: undefined,
2322
+ state: null
2323
+ };
2324
+ if (this.isShuttingDown) {
2325
+ responseFinalizer.finalize((response)=>{
2326
+ this._sendJsonRpcError(response, 503, -32603, 'Server is shutting down');
2327
+ });
2328
+ return null;
2329
+ }
2330
+ const session = this._resolveSession(req);
2331
+ if (!('state' in session)) {
2332
+ responseFinalizer.finalize((response)=>{
2333
+ this._sendJsonRpcError(response, session.statusCode, session.code, session.message);
2334
+ });
2335
+ return null;
2336
+ }
2337
+ session.state.acceptedPostCount++;
2338
+ return {
2339
+ id: session.id,
2340
+ state: session.state
2341
+ };
2342
+ }
2304
2343
  async _receiveAndFinalize(jsonRpcRequest, sessionId, responseFinalizer) {
2305
2344
  const owner = this._stateful ? sessionId ?? (0,__rspack_external_node_crypto_2e7c4b46.randomUUID)() : (0,__rspack_external_node_crypto_2e7c4b46.randomUUID)();
2306
2345
  const response = await (0,RequestContext/* .runWithContext */.Vo)({
@@ -2378,6 +2417,7 @@ class AcceptedWorkTracker {
2378
2417
  }));
2379
2418
  return;
2380
2419
  }
2420
+ session.lastActivityAt = Date.now();
2381
2421
  // Set SSE headers
2382
2422
  res.writeHead(200, {
2383
2423
  'Content-Type': 'text/event-stream',
@@ -2392,7 +2432,6 @@ class AcceptedWorkTracker {
2392
2432
  });
2393
2433
  // Track this notification stream
2394
2434
  session.notificationStreams.add(res);
2395
- session.lastActivityAt = Date.now();
2396
2435
  this._updateSessionMetrics();
2397
2436
  // Handle client disconnect
2398
2437
  req.on('close', ()=>{
@@ -2419,7 +2458,10 @@ class AcceptedWorkTracker {
2419
2458
  const session = this._sessions.get(asSessionId(headerSessionId));
2420
2459
  if (session) {
2421
2460
  session.lastActivityAt = Date.now();
2422
- return headerSessionId;
2461
+ return {
2462
+ id: session.id,
2463
+ state: session
2464
+ };
2423
2465
  }
2424
2466
  // Unknown session ID — per spec, return 404
2425
2467
  return {
@@ -2429,17 +2471,59 @@ class AcceptedWorkTracker {
2429
2471
  };
2430
2472
  }
2431
2473
  // No session header — create new session
2432
- const newSessionId = this._sessionIdGenerator();
2474
+ if (this._sessionRetention && this._sessions.size >= this._sessionRetention.maxSessions) {
2475
+ return {
2476
+ statusCode: 503,
2477
+ code: -32000,
2478
+ message: 'Session capacity reached'
2479
+ };
2480
+ }
2481
+ const newSessionId = asSessionId(this._sessionIdGenerator());
2482
+ const now = Date.now();
2433
2483
  const sessionState = {
2434
2484
  id: newSessionId,
2435
- createdAt: Date.now(),
2436
- lastActivityAt: Date.now(),
2437
- notificationStreams: new Set()
2485
+ createdAt: now,
2486
+ lastActivityAt: now,
2487
+ notificationStreams: new Set(),
2488
+ acceptedPostCount: 0
2438
2489
  };
2439
- this._sessions.set(asSessionId(newSessionId), sessionState);
2490
+ this._sessions.set(newSessionId, sessionState);
2440
2491
  this.log('info', `New session created: ${newSessionId}`);
2441
2492
  this._updateSessionMetrics();
2442
- return newSessionId;
2493
+ return {
2494
+ id: newSessionId,
2495
+ state: sessionState
2496
+ };
2497
+ }
2498
+ _sweepExpiredSessions() {
2499
+ if (!this._sessionRetention) return;
2500
+ const now = Date.now();
2501
+ for (const [sessionId, session] of this._sessions){
2502
+ if (session.acceptedPostCount === 0 && now - session.lastActivityAt >= this._sessionRetention.sessionIdleTimeoutMs) {
2503
+ this._disposeSession(sessionId);
2504
+ }
2505
+ }
2506
+ }
2507
+ _disposeSession(sessionId) {
2508
+ const session = this._sessions.get(sessionId);
2509
+ if (!session) return;
2510
+ this._sessions.delete(sessionId);
2511
+ for (const stream of session.notificationStreams){
2512
+ try {
2513
+ stream.end();
2514
+ } catch (error) {
2515
+ this._lifecycleFailureReporter.report(error);
2516
+ }
2517
+ }
2518
+ session.notificationStreams.clear();
2519
+ this._updateSessionMetrics();
2520
+ }
2521
+ _stopSessionSweep() {
2522
+ const interval = this._sessionSweepIntervalId;
2523
+ if (!interval) return;
2524
+ interval.unref();
2525
+ clearInterval(interval);
2526
+ this._sessionSweepIntervalId = null;
2443
2527
  }
2444
2528
  /**
2445
2529
  * Extract Mcp-Session-Id from request headers.
@@ -2547,19 +2631,11 @@ class AcceptedWorkTracker {
2547
2631
  if (this._stopPromise) return this._stopPromise;
2548
2632
  this._isShuttingDown = true;
2549
2633
  this._stopRateLimitCleanup();
2634
+ this._stopSessionSweep();
2550
2635
  const shutdownTimeout = timeout ?? 30000;
2551
- // Close all SSE notification streams
2552
- for (const session of this._sessions.values()){
2553
- for (const stream of session.notificationStreams){
2554
- try {
2555
- stream.end();
2556
- } catch {
2557
- // Ignore errors
2558
- }
2559
- }
2560
- session.notificationStreams.clear();
2636
+ for (const sessionId of this._sessions.keys()){
2637
+ this._disposeSession(sessionId);
2561
2638
  }
2562
- this._sessions.clear();
2563
2639
  const server = this._server;
2564
2640
  const serverClosed = new Promise((resolve, reject)=>{
2565
2641
  if (!server?.listening) {
@@ -93,6 +93,9 @@ export interface ConfigFileOptions {
93
93
  * Persistence configuration for storing history and state.
94
94
  */
95
95
  readonly persistence?: PersistenceConfig;
96
+ readonly persistenceBufferSize?: number;
97
+ readonly persistenceFlushInterval?: number;
98
+ readonly persistenceMaxRetries?: number;
96
99
  /**
97
100
  * Feature flag overrides. Each field can be set independently.
98
101
  * Can be overridden by `TRACELATTICE_FEATURES_*` environment variables.
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigLoader.d.ts","sourceRoot":"","sources":["../../src/config/ConfigLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AA8B9D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAExD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE;QACzB;;;WAGG;QACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QACtB;;WAEG;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEzC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAE1C;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAEtC;;;OAGG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,YAAY;IACxB,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAW;IAE/B;;;;;;;;;;;;;OAaG;gBACS,UAAU,CAAC,EAAE,MAAM;IAa/B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,IAAI,iBAAiB,GAAG,IAAI;IAiBhC;;;;;;;;;;;;;;;OAeG;IACI,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB;IA6F9E;;;;;;;OAOG;IACH,OAAO,CAAC,yBAAyB;IAuCjC,OAAO,CAAC,uBAAuB;IAW/B;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IAOpB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IASnB;;;;;;;;;;;;;;;;;;OAkBG;IACH,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,mBAAmB;CAerE"}
1
+ {"version":3,"file":"ConfigLoader.d.ts","sourceRoot":"","sources":["../../src/config/ConfigLoader.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAE5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAiC9D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAExD;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAE7B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAE9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE;QACzB;;;WAGG;QACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QACtB;;WAEG;QACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IAEF;;OAEG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAEzC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAExC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAE3C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAExC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAE1C;;;OAGG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAEtC;;;OAGG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAExC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,YAAY;IACxB,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAW;IAE/B;;;;;;;;;;;;;OAaG;gBACS,UAAU,CAAC,EAAE,MAAM;IAa/B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,IAAI,IAAI,iBAAiB,GAAG,IAAI;IAiBhC;;;;;;;;;;;;;;;OAeG;IACI,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB;IA6F9E;;;;;;;OAOG;IACH,OAAO,CAAC,yBAAyB;IAuCjC,OAAO,CAAC,uBAAuB;IAW/B;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;IAOpB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IASnB;;;;;;;;;;;;;;;;;;OAkBG;IACH,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,mBAAmB;CAkBrE"}
@@ -22,6 +22,9 @@ const ConfigFileOptionsSchema = __rspack_external_valibot.looseObject({
22
22
  maxSize: __rspack_external_valibot.optional(__rspack_external_valibot.number())
23
23
  })),
24
24
  persistence: __rspack_external_valibot.optional(__rspack_external_valibot.looseObject({})),
25
+ persistenceBufferSize: __rspack_external_valibot.optional(__rspack_external_valibot.number()),
26
+ persistenceFlushInterval: __rspack_external_valibot.optional(__rspack_external_valibot.number()),
27
+ persistenceMaxRetries: __rspack_external_valibot.optional(__rspack_external_valibot.number()),
25
28
  features: __rspack_external_valibot.optional(__rspack_external_valibot.looseObject({})),
26
29
  toolInterleaveTtlMs: __rspack_external_valibot.optional(__rspack_external_valibot.number()),
27
30
  toolInterleaveSweepMs: __rspack_external_valibot.optional(__rspack_external_valibot.number()),
@@ -174,6 +177,9 @@ class ConfigLoader {
174
177
  toolDirs: config.toolDirs,
175
178
  discoveryCache: config.discoveryCache,
176
179
  persistence: config.persistence,
180
+ persistenceBufferSize: config.persistenceBufferSize,
181
+ persistenceFlushInterval: config.persistenceFlushInterval,
182
+ persistenceMaxRetries: config.persistenceMaxRetries,
177
183
  features: config.features,
178
184
  toolInterleaveTtlMs: config.toolInterleaveTtlMs,
179
185
  toolInterleaveSweepMs: config.toolInterleaveSweepMs,
@@ -1 +1 @@
1
- {"version":3,"file":"config/ConfigLoader.js","sources":["../../src/config/ConfigLoader.ts"],"sourcesContent":["/**\n * Configuration file loading with environment variable override support.\n *\n * This module provides the `ConfigLoader` class which handles loading configuration\n * from YAML and JSON files in standard locations, with automatic environment variable\n * overrides for all settings.\n *\n * @module config\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport * as v from 'valibot';\nimport type { PersistenceConfig } from '../contracts/PersistenceBackend.js';\nimport { ConfigurationError, getErrorMessage } from '../errors.js';\nimport type { FeatureFlags } from '../contracts/features.js';\nimport type { ServerConfigOptions } from '../ServerConfig.js';\n\n/**\n * Lenient runtime schema for config files. Only validates top-level shape;\n * unknown extra fields are preserved by Valibot's default object behavior\n * being strict is avoided by using `v.looseObject`. All fields are optional.\n */\nconst ConfigFileOptionsSchema = v.looseObject({\n\tmaxHistorySize: v.optional(v.number()),\n\tmaxBranches: v.optional(v.number()),\n\tmaxBranchSize: v.optional(v.number()),\n\tlogLevel: v.optional(v.picklist(['debug', 'info', 'warn', 'error'])),\n\tprettyLog: v.optional(v.boolean()),\n\tskillDirs: v.optional(v.array(v.string())),\n\ttoolDirs: v.optional(v.array(v.string())),\n\tdiscoveryCache: v.optional(\n\t\tv.looseObject({\n\t\t\tttl: v.optional(v.number()),\n\t\t\tmaxSize: v.optional(v.number()),\n\t\t})\n\t),\n\tpersistence: v.optional(v.looseObject({})),\n\tfeatures: v.optional(v.looseObject({})),\n\ttoolInterleaveTtlMs: v.optional(v.number()),\n\ttoolInterleaveSweepMs: v.optional(v.number()),\n\tmaxSessionsPerOwner: v.optional(v.number()),\n});\n\ntype Mutable<T> = { -readonly [K in keyof T]: T[K] };\n\n/**\n * Configuration options loaded from config files.\n *\n * These options represent the structure of configuration files (JSON or YAML)\n * that can be loaded from standard locations. All values can be overridden\n * by environment variables.\n *\n * @example\n * ```yaml\n * # .claude/config.yaml\n * maxHistorySize: 500\n * maxBranches: 25\n * logLevel: debug\n * prettyLog: true\n * skillDirs:\n * - ./custom-skills\n * toolDirs:\n * - ./custom-tools\n * discoveryCache:\n * ttl: 600000\n * maxSize: 200\n * persistence:\n * enabled: true\n * backend: sqlite\n * options:\n * dbPath: ./data/history.db\n * ```\n */\nexport interface ConfigFileOptions {\n\t/**\n\t * Maximum number of thoughts to keep in history.\n\t * Can be overridden by `MAX_HISTORY_SIZE` environment variable.\n\t */\n\treadonly maxHistorySize?: number;\n\n\t/**\n\t * Maximum number of branches to maintain.\n\t * Can be overridden by `MAX_BRANCHES` environment variable.\n\t */\n\treadonly maxBranches?: number;\n\n\t/**\n\t * Maximum size of each branch.\n\t * Can be overridden by `MAX_BRANCH_SIZE` environment variable.\n\t */\n\treadonly maxBranchSize?: number;\n\n\t/**\n\t * Logging level for the application.\n\t * Can be overridden by `LOG_LEVEL` environment variable.\n\t */\n\treadonly logLevel?: 'debug' | 'info' | 'warn' | 'error';\n\n\t/**\n\t * Whether to enable pretty (formatted) logging output.\n\t * Can be overridden by `PRETTY_LOG` environment variable (set to \"false\" to disable).\n\t */\n\treadonly prettyLog?: boolean;\n\n\t/**\n\t * Directory paths to search for skills.\n\t * Can be overridden by `SKILL_DIRS` environment variable (colon-separated).\n\t */\n\treadonly skillDirs?: string[];\n\n\t/**\n\t * Directory paths to search for tools.\n\t * Can be overridden by `TOOL_DIRS` environment variable (colon-separated).\n\t */\n\treadonly toolDirs?: string[];\n\n\t/**\n\t * Discovery cache configuration.\n\t * Can be overridden by `DISCOVERY_CACHE_TTL` and `DISCOVERY_CACHE_MAX_SIZE` environment variables.\n\t */\n\treadonly discoveryCache?: {\n\t\t/**\n\t\t * Time-to-live for cache entries in milliseconds.\n\t\t * Environment variable `DISCOVERY_CACHE_TTL` accepts seconds.\n\t\t */\n\t\treadonly ttl?: number;\n\t\t/**\n\t\t * Maximum number of entries in the cache.\n\t\t */\n\t\treadonly maxSize?: number;\n\t};\n\n\t/**\n\t * Persistence configuration for storing history and state.\n\t */\n\treadonly persistence?: PersistenceConfig;\n\n\t/**\n\t * Feature flag overrides. Each field can be set independently.\n\t * Can be overridden by `TRACELATTICE_FEATURES_*` environment variables.\n\t */\n\treadonly features?: Partial<FeatureFlags>;\n\n\t/**\n\t * TTL in milliseconds for suspended tool-interleave entries.\n\t * Can be overridden by `TRACELATTICE_TOOL_INTERLEAVE_TTL_MS` environment variable.\n\t */\n\treadonly toolInterleaveTtlMs?: number;\n\n\t/**\n\t * Sweep interval in milliseconds for SuspensionStore expiration cleanup.\n\t * Can be overridden by `TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS` environment variable.\n\t */\n\treadonly toolInterleaveSweepMs?: number;\n\n\t/**\n\t * Maximum sessions per owner. Per-owner LRU bucket prevents one user from\n\t * consuming all session slots.\n\t * Can be overridden by `SESSION_MAX_PER_OWNER` environment variable.\n\t */\n\treadonly maxSessionsPerOwner?: number;\n}\n\n/**\n * Loads configuration from files with environment variable overrides.\n *\n * This class searches for configuration files in standard locations and applies\n * environment variable overrides. Files are searched in priority order, with the\n * first match being used. Environment variables always take precedence over file values.\n *\n * @remarks\n * **Config File Search Order (priority):**\n * 1. Custom path (if provided to constructor)\n * 2. `.claude/config.json` (project-local)\n * 3. `.claude/config.yaml` (project-local)\n * 4. `.claude/config.yml` (project-local)\n * 5. `~/.claude/config.json` (user-global)\n * 6. `~/.claude/config.yaml` (user-global)\n * 7. `~/.claude/config.yml` (user-global)\n *\n * **Environment Variable Overrides:**\n * | Variable | Type | Description |\n * |----------|------|-------------|\n * | `MAX_HISTORY_SIZE` | number | Max thoughts in history |\n * | `MAX_BRANCHES` | number | Max number of branches |\n * | `MAX_BRANCH_SIZE` | number | Max size of each branch |\n * | `LOG_LEVEL` | string | Logging level (debug/info/warn/error) |\n * | `PRETTY_LOG` | string | \"false\" to disable pretty logging |\n * | `SKILL_DIRS` | string | Colon-separated directory paths |\n * | `TOOL_DIRS` | string | Colon-separated directory paths |\n * | `DISCOVERY_CACHE_TTL` | number | TTL in seconds (converted to ms) |\n * | `DISCOVERY_CACHE_MAX_SIZE` | number | Max cache entries |\n *\n * @example\n * ```typescript\n * // Use default search paths\n * const loader1 = new ConfigLoader();\n * const config1 = loader1.load();\n *\n * // Use custom config path\n * const loader2 = new ConfigLoader('./my-config.yaml');\n * const config2 = loader2.load();\n *\n * // Convert to ServerConfig options\n * const serverOptions = loader2.toServerConfigOptions(config2);\n * ```\n */\nexport class ConfigLoader {\n\t/** Array of config file paths to search, in priority order. */\n\tprivate _configPaths: string[];\n\n\t/**\n\t * Creates a new ConfigLoader instance.\n\t *\n\t * @param customPath - Optional custom config file path. If provided, only this path will be checked.\n\t *\n\t * @example\n\t * ```typescript\n\t * // Use default search paths\n\t * const loader1 = new ConfigLoader();\n\t *\n\t * // Use a specific config file\n\t * const loader2 = new ConfigLoader('./custom-config.json');\n\t * ```\n\t */\n\tconstructor(customPath?: string) {\n\t\tthis._configPaths = customPath\n\t\t\t? [customPath]\n\t\t\t: [\n\t\t\t\t\t'.claude/config.json',\n\t\t\t\t\t'.claude/config.yaml',\n\t\t\t\t\t'.claude/config.yml',\n\t\t\t\t\tjoin(homedir(), '.claude/config.json'),\n\t\t\t\t\tjoin(homedir(), '.claude/config.yaml'),\n\t\t\t\t\tjoin(homedir(), '.claude/config.yml'),\n\t\t\t\t];\n\t}\n\n\t/**\n\t * Loads configuration from files and applies environment overrides.\n\t *\n\t * Searches for config files in the configured paths (in priority order),\n\t * parses the first match, and applies environment variable overrides.\n\t * Returns null if no config file is found and no environment overrides are set.\n\t *\n\t * @returns The loaded configuration with environment overrides applied, or null if no config found\n\t *\n\t * @example\n\t * ```typescript\n\t * const loader = new ConfigLoader();\n\t * const config = loader.load();\n\t *\n\t * if (config) {\n\t * console.log('Max history size:', config.maxHistorySize);\n\t * console.log('Log level:', config.logLevel);\n\t * }\n\t * ```\n\t */\n\tload(): ConfigFileOptions | null {\n\t\tlet config: ConfigFileOptions | null = null;\n\n\t\tfor (const configPath of this._configPaths) {\n\t\t\tif (existsSync(configPath)) {\n\t\t\t\ttry {\n\t\t\t\t\tconfig = this.parseConfig(configPath);\n\t\t\t\t\tbreak;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`Failed to load config from ${configPath}:`, getErrorMessage(error));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.applyEnvironmentOverrides(config || {});\n\t}\n\n\t/**\n\t * Applies environment variable overrides to the configuration.\n\t *\n\t * Environment variables take precedence over file-based configuration.\n\t * Supported environment variables:\n\t * - `MAX_HISTORY_SIZE`, `MAX_BRANCHES`, `MAX_BRANCH_SIZE` (numbers)\n\t * - `LOG_LEVEL` (debug/info/warn/error)\n\t * - `PRETTY_LOG` (\"false\" to disable)\n\t * - `SKILL_DIRS` (colon-separated paths)\n\t * - `TOOL_DIRS` (colon-separated paths)\n\t * - `DISCOVERY_CACHE_TTL` (in seconds, converted to ms)\n\t * - `DISCOVERY_CACHE_MAX_SIZE` (number)\n\t *\n\t * @param config - The configuration to apply overrides to\n\t * @returns A new configuration object with environment overrides applied\n\t */\n\tpublic applyEnvironmentOverrides(config: ConfigFileOptions): ConfigFileOptions {\n\t\tconst result: Mutable<ConfigFileOptions> = {\n\t\t\t...config,\n\t\t\t...(config.skillDirs === undefined ? {} : { skillDirs: [...config.skillDirs] }),\n\t\t\t...(config.toolDirs === undefined ? {} : { toolDirs: [...config.toolDirs] }),\n\t\t\t...(config.discoveryCache === undefined\n\t\t\t\t? {}\n\t\t\t\t: { discoveryCache: { ...config.discoveryCache } }),\n\t\t\t...(config.persistence === undefined\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\tpersistence: {\n\t\t\t\t\t\t\t...config.persistence,\n\t\t\t\t\t\t\t...(config.persistence.options === undefined\n\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t: { options: { ...config.persistence.options } }),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t...(config.features === undefined ? {} : { features: { ...config.features } }),\n\t\t};\n\n\t\tif (process.env.MAX_HISTORY_SIZE !== undefined) {\n\t\t\tresult.maxHistorySize = this.parseEnvironmentInteger(\n\t\t\t\t'MAX_HISTORY_SIZE',\n\t\t\t\tprocess.env.MAX_HISTORY_SIZE\n\t\t\t);\n\t\t}\n\t\tif (process.env.MAX_BRANCHES !== undefined) {\n\t\t\tresult.maxBranches = this.parseEnvironmentInteger('MAX_BRANCHES', process.env.MAX_BRANCHES);\n\t\t}\n\t\tif (process.env.MAX_BRANCH_SIZE !== undefined) {\n\t\t\tresult.maxBranchSize = this.parseEnvironmentInteger(\n\t\t\t\t'MAX_BRANCH_SIZE',\n\t\t\t\tprocess.env.MAX_BRANCH_SIZE\n\t\t\t);\n\t\t}\n\t\tif (\n\t\t\tprocess.env.LOG_LEVEL &&\n\t\t\t['debug', 'info', 'warn', 'error'].includes(process.env.LOG_LEVEL)\n\t\t) {\n\t\t\tresult.logLevel = process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error';\n\t\t}\n\t\tif (process.env.PRETTY_LOG === 'false') {\n\t\t\tresult.prettyLog = false;\n\t\t}\n\t\tif (process.env.SKILL_DIRS !== undefined) {\n\t\t\tresult.skillDirs = process.env.SKILL_DIRS === '' ? [] : process.env.SKILL_DIRS.split(':');\n\t\t}\n\t\tif (process.env.TOOL_DIRS !== undefined) {\n\t\t\tresult.toolDirs = process.env.TOOL_DIRS === '' ? [] : process.env.TOOL_DIRS.split(':');\n\t\t}\n\t\tif (process.env.DISCOVERY_CACHE_TTL !== undefined) {\n\t\t\tconst seconds = this.parseEnvironmentInteger(\n\t\t\t\t'DISCOVERY_CACHE_TTL',\n\t\t\t\tprocess.env.DISCOVERY_CACHE_TTL\n\t\t\t);\n\t\t\tconst ttl = seconds * 1000;\n\t\t\tif (!Number.isSafeInteger(ttl)) {\n\t\t\t\tthrow new ConfigurationError('DISCOVERY_CACHE_TTL exceeds the safe integer range');\n\t\t\t}\n\t\t\tresult.discoveryCache = { ...result.discoveryCache, ttl };\n\t\t}\n\t\tif (process.env.DISCOVERY_CACHE_MAX_SIZE !== undefined) {\n\t\t\tconst maxSize = this.parseEnvironmentInteger(\n\t\t\t\t'DISCOVERY_CACHE_MAX_SIZE',\n\t\t\t\tprocess.env.DISCOVERY_CACHE_MAX_SIZE\n\t\t\t);\n\t\t\tresult.discoveryCache = { ...result.discoveryCache, maxSize };\n\t\t}\n\t\tif (process.env.TRACELATTICE_TOOL_INTERLEAVE_TTL_MS !== undefined) {\n\t\t\tresult.toolInterleaveTtlMs = this.parseEnvironmentInteger(\n\t\t\t\t'TRACELATTICE_TOOL_INTERLEAVE_TTL_MS',\n\t\t\t\tprocess.env.TRACELATTICE_TOOL_INTERLEAVE_TTL_MS\n\t\t\t);\n\t\t}\n\t\tif (process.env.TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS !== undefined) {\n\t\t\tresult.toolInterleaveSweepMs = this.parseEnvironmentInteger(\n\t\t\t\t'TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS',\n\t\t\t\tprocess.env.TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS\n\t\t\t);\n\t\t}\n\t\tif (process.env.SESSION_MAX_PER_OWNER !== undefined) {\n\t\t\tresult.maxSessionsPerOwner = this.parseEnvironmentInteger(\n\t\t\t\t'SESSION_MAX_PER_OWNER',\n\t\t\t\tprocess.env.SESSION_MAX_PER_OWNER\n\t\t\t);\n\t\t}\n\n\t\tthis.applyFeatureFlagOverrides(result);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Applies TRACELATTICE_FEATURES_* environment variable overrides for feature flags.\n\t * Booleans accept 'true'/'false'/'1'/'0' (case-insensitive).\n\t * Invalid reasoningStrategy values are rejected.\n\t *\n\t * @param result - Configuration object to mutate with feature flag overrides\n\t * @private\n\t */\n\tprivate applyFeatureFlagOverrides(result: Mutable<ConfigFileOptions>): void {\n\t\tconst boolMap: Record<string, Exclude<keyof FeatureFlags, 'reasoningStrategy'>> = {\n\t\t\tTRACELATTICE_FEATURES_DAG_EDGES: 'dagEdges',\n\t\t\tTRACELATTICE_FEATURES_CALIBRATION: 'calibration',\n\t\t\tTRACELATTICE_FEATURES_COMPRESSION: 'compression',\n\t\t\tTRACELATTICE_FEATURES_TOOL_INTERLEAVE: 'toolInterleave',\n\t\t\tTRACELATTICE_FEATURES_NEW_THOUGHT_TYPES: 'newThoughtTypes',\n\t\t\tTRACELATTICE_FEATURES_OUTCOME_RECORDING: 'outcomeRecording',\n\t\t};\n\t\tfor (const [envVar, key] of Object.entries(boolMap)) {\n\t\t\tconst raw = process.env[envVar];\n\t\t\tif (raw === undefined) continue;\n\t\t\tconst parsed = this.parseBoolean(raw);\n\t\t\tif (parsed === undefined) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`Invalid boolean value for ${envVar}: \"${raw}\" (expected true/false/1/0). Ignoring.`\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst features: { -readonly [K in keyof FeatureFlags]?: FeatureFlags[K] } =\n\t\t\t\tresult.features ?? {};\n\t\t\tfeatures[key] = parsed;\n\t\t\tresult.features = features;\n\t\t}\n\n\t\tconst strategyRaw = process.env.TRACELATTICE_FEATURES_REASONING_STRATEGY;\n\t\tif (strategyRaw !== undefined) {\n\t\t\tif (strategyRaw !== 'sequential' && strategyRaw !== 'tot') {\n\t\t\t\tthrow new ConfigurationError(\n\t\t\t\t\t`TRACELATTICE_FEATURES_REASONING_STRATEGY must be one of sequential, tot, got ${strategyRaw}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst features: { -readonly [K in keyof FeatureFlags]?: FeatureFlags[K] } =\n\t\t\t\tresult.features ?? {};\n\t\t\tfeatures.reasoningStrategy = strategyRaw;\n\t\t\tresult.features = features;\n\t\t}\n\t}\n\n\tprivate parseEnvironmentInteger(environmentName: string, raw: string): number {\n\t\tif (!/^(0|[1-9]\\d*)$/.test(raw)) {\n\t\t\tthrow new ConfigurationError(`${environmentName} must be an integer, got ${raw}`);\n\t\t}\n\t\tconst parsed = Number(raw);\n\t\tif (!Number.isSafeInteger(parsed)) {\n\t\t\tthrow new ConfigurationError(`${environmentName} must be a safe integer, got ${raw}`);\n\t\t}\n\t\treturn parsed;\n\t}\n\n\t/**\n\t * Parses a boolean from an environment variable string.\n\t * Accepts 'true'/'false'/'1'/'0' case-insensitively.\n\t *\n\t * @param raw - Raw environment variable string\n\t * @returns Parsed boolean, or undefined if the value is invalid\n\t * @private\n\t */\n\tprivate parseBoolean(raw: string): boolean | undefined {\n\t\tconst v = raw.trim().toLowerCase();\n\t\tif (v === 'true' || v === '1') return true;\n\t\tif (v === 'false' || v === '0') return false;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Parses a configuration file (JSON or YAML).\n\t *\n\t * Detects the file type by extension and uses the appropriate parser.\n\t * Supports `.json`, `.yaml`, and `.yml` file extensions.\n\t *\n\t * @param filePath - Path to the configuration file to parse\n\t * @returns The parsed configuration object\n\t * @throws {Error} If the file cannot be parsed\n\t * @private\n\t */\n\tprivate parseConfig(filePath: string): ConfigFileOptions {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\t\tconst ext = filePath.split('.').pop()?.toLowerCase();\n\n\t\tconst raw: unknown = ext === 'yaml' || ext === 'yml' ? parseYaml(content) : JSON.parse(content);\n\n\t\treturn v.parse(ConfigFileOptionsSchema, raw) as ConfigFileOptions;\n\t}\n\n\t/**\n\t * Converts file-based configuration to ServerConfig options.\n\t *\n\t * This is a convenience method for extracting the ServerConfig-relevant\n\t * options from a file-based configuration.\n\t *\n\t * @param config - The configuration to convert\n\t * @returns An object with ServerConfig-compatible options\n\t *\n\t * @example\n\t * ```typescript\n\t * const loader = new ConfigLoader();\n\t * const config = loader.load();\n\t * if (config) {\n\t * const serverOpts = loader.toServerConfigOptions(config);\n\t * const serverConfig = new ServerConfig(serverOpts);\n\t * }\n\t * ```\n\t */\n\ttoServerConfigOptions(config: ConfigFileOptions): ServerConfigOptions {\n\t\treturn {\n\t\t\tmaxHistorySize: config.maxHistorySize,\n\t\t\tmaxBranches: config.maxBranches,\n\t\t\tmaxBranchSize: config.maxBranchSize,\n\t\t\tskillDirs: config.skillDirs,\n\t\t\ttoolDirs: config.toolDirs,\n\t\t\tdiscoveryCache: config.discoveryCache,\n\t\t\tpersistence: config.persistence,\n\t\t\tfeatures: config.features,\n\t\t\ttoolInterleaveTtlMs: config.toolInterleaveTtlMs,\n\t\t\ttoolInterleaveSweepMs: config.toolInterleaveSweepMs,\n\t\t\tmaxSessionsPerOwner: config.maxSessionsPerOwner,\n\t\t};\n\t}\n}\n"],"names":["ConfigFileOptionsSchema","v","ConfigLoader","customPath","join","homedir","config","configPath","existsSync","error","console","getErrorMessage","result","undefined","process","seconds","ttl","Number","ConfigurationError","maxSize","boolMap","envVar","key","Object","raw","parsed","features","strategyRaw","environmentName","filePath","content","readFileSync","ext","parseYaml","JSON"],"mappings":";;;;;;AAyBA,MAAMA,0BAA0BC,0BAAAA,WAAa,CAAC;IAC7C,gBAAgBA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IACnC,aAAaA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IAChC,eAAeA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IAClC,UAAUA,0BAAAA,QAAU,CAACA,0BAAAA,QAAU,CAAC;QAAC;QAAS;QAAQ;QAAQ;KAAQ;IAClE,WAAWA,0BAAAA,QAAU,CAACA,0BAAAA,OAAS;IAC/B,WAAWA,0BAAAA,QAAU,CAACA,0BAAAA,KAAO,CAACA,0BAAAA,MAAQ;IACtC,UAAUA,0BAAAA,QAAU,CAACA,0BAAAA,KAAO,CAACA,0BAAAA,MAAQ;IACrC,gBAAgBA,0BAAAA,QAAU,CACzBA,0BAAAA,WAAa,CAAC;QACb,KAAKA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;QACxB,SAASA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IAC7B;IAED,aAAaA,0BAAAA,QAAU,CAACA,0BAAAA,WAAa,CAAC,CAAC;IACvC,UAAUA,0BAAAA,QAAU,CAACA,0BAAAA,WAAa,CAAC,CAAC;IACpC,qBAAqBA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IACxC,uBAAuBA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;IAC1C,qBAAqBA,0BAAAA,QAAU,CAACA,0BAAAA,MAAQ;AACzC;AAsKO,MAAMC;IAEJ,aAAuB;IAgB/B,YAAYC,UAAmB,CAAE;QAChC,IAAI,CAAC,YAAY,GAAGA,aACjB;YAACA;SAAW,GACZ;YACA;YACA;YACA;YACAC,KAAKC,WAAW;YAChBD,KAAKC,WAAW;YAChBD,KAAKC,WAAW;SAChB;IACJ;IAsBA,OAAiC;QAChC,IAAIC,SAAmC;QAEvC,KAAK,MAAMC,cAAc,IAAI,CAAC,YAAY,CACzC,IAAIC,WAAWD,aACd,IAAI;YACHD,SAAS,IAAI,CAAC,WAAW,CAACC;YAC1B;QACD,EAAE,OAAOE,OAAO;YACfC,QAAQ,KAAK,CAAC,CAAC,2BAA2B,EAAEH,WAAW,CAAC,CAAC,EAAEI,gBAAgBF;QAC5E;QAIF,OAAO,IAAI,CAAC,yBAAyB,CAACH,UAAU,CAAC;IAClD;IAkBO,0BAA0BA,MAAyB,EAAqB;QAC9E,MAAMM,SAAqC;YAC1C,GAAGN,MAAM;YACT,GAAIA,AAAqBO,WAArBP,OAAO,SAAS,GAAiB,CAAC,IAAI;gBAAE,WAAW;uBAAIA,OAAO,SAAS;iBAAC;YAAC,CAAC;YAC9E,GAAIA,AAAoBO,WAApBP,OAAO,QAAQ,GAAiB,CAAC,IAAI;gBAAE,UAAU;uBAAIA,OAAO,QAAQ;iBAAC;YAAC,CAAC;YAC3E,GAAIA,AAA0BO,WAA1BP,OAAO,cAAc,GACtB,CAAC,IACD;gBAAE,gBAAgB;oBAAE,GAAGA,OAAO,cAAc;gBAAC;YAAE,CAAC;YACnD,GAAIA,AAAuBO,WAAvBP,OAAO,WAAW,GACnB,CAAC,IACD;gBACA,aAAa;oBACZ,GAAGA,OAAO,WAAW;oBACrB,GAAIA,AAA+BO,WAA/BP,OAAO,WAAW,CAAC,OAAO,GAC3B,CAAC,IACD;wBAAE,SAAS;4BAAE,GAAGA,OAAO,WAAW,CAAC,OAAO;wBAAC;oBAAE,CAAC;gBAClD;YACD,CAAC;YACH,GAAIA,AAAoBO,WAApBP,OAAO,QAAQ,GAAiB,CAAC,IAAI;gBAAE,UAAU;oBAAE,GAAGA,OAAO,QAAQ;gBAAC;YAAE,CAAC;QAC9E;QAEA,IAAIQ,AAAiCD,WAAjCC,QAAQ,GAAG,CAAC,gBAAgB,EAC/BF,OAAO,cAAc,GAAG,IAAI,CAAC,uBAAuB,CACnD,oBACAE,QAAQ,GAAG,CAAC,gBAAgB;QAG9B,IAAIA,AAA6BD,WAA7BC,QAAQ,GAAG,CAAC,YAAY,EAC3BF,OAAO,WAAW,GAAG,IAAI,CAAC,uBAAuB,CAAC,gBAAgBE,QAAQ,GAAG,CAAC,YAAY;QAE3F,IAAIA,AAAgCD,WAAhCC,QAAQ,GAAG,CAAC,eAAe,EAC9BF,OAAO,aAAa,GAAG,IAAI,CAAC,uBAAuB,CAClD,mBACAE,QAAQ,GAAG,CAAC,eAAe;QAG7B,IACCA,QAAQ,GAAG,CAAC,SAAS,IACrB;YAAC;YAAS;YAAQ;YAAQ;SAAQ,CAAC,QAAQ,CAACA,QAAQ,GAAG,CAAC,SAAS,GAEjEF,OAAO,QAAQ,GAAGE,QAAQ,GAAG,CAAC,SAAS;QAExC,IAAIA,AAA2B,YAA3BA,QAAQ,GAAG,CAAC,UAAU,EACzBF,OAAO,SAAS,GAAG;QAEpB,IAAIE,AAA2BD,WAA3BC,QAAQ,GAAG,CAAC,UAAU,EACzBF,OAAO,SAAS,GAAGE,AAA2B,OAA3BA,QAAQ,GAAG,CAAC,UAAU,GAAU,EAAE,GAAGA,QAAQ,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC;QAEtF,IAAIA,AAA0BD,WAA1BC,QAAQ,GAAG,CAAC,SAAS,EACxBF,OAAO,QAAQ,GAAGE,AAA0B,OAA1BA,QAAQ,GAAG,CAAC,SAAS,GAAU,EAAE,GAAGA,QAAQ,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC;QAEnF,IAAIA,AAAoCD,WAApCC,QAAQ,GAAG,CAAC,mBAAmB,EAAgB;YAClD,MAAMC,UAAU,IAAI,CAAC,uBAAuB,CAC3C,uBACAD,QAAQ,GAAG,CAAC,mBAAmB;YAEhC,MAAME,MAAMD,AAAU,OAAVA;YACZ,IAAI,CAACE,OAAO,aAAa,CAACD,MACzB,MAAM,IAAIE,mBAAmB;YAE9BN,OAAO,cAAc,GAAG;gBAAE,GAAGA,OAAO,cAAc;gBAAEI;YAAI;QACzD;QACA,IAAIF,AAAyCD,WAAzCC,QAAQ,GAAG,CAAC,wBAAwB,EAAgB;YACvD,MAAMK,UAAU,IAAI,CAAC,uBAAuB,CAC3C,4BACAL,QAAQ,GAAG,CAAC,wBAAwB;YAErCF,OAAO,cAAc,GAAG;gBAAE,GAAGA,OAAO,cAAc;gBAAEO;YAAQ;QAC7D;QACA,IAAIL,AAAoDD,WAApDC,QAAQ,GAAG,CAAC,mCAAmC,EAClDF,OAAO,mBAAmB,GAAG,IAAI,CAAC,uBAAuB,CACxD,uCACAE,QAAQ,GAAG,CAAC,mCAAmC;QAGjD,IAAIA,AAAsDD,WAAtDC,QAAQ,GAAG,CAAC,qCAAqC,EACpDF,OAAO,qBAAqB,GAAG,IAAI,CAAC,uBAAuB,CAC1D,yCACAE,QAAQ,GAAG,CAAC,qCAAqC;QAGnD,IAAIA,AAAsCD,WAAtCC,QAAQ,GAAG,CAAC,qBAAqB,EACpCF,OAAO,mBAAmB,GAAG,IAAI,CAAC,uBAAuB,CACxD,yBACAE,QAAQ,GAAG,CAAC,qBAAqB;QAInC,IAAI,CAAC,yBAAyB,CAACF;QAE/B,OAAOA;IACR;IAUQ,0BAA0BA,MAAkC,EAAQ;QAC3E,MAAMQ,UAA4E;YACjF,iCAAiC;YACjC,mCAAmC;YACnC,mCAAmC;YACnC,uCAAuC;YACvC,yCAAyC;YACzC,yCAAyC;QAC1C;QACA,KAAK,MAAM,CAACC,QAAQC,IAAI,IAAIC,OAAO,OAAO,CAACH,SAAU;YACpD,MAAMI,MAAMV,QAAQ,GAAG,CAACO,OAAO;YAC/B,IAAIG,AAAQX,WAARW,KAAmB;YACvB,MAAMC,SAAS,IAAI,CAAC,YAAY,CAACD;YACjC,IAAIC,AAAWZ,WAAXY,QAAsB;gBACzBf,QAAQ,IAAI,CACX,CAAC,0BAA0B,EAAEW,OAAO,GAAG,EAAEG,IAAI,sCAAsC,CAAC;gBAErF;YACD;YACA,MAAME,WACLd,OAAO,QAAQ,IAAI,CAAC;YACrBc,QAAQ,CAACJ,IAAI,GAAGG;YAChBb,OAAO,QAAQ,GAAGc;QACnB;QAEA,MAAMC,cAAcb,QAAQ,GAAG,CAAC,wCAAwC;QACxE,IAAIa,AAAgBd,WAAhBc,aAA2B;YAC9B,IAAIA,AAAgB,iBAAhBA,eAAgCA,AAAgB,UAAhBA,aACnC,MAAM,IAAIT,mBACT,CAAC,6EAA6E,EAAES,aAAa;YAG/F,MAAMD,WACLd,OAAO,QAAQ,IAAI,CAAC;YACrBc,SAAS,iBAAiB,GAAGC;YAC7Bf,OAAO,QAAQ,GAAGc;QACnB;IACD;IAEQ,wBAAwBE,eAAuB,EAAEJ,GAAW,EAAU;QAC7E,IAAI,CAAC,iBAAiB,IAAI,CAACA,MAC1B,MAAM,IAAIN,mBAAmB,GAAGU,gBAAgB,yBAAyB,EAAEJ,KAAK;QAEjF,MAAMC,SAASR,OAAOO;QACtB,IAAI,CAACP,OAAO,aAAa,CAACQ,SACzB,MAAM,IAAIP,mBAAmB,GAAGU,gBAAgB,6BAA6B,EAAEJ,KAAK;QAErF,OAAOC;IACR;IAUQ,aAAaD,GAAW,EAAuB;QACtD,MAAMvB,IAAIuB,IAAI,IAAI,GAAG,WAAW;QAChC,IAAIvB,AAAM,WAANA,KAAgBA,AAAM,QAANA,GAAW,OAAO;QACtC,IAAIA,AAAM,YAANA,KAAiBA,AAAM,QAANA,GAAW,OAAO;IAExC;IAaQ,YAAY4B,QAAgB,EAAqB;QACxD,MAAMC,UAAUC,aAAaF,UAAU;QACvC,MAAMG,MAAMH,SAAS,KAAK,CAAC,KAAK,GAAG,IAAI;QAEvC,MAAML,MAAeQ,AAAQ,WAARA,OAAkBA,AAAQ,UAARA,MAAgBC,MAAUH,WAAWI,KAAK,KAAK,CAACJ;QAEvF,OAAO7B,0BAAAA,KAAO,CAACD,yBAAyBwB;IACzC;IAqBA,sBAAsBlB,MAAyB,EAAuB;QACrE,OAAO;YACN,gBAAgBA,OAAO,cAAc;YACrC,aAAaA,OAAO,WAAW;YAC/B,eAAeA,OAAO,aAAa;YACnC,WAAWA,OAAO,SAAS;YAC3B,UAAUA,OAAO,QAAQ;YACzB,gBAAgBA,OAAO,cAAc;YACrC,aAAaA,OAAO,WAAW;YAC/B,UAAUA,OAAO,QAAQ;YACzB,qBAAqBA,OAAO,mBAAmB;YAC/C,uBAAuBA,OAAO,qBAAqB;YACnD,qBAAqBA,OAAO,mBAAmB;QAChD;IACD;AACD"}
1
+ {"version":3,"file":"config/ConfigLoader.js","sources":["../../src/config/ConfigLoader.ts"],"sourcesContent":["/**\n * Configuration file loading with environment variable override support.\n *\n * This module provides the `ConfigLoader` class which handles loading configuration\n * from YAML and JSON files in standard locations, with automatic environment variable\n * overrides for all settings.\n *\n * @module config\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport * as v from 'valibot';\nimport type { PersistenceConfig } from '../contracts/PersistenceBackend.js';\nimport { ConfigurationError, getErrorMessage } from '../errors.js';\nimport type { FeatureFlags } from '../contracts/features.js';\nimport type { ServerConfigOptions } from '../ServerConfig.js';\n\n/**\n * Lenient runtime schema for config files. Only validates top-level shape;\n * unknown extra fields are preserved by Valibot's default object behavior\n * being strict is avoided by using `v.looseObject`. All fields are optional.\n */\nconst ConfigFileOptionsSchema = v.looseObject({\n\tmaxHistorySize: v.optional(v.number()),\n\tmaxBranches: v.optional(v.number()),\n\tmaxBranchSize: v.optional(v.number()),\n\tlogLevel: v.optional(v.picklist(['debug', 'info', 'warn', 'error'])),\n\tprettyLog: v.optional(v.boolean()),\n\tskillDirs: v.optional(v.array(v.string())),\n\ttoolDirs: v.optional(v.array(v.string())),\n\tdiscoveryCache: v.optional(\n\t\tv.looseObject({\n\t\t\tttl: v.optional(v.number()),\n\t\t\tmaxSize: v.optional(v.number()),\n\t\t})\n\t),\n\tpersistence: v.optional(v.looseObject({})),\n\tpersistenceBufferSize: v.optional(v.number()),\n\tpersistenceFlushInterval: v.optional(v.number()),\n\tpersistenceMaxRetries: v.optional(v.number()),\n\tfeatures: v.optional(v.looseObject({})),\n\ttoolInterleaveTtlMs: v.optional(v.number()),\n\ttoolInterleaveSweepMs: v.optional(v.number()),\n\tmaxSessionsPerOwner: v.optional(v.number()),\n});\n\ntype Mutable<T> = { -readonly [K in keyof T]: T[K] };\n\n/**\n * Configuration options loaded from config files.\n *\n * These options represent the structure of configuration files (JSON or YAML)\n * that can be loaded from standard locations. All values can be overridden\n * by environment variables.\n *\n * @example\n * ```yaml\n * # .claude/config.yaml\n * maxHistorySize: 500\n * maxBranches: 25\n * logLevel: debug\n * prettyLog: true\n * skillDirs:\n * - ./custom-skills\n * toolDirs:\n * - ./custom-tools\n * discoveryCache:\n * ttl: 600000\n * maxSize: 200\n * persistence:\n * enabled: true\n * backend: sqlite\n * options:\n * dbPath: ./data/history.db\n * ```\n */\nexport interface ConfigFileOptions {\n\t/**\n\t * Maximum number of thoughts to keep in history.\n\t * Can be overridden by `MAX_HISTORY_SIZE` environment variable.\n\t */\n\treadonly maxHistorySize?: number;\n\n\t/**\n\t * Maximum number of branches to maintain.\n\t * Can be overridden by `MAX_BRANCHES` environment variable.\n\t */\n\treadonly maxBranches?: number;\n\n\t/**\n\t * Maximum size of each branch.\n\t * Can be overridden by `MAX_BRANCH_SIZE` environment variable.\n\t */\n\treadonly maxBranchSize?: number;\n\n\t/**\n\t * Logging level for the application.\n\t * Can be overridden by `LOG_LEVEL` environment variable.\n\t */\n\treadonly logLevel?: 'debug' | 'info' | 'warn' | 'error';\n\n\t/**\n\t * Whether to enable pretty (formatted) logging output.\n\t * Can be overridden by `PRETTY_LOG` environment variable (set to \"false\" to disable).\n\t */\n\treadonly prettyLog?: boolean;\n\n\t/**\n\t * Directory paths to search for skills.\n\t * Can be overridden by `SKILL_DIRS` environment variable (colon-separated).\n\t */\n\treadonly skillDirs?: string[];\n\n\t/**\n\t * Directory paths to search for tools.\n\t * Can be overridden by `TOOL_DIRS` environment variable (colon-separated).\n\t */\n\treadonly toolDirs?: string[];\n\n\t/**\n\t * Discovery cache configuration.\n\t * Can be overridden by `DISCOVERY_CACHE_TTL` and `DISCOVERY_CACHE_MAX_SIZE` environment variables.\n\t */\n\treadonly discoveryCache?: {\n\t\t/**\n\t\t * Time-to-live for cache entries in milliseconds.\n\t\t * Environment variable `DISCOVERY_CACHE_TTL` accepts seconds.\n\t\t */\n\t\treadonly ttl?: number;\n\t\t/**\n\t\t * Maximum number of entries in the cache.\n\t\t */\n\t\treadonly maxSize?: number;\n\t};\n\n\t/**\n\t * Persistence configuration for storing history and state.\n\t */\n\treadonly persistence?: PersistenceConfig;\n\n\treadonly persistenceBufferSize?: number;\n\n\treadonly persistenceFlushInterval?: number;\n\n\treadonly persistenceMaxRetries?: number;\n\n\t/**\n\t * Feature flag overrides. Each field can be set independently.\n\t * Can be overridden by `TRACELATTICE_FEATURES_*` environment variables.\n\t */\n\treadonly features?: Partial<FeatureFlags>;\n\n\t/**\n\t * TTL in milliseconds for suspended tool-interleave entries.\n\t * Can be overridden by `TRACELATTICE_TOOL_INTERLEAVE_TTL_MS` environment variable.\n\t */\n\treadonly toolInterleaveTtlMs?: number;\n\n\t/**\n\t * Sweep interval in milliseconds for SuspensionStore expiration cleanup.\n\t * Can be overridden by `TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS` environment variable.\n\t */\n\treadonly toolInterleaveSweepMs?: number;\n\n\t/**\n\t * Maximum sessions per owner. Per-owner LRU bucket prevents one user from\n\t * consuming all session slots.\n\t * Can be overridden by `SESSION_MAX_PER_OWNER` environment variable.\n\t */\n\treadonly maxSessionsPerOwner?: number;\n}\n\n/**\n * Loads configuration from files with environment variable overrides.\n *\n * This class searches for configuration files in standard locations and applies\n * environment variable overrides. Files are searched in priority order, with the\n * first match being used. Environment variables always take precedence over file values.\n *\n * @remarks\n * **Config File Search Order (priority):**\n * 1. Custom path (if provided to constructor)\n * 2. `.claude/config.json` (project-local)\n * 3. `.claude/config.yaml` (project-local)\n * 4. `.claude/config.yml` (project-local)\n * 5. `~/.claude/config.json` (user-global)\n * 6. `~/.claude/config.yaml` (user-global)\n * 7. `~/.claude/config.yml` (user-global)\n *\n * **Environment Variable Overrides:**\n * | Variable | Type | Description |\n * |----------|------|-------------|\n * | `MAX_HISTORY_SIZE` | number | Max thoughts in history |\n * | `MAX_BRANCHES` | number | Max number of branches |\n * | `MAX_BRANCH_SIZE` | number | Max size of each branch |\n * | `LOG_LEVEL` | string | Logging level (debug/info/warn/error) |\n * | `PRETTY_LOG` | string | \"false\" to disable pretty logging |\n * | `SKILL_DIRS` | string | Colon-separated directory paths |\n * | `TOOL_DIRS` | string | Colon-separated directory paths |\n * | `DISCOVERY_CACHE_TTL` | number | TTL in seconds (converted to ms) |\n * | `DISCOVERY_CACHE_MAX_SIZE` | number | Max cache entries |\n *\n * @example\n * ```typescript\n * // Use default search paths\n * const loader1 = new ConfigLoader();\n * const config1 = loader1.load();\n *\n * // Use custom config path\n * const loader2 = new ConfigLoader('./my-config.yaml');\n * const config2 = loader2.load();\n *\n * // Convert to ServerConfig options\n * const serverOptions = loader2.toServerConfigOptions(config2);\n * ```\n */\nexport class ConfigLoader {\n\t/** Array of config file paths to search, in priority order. */\n\tprivate _configPaths: string[];\n\n\t/**\n\t * Creates a new ConfigLoader instance.\n\t *\n\t * @param customPath - Optional custom config file path. If provided, only this path will be checked.\n\t *\n\t * @example\n\t * ```typescript\n\t * // Use default search paths\n\t * const loader1 = new ConfigLoader();\n\t *\n\t * // Use a specific config file\n\t * const loader2 = new ConfigLoader('./custom-config.json');\n\t * ```\n\t */\n\tconstructor(customPath?: string) {\n\t\tthis._configPaths = customPath\n\t\t\t? [customPath]\n\t\t\t: [\n\t\t\t\t\t'.claude/config.json',\n\t\t\t\t\t'.claude/config.yaml',\n\t\t\t\t\t'.claude/config.yml',\n\t\t\t\t\tjoin(homedir(), '.claude/config.json'),\n\t\t\t\t\tjoin(homedir(), '.claude/config.yaml'),\n\t\t\t\t\tjoin(homedir(), '.claude/config.yml'),\n\t\t\t\t];\n\t}\n\n\t/**\n\t * Loads configuration from files and applies environment overrides.\n\t *\n\t * Searches for config files in the configured paths (in priority order),\n\t * parses the first match, and applies environment variable overrides.\n\t * Returns null if no config file is found and no environment overrides are set.\n\t *\n\t * @returns The loaded configuration with environment overrides applied, or null if no config found\n\t *\n\t * @example\n\t * ```typescript\n\t * const loader = new ConfigLoader();\n\t * const config = loader.load();\n\t *\n\t * if (config) {\n\t * console.log('Max history size:', config.maxHistorySize);\n\t * console.log('Log level:', config.logLevel);\n\t * }\n\t * ```\n\t */\n\tload(): ConfigFileOptions | null {\n\t\tlet config: ConfigFileOptions | null = null;\n\n\t\tfor (const configPath of this._configPaths) {\n\t\t\tif (existsSync(configPath)) {\n\t\t\t\ttry {\n\t\t\t\t\tconfig = this.parseConfig(configPath);\n\t\t\t\t\tbreak;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(`Failed to load config from ${configPath}:`, getErrorMessage(error));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this.applyEnvironmentOverrides(config || {});\n\t}\n\n\t/**\n\t * Applies environment variable overrides to the configuration.\n\t *\n\t * Environment variables take precedence over file-based configuration.\n\t * Supported environment variables:\n\t * - `MAX_HISTORY_SIZE`, `MAX_BRANCHES`, `MAX_BRANCH_SIZE` (numbers)\n\t * - `LOG_LEVEL` (debug/info/warn/error)\n\t * - `PRETTY_LOG` (\"false\" to disable)\n\t * - `SKILL_DIRS` (colon-separated paths)\n\t * - `TOOL_DIRS` (colon-separated paths)\n\t * - `DISCOVERY_CACHE_TTL` (in seconds, converted to ms)\n\t * - `DISCOVERY_CACHE_MAX_SIZE` (number)\n\t *\n\t * @param config - The configuration to apply overrides to\n\t * @returns A new configuration object with environment overrides applied\n\t */\n\tpublic applyEnvironmentOverrides(config: ConfigFileOptions): ConfigFileOptions {\n\t\tconst result: Mutable<ConfigFileOptions> = {\n\t\t\t...config,\n\t\t\t...(config.skillDirs === undefined ? {} : { skillDirs: [...config.skillDirs] }),\n\t\t\t...(config.toolDirs === undefined ? {} : { toolDirs: [...config.toolDirs] }),\n\t\t\t...(config.discoveryCache === undefined\n\t\t\t\t? {}\n\t\t\t\t: { discoveryCache: { ...config.discoveryCache } }),\n\t\t\t...(config.persistence === undefined\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\tpersistence: {\n\t\t\t\t\t\t\t...config.persistence,\n\t\t\t\t\t\t\t...(config.persistence.options === undefined\n\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t: { options: { ...config.persistence.options } }),\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t...(config.features === undefined ? {} : { features: { ...config.features } }),\n\t\t};\n\n\t\tif (process.env.MAX_HISTORY_SIZE !== undefined) {\n\t\t\tresult.maxHistorySize = this.parseEnvironmentInteger(\n\t\t\t\t'MAX_HISTORY_SIZE',\n\t\t\t\tprocess.env.MAX_HISTORY_SIZE\n\t\t\t);\n\t\t}\n\t\tif (process.env.MAX_BRANCHES !== undefined) {\n\t\t\tresult.maxBranches = this.parseEnvironmentInteger('MAX_BRANCHES', process.env.MAX_BRANCHES);\n\t\t}\n\t\tif (process.env.MAX_BRANCH_SIZE !== undefined) {\n\t\t\tresult.maxBranchSize = this.parseEnvironmentInteger(\n\t\t\t\t'MAX_BRANCH_SIZE',\n\t\t\t\tprocess.env.MAX_BRANCH_SIZE\n\t\t\t);\n\t\t}\n\t\tif (\n\t\t\tprocess.env.LOG_LEVEL &&\n\t\t\t['debug', 'info', 'warn', 'error'].includes(process.env.LOG_LEVEL)\n\t\t) {\n\t\t\tresult.logLevel = process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error';\n\t\t}\n\t\tif (process.env.PRETTY_LOG === 'false') {\n\t\t\tresult.prettyLog = false;\n\t\t}\n\t\tif (process.env.SKILL_DIRS !== undefined) {\n\t\t\tresult.skillDirs = process.env.SKILL_DIRS === '' ? [] : process.env.SKILL_DIRS.split(':');\n\t\t}\n\t\tif (process.env.TOOL_DIRS !== undefined) {\n\t\t\tresult.toolDirs = process.env.TOOL_DIRS === '' ? [] : process.env.TOOL_DIRS.split(':');\n\t\t}\n\t\tif (process.env.DISCOVERY_CACHE_TTL !== undefined) {\n\t\t\tconst seconds = this.parseEnvironmentInteger(\n\t\t\t\t'DISCOVERY_CACHE_TTL',\n\t\t\t\tprocess.env.DISCOVERY_CACHE_TTL\n\t\t\t);\n\t\t\tconst ttl = seconds * 1000;\n\t\t\tif (!Number.isSafeInteger(ttl)) {\n\t\t\t\tthrow new ConfigurationError('DISCOVERY_CACHE_TTL exceeds the safe integer range');\n\t\t\t}\n\t\t\tresult.discoveryCache = { ...result.discoveryCache, ttl };\n\t\t}\n\t\tif (process.env.DISCOVERY_CACHE_MAX_SIZE !== undefined) {\n\t\t\tconst maxSize = this.parseEnvironmentInteger(\n\t\t\t\t'DISCOVERY_CACHE_MAX_SIZE',\n\t\t\t\tprocess.env.DISCOVERY_CACHE_MAX_SIZE\n\t\t\t);\n\t\t\tresult.discoveryCache = { ...result.discoveryCache, maxSize };\n\t\t}\n\t\tif (process.env.TRACELATTICE_TOOL_INTERLEAVE_TTL_MS !== undefined) {\n\t\t\tresult.toolInterleaveTtlMs = this.parseEnvironmentInteger(\n\t\t\t\t'TRACELATTICE_TOOL_INTERLEAVE_TTL_MS',\n\t\t\t\tprocess.env.TRACELATTICE_TOOL_INTERLEAVE_TTL_MS\n\t\t\t);\n\t\t}\n\t\tif (process.env.TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS !== undefined) {\n\t\t\tresult.toolInterleaveSweepMs = this.parseEnvironmentInteger(\n\t\t\t\t'TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS',\n\t\t\t\tprocess.env.TRACELATTICE_TOOL_INTERLEAVE_SWEEP_MS\n\t\t\t);\n\t\t}\n\t\tif (process.env.SESSION_MAX_PER_OWNER !== undefined) {\n\t\t\tresult.maxSessionsPerOwner = this.parseEnvironmentInteger(\n\t\t\t\t'SESSION_MAX_PER_OWNER',\n\t\t\t\tprocess.env.SESSION_MAX_PER_OWNER\n\t\t\t);\n\t\t}\n\n\t\tthis.applyFeatureFlagOverrides(result);\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Applies TRACELATTICE_FEATURES_* environment variable overrides for feature flags.\n\t * Booleans accept 'true'/'false'/'1'/'0' (case-insensitive).\n\t * Invalid reasoningStrategy values are rejected.\n\t *\n\t * @param result - Configuration object to mutate with feature flag overrides\n\t * @private\n\t */\n\tprivate applyFeatureFlagOverrides(result: Mutable<ConfigFileOptions>): void {\n\t\tconst boolMap: Record<string, Exclude<keyof FeatureFlags, 'reasoningStrategy'>> = {\n\t\t\tTRACELATTICE_FEATURES_DAG_EDGES: 'dagEdges',\n\t\t\tTRACELATTICE_FEATURES_CALIBRATION: 'calibration',\n\t\t\tTRACELATTICE_FEATURES_COMPRESSION: 'compression',\n\t\t\tTRACELATTICE_FEATURES_TOOL_INTERLEAVE: 'toolInterleave',\n\t\t\tTRACELATTICE_FEATURES_NEW_THOUGHT_TYPES: 'newThoughtTypes',\n\t\t\tTRACELATTICE_FEATURES_OUTCOME_RECORDING: 'outcomeRecording',\n\t\t};\n\t\tfor (const [envVar, key] of Object.entries(boolMap)) {\n\t\t\tconst raw = process.env[envVar];\n\t\t\tif (raw === undefined) continue;\n\t\t\tconst parsed = this.parseBoolean(raw);\n\t\t\tif (parsed === undefined) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`Invalid boolean value for ${envVar}: \"${raw}\" (expected true/false/1/0). Ignoring.`\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst features: { -readonly [K in keyof FeatureFlags]?: FeatureFlags[K] } =\n\t\t\t\tresult.features ?? {};\n\t\t\tfeatures[key] = parsed;\n\t\t\tresult.features = features;\n\t\t}\n\n\t\tconst strategyRaw = process.env.TRACELATTICE_FEATURES_REASONING_STRATEGY;\n\t\tif (strategyRaw !== undefined) {\n\t\t\tif (strategyRaw !== 'sequential' && strategyRaw !== 'tot') {\n\t\t\t\tthrow new ConfigurationError(\n\t\t\t\t\t`TRACELATTICE_FEATURES_REASONING_STRATEGY must be one of sequential, tot, got ${strategyRaw}`\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst features: { -readonly [K in keyof FeatureFlags]?: FeatureFlags[K] } =\n\t\t\t\tresult.features ?? {};\n\t\t\tfeatures.reasoningStrategy = strategyRaw;\n\t\t\tresult.features = features;\n\t\t}\n\t}\n\n\tprivate parseEnvironmentInteger(environmentName: string, raw: string): number {\n\t\tif (!/^(0|[1-9]\\d*)$/.test(raw)) {\n\t\t\tthrow new ConfigurationError(`${environmentName} must be an integer, got ${raw}`);\n\t\t}\n\t\tconst parsed = Number(raw);\n\t\tif (!Number.isSafeInteger(parsed)) {\n\t\t\tthrow new ConfigurationError(`${environmentName} must be a safe integer, got ${raw}`);\n\t\t}\n\t\treturn parsed;\n\t}\n\n\t/**\n\t * Parses a boolean from an environment variable string.\n\t * Accepts 'true'/'false'/'1'/'0' case-insensitively.\n\t *\n\t * @param raw - Raw environment variable string\n\t * @returns Parsed boolean, or undefined if the value is invalid\n\t * @private\n\t */\n\tprivate parseBoolean(raw: string): boolean | undefined {\n\t\tconst v = raw.trim().toLowerCase();\n\t\tif (v === 'true' || v === '1') return true;\n\t\tif (v === 'false' || v === '0') return false;\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Parses a configuration file (JSON or YAML).\n\t *\n\t * Detects the file type by extension and uses the appropriate parser.\n\t * Supports `.json`, `.yaml`, and `.yml` file extensions.\n\t *\n\t * @param filePath - Path to the configuration file to parse\n\t * @returns The parsed configuration object\n\t * @throws {Error} If the file cannot be parsed\n\t * @private\n\t */\n\tprivate parseConfig(filePath: string): ConfigFileOptions {\n\t\tconst content = readFileSync(filePath, 'utf-8');\n\t\tconst ext = filePath.split('.').pop()?.toLowerCase();\n\n\t\tconst raw: unknown = ext === 'yaml' || ext === 'yml' ? parseYaml(content) : JSON.parse(content);\n\n\t\treturn v.parse(ConfigFileOptionsSchema, raw) as ConfigFileOptions;\n\t}\n\n\t/**\n\t * Converts file-based configuration to ServerConfig options.\n\t *\n\t * This is a convenience method for extracting the ServerConfig-relevant\n\t * options from a file-based configuration.\n\t *\n\t * @param config - The configuration to convert\n\t * @returns An object with ServerConfig-compatible options\n\t *\n\t * @example\n\t * ```typescript\n\t * const loader = new ConfigLoader();\n\t * const config = loader.load();\n\t * if (config) {\n\t * const serverOpts = loader.toServerConfigOptions(config);\n\t * const serverConfig = new ServerConfig(serverOpts);\n\t * }\n\t * ```\n\t */\n\ttoServerConfigOptions(config: ConfigFileOptions): ServerConfigOptions {\n\t\treturn {\n\t\t\tmaxHistorySize: config.maxHistorySize,\n\t\t\tmaxBranches: config.maxBranches,\n\t\t\tmaxBranchSize: config.maxBranchSize,\n\t\t\tskillDirs: config.skillDirs,\n\t\t\ttoolDirs: config.toolDirs,\n\t\t\tdiscoveryCache: config.discoveryCache,\n\t\t\tpersistence: config.persistence,\n\t\t\tpersistenceBufferSize: config.persistenceBufferSize,\n\t\t\tpersistenceFlushInterval: config.persistenceFlushInterval,\n\t\t\tpersistenceMaxRetries: config.persistenceMaxRetries,\n\t\t\tfeatures: config.features,\n\t\t\ttoolInterleaveTtlMs: config.toolInterleaveTtlMs,\n\t\t\ttoolInterleaveSweepMs: config.toolInterleaveSweepMs,\n\t\t\tmaxSessionsPerOwner: config.maxSessionsPerOwner,\n\t\t};\n\t}\n}\n"],"names":["ConfigFileOptionsSchema","v","ConfigLoader","customPath","join","homedir","config","configPath","existsSync","error","console","getErrorMessage","result","undefined","process","seconds","ttl","Number","ConfigurationError","maxSize","boolMap","envVar","key","Object","raw","parsed","features","strategyRaw","environmentName","filePath","content","readFileSync","ext","parseYaml","JSON"],"mappings":"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"}
@@ -51,10 +51,11 @@ export declare class CompressionService {
51
51
  /** Collect the root and all of its descendants (chronological BFS). */
52
52
  private _collectCovered;
53
53
  /**
54
- * Resolve covered ids to {@link ThoughtData}, dropping any not found in
55
- * the history (defensive — graph may reference evicted thoughts).
54
+ * Resolve candidate ids against one stable session snapshot. Main-history
55
+ * records take precedence over branch copies; branch-only records are added
56
+ * in declared branch order. Graph ids absent from the snapshot are omitted.
56
57
  */
57
- private _lookupThoughts;
58
+ private _resolveThoughts;
58
59
  /**
59
60
  * Mean of (`calibrated_confidence` ?? `confidence` ?? 0) across covered.
60
61
  * Returns 0 when no thoughts are covered.
@@ -1 +1 @@
1
- {"version":3,"file":"CompressionService.d.ts","sourceRoot":"","sources":["../../../src/core/compression/CompressionService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAK7D,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAiB7E,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;gBAEvB,IAAI,EAAE,eAAe;IAKjC;;;;;;OAMG;IACH,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,GAAG,OAAO;IA8B3F,uEAAuE;IACvE,OAAO,CAAC,oBAAoB;IAW5B,uEAAuE;IACvE,OAAO,CAAC,eAAe;IAKvB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAcvB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAWvB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAYrB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAsBtB,8EAA8E;IAC9E,OAAO,CAAC,eAAe;CAMvB"}
1
+ {"version":3,"file":"CompressionService.d.ts","sourceRoot":"","sources":["../../../src/core/compression/CompressionService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAC/D,OAAO,KAAK,EAA0B,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAKrF,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAiB7E,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,cAAc,EAAE,eAAe,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;gBAEvB,IAAI,EAAE,eAAe;IAKjC;;;;;;OAMG;IACH,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,GAAG,OAAO;IAkC3F,uEAAuE;IACvE,OAAO,CAAC,oBAAoB;IAW5B,uEAAuE;IACvE,OAAO,CAAC,eAAe;IAKvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IA0BxB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAWvB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAYrB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAsBtB,8EAA8E;IAC9E,OAAO,CAAC,eAAe;CAMvB"}
@@ -56,8 +56,12 @@ class CompressionService {
56
56
  compressBranch(sessionId, branchId, rootThoughtId) {
57
57
  const existing = this._findExistingForRoot(sessionId, branchId, rootThoughtId);
58
58
  if (existing) return existing;
59
- const coveredIds = this._collectCovered(sessionId, rootThoughtId);
60
- const thoughts = this._lookupThoughts(sessionId, coveredIds);
59
+ const snapshot = this._deps.historyManager.inspectSession(sessionId);
60
+ const candidateIds = this._collectCovered(sessionId, rootThoughtId);
61
+ const thoughts = this._resolveThoughts(snapshot, candidateIds);
62
+ const coveredIds = thoughts.flatMap((thought)=>void 0 === thought.id ? [] : [
63
+ thought.id
64
+ ]);
61
65
  const summary = {
62
66
  id: generateUlid(),
63
67
  sessionId,
@@ -90,16 +94,21 @@ class CompressionService {
90
94
  ...descendants
91
95
  ];
92
96
  }
93
- _lookupThoughts(sessionId, coveredIds) {
94
- const history = this._deps.historyManager.getHistory(sessionId);
97
+ _resolveThoughts(snapshot, candidateIds) {
95
98
  const byId = new Map();
96
- for (const t of history)if (void 0 !== t.id) byId.set(t.id, t);
97
- const out = [];
98
- for (const id of coveredIds){
99
- const t = byId.get(id);
100
- if (t) out.push(t);
99
+ for (const thought of snapshot.history)if (void 0 !== thought.id && !byId.has(thought.id)) byId.set(thought.id, thought);
100
+ for (const branchId of snapshot.branchIds)for (const thought of snapshot.branches[branchId] ?? [])if (void 0 !== thought.id && !byId.has(thought.id)) byId.set(thought.id, thought);
101
+ const resolved = [];
102
+ const seen = new Set();
103
+ for (const id of candidateIds){
104
+ if (seen.has(id)) continue;
105
+ const thought = byId.get(id);
106
+ if (void 0 !== thought) {
107
+ seen.add(id);
108
+ resolved.push(thought);
109
+ }
101
110
  }
102
- return out;
111
+ return resolved;
103
112
  }
104
113
  _meanConfidence(thoughts) {
105
114
  if (0 === thoughts.length) return 0;
@@ -1 +1 @@
1
- {"version":3,"file":"core/compression/CompressionService.js","sources":["../../../src/core/compression/CompressionService.ts"],"sourcesContent":["/**\n * CompressionService — deterministic branch summarization.\n *\n * Compresses a terminated branch (or any subtree rooted at a thought) into a\n * single {@link Summary} record. Pure offline computation: no LLM, no I/O.\n *\n * Topic extraction is unigram frequency with stopword filtering; aggregate\n * confidence is the arithmetic mean of per-thought calibrated (or raw)\n * confidence values. The covered set is identified via\n * {@link GraphView.descendants}. Compression is **additive**: original\n * thoughts are never mutated or deleted.\n *\n * @module core/compression/CompressionService\n */\n\nimport type { ISummaryStore, Summary } from '../../contracts/summary.js';\nimport type { IEdgeStore } from '../../contracts/interfaces.js';\nimport type { Logger } from '../../logger/StructuredLogger.js';\nimport type { IHistoryManager } from '../IHistoryManager.js';\nimport type { ThoughtData } from '../thought.js';\nimport type { ConfidenceSignals } from '../reasoning.js';\nimport { GraphView } from '../graph/GraphView.js';\nimport { generateUlid } from '../ids.js';\nimport type { BranchId, SessionId, ThoughtId } from '../../contracts/ids.js';\n\n/** Stopwords excluded from topic extraction (lowercase). */\nconst STOPWORDS: ReadonlySet<string> = new Set([\n\t'the', 'this', 'that', 'then', 'than', 'with', 'from', 'have', 'will', 'been',\n\t'they', 'them', 'their', 'there', 'about', 'would', 'could', 'should', 'which',\n\t'where', 'when', 'what', 'some', 'into', 'also', 'just', 'like', 'over', 'such',\n\t'after', 'only', 'most', 'very', 'much', 'well', 'even', 'still', 'since',\n\t'being', 'doing', 'going', 'using',\n]);\n\nconst MIN_TOKEN_LENGTH = 4;\nconst TOP_TOPICS = 3;\n\n/** Local view of {@link ThoughtData} that exposes optional confidence signals. */\ntype ThoughtWithSignals = ThoughtData & { readonly confidence_signals?: ConfidenceSignals };\n\n/** Dependencies required by {@link CompressionService}. */\nexport interface CompressionDeps {\n\treadonly historyManager: IHistoryManager;\n\treadonly edgeStore: IEdgeStore;\n\treadonly summaryStore: ISummaryStore;\n\treadonly onSummaryCreated?: (summary: Summary) => void;\n\treadonly logger?: Logger;\n}\n\n/**\n * Deterministic, offline compression of branch subtrees into {@link Summary}.\n *\n * @example\n * ```typescript\n * const svc = new CompressionService({ historyManager, edgeStore, summaryStore });\n * const summary = svc.compressBranch('s1', 'alt-1', 'thought-root');\n * ```\n */\nexport class CompressionService {\n\tprivate readonly _deps: CompressionDeps;\n\tprivate readonly _graph: GraphView;\n\n\tconstructor(deps: CompressionDeps) {\n\t\tthis._deps = deps;\n\t\tthis._graph = new GraphView(deps.edgeStore);\n\t}\n\n\t/**\n\t * Compress the subtree rooted at `rootThoughtId` into a {@link Summary}.\n\t *\n\t * Idempotency: if a summary for the same `(branchId, rootThoughtId)` already\n\t * exists in the store, the existing summary is returned unchanged — same\n\t * inputs always yield the same Summary identity.\n\t */\n\tcompressBranch(sessionId: SessionId, branchId: BranchId, rootThoughtId: ThoughtId): Summary {\n\t\tconst existing = this._findExistingForRoot(sessionId, branchId, rootThoughtId);\n\t\tif (existing) return existing;\n\n\t\tconst coveredIds = this._collectCovered(sessionId, rootThoughtId);\n\t\tconst thoughts = this._lookupThoughts(sessionId, coveredIds);\n\t\tconst summary: Summary = {\n\t\t\tid: generateUlid(),\n\t\t\tsessionId,\n\t\t\tbranchId,\n\t\t\trootThoughtId,\n\t\t\tcoveredIds,\n\t\t\tcoveredRange: this._coveredRange(thoughts),\n\t\t\ttopics: this._extractTopics(thoughts),\n\t\t\taggregateConfidence: this._meanConfidence(thoughts),\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tthis._deps.summaryStore.add(summary);\n\t\tthis._deps.onSummaryCreated?.(summary);\n\t\tthis._deps.logger?.debug('compression.branch.compressed', {\n\t\t\tsessionId,\n\t\t\tbranchId,\n\t\t\trootThoughtId,\n\t\t\tcovered: coveredIds.length,\n\t\t\ttopics: summary.topics,\n\t\t});\n\t\treturn summary;\n\t}\n\n\t/** Look up an existing summary on this branch with a matching root. */\n\tprivate _findExistingForRoot(\n\t\tsessionId: SessionId,\n\t\tbranchId: BranchId,\n\t\trootThoughtId: ThoughtId\n\t): Summary | undefined {\n\t\tfor (const s of this._deps.summaryStore.forBranch(sessionId, branchId)) {\n\t\t\tif (s.rootThoughtId === rootThoughtId) return s;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/** Collect the root and all of its descendants (chronological BFS). */\n\tprivate _collectCovered(sessionId: SessionId, rootThoughtId: ThoughtId): readonly ThoughtId[] {\n\t\tconst descendants = this._graph.descendants(sessionId, rootThoughtId);\n\t\treturn [rootThoughtId, ...descendants];\n\t}\n\n\t/**\n\t * Resolve covered ids to {@link ThoughtData}, dropping any not found in\n\t * the history (defensive — graph may reference evicted thoughts).\n\t */\n\tprivate _lookupThoughts(sessionId: SessionId, coveredIds: readonly ThoughtId[]): ThoughtData[] {\n\t\tconst history = this._deps.historyManager.getHistory(sessionId);\n\t\tconst byId = new Map<string, ThoughtData>();\n\t\tfor (const t of history) {\n\t\t\tif (t.id !== undefined) byId.set(t.id, t);\n\t\t}\n\t\tconst out: ThoughtData[] = [];\n\t\tfor (const id of coveredIds) {\n\t\t\tconst t = byId.get(id);\n\t\t\tif (t) out.push(t);\n\t\t}\n\t\treturn out;\n\t}\n\n\t/**\n\t * Mean of (`calibrated_confidence` ?? `confidence` ?? 0) across covered.\n\t * Returns 0 when no thoughts are covered.\n\t */\n\tprivate _meanConfidence(thoughts: readonly ThoughtData[]): number {\n\t\tif (thoughts.length === 0) return 0;\n\t\tlet sum = 0;\n\t\tfor (const t of thoughts) {\n\t\t\tconst withSignals = t as ThoughtWithSignals;\n\t\t\tconst calibrated = withSignals.confidence_signals?.calibrated_confidence;\n\t\t\tsum += calibrated ?? t.confidence ?? 0;\n\t\t}\n\t\treturn sum / thoughts.length;\n\t}\n\n\t/**\n\t * Inclusive `[min, max]` of `thought_number` across covered thoughts.\n\t * Returns `[0, 0]` when no thoughts are covered.\n\t */\n\tprivate _coveredRange(thoughts: readonly ThoughtData[]): readonly [number, number] {\n\t\tif (thoughts.length === 0) return [0, 0];\n\t\tlet min = Number.POSITIVE_INFINITY;\n\t\tlet max = Number.NEGATIVE_INFINITY;\n\t\tfor (const t of thoughts) {\n\t\t\tconst n = t.thought_number;\n\t\t\tif (n < min) min = n;\n\t\t\tif (n > max) max = n;\n\t\t}\n\t\treturn [min, max];\n\t}\n\n\t/**\n\t * Extract top-3 unigrams by frequency from concatenated thought text.\n\t * Ties broken by first-occurrence order (stable).\n\t */\n\tprivate _extractTopics(thoughts: readonly ThoughtData[]): readonly string[] {\n\t\tif (thoughts.length === 0) return [];\n\t\tconst counts = new Map<string, number>();\n\t\tconst order = new Map<string, number>();\n\t\tlet position = 0;\n\t\tfor (const t of thoughts) {\n\t\t\tfor (const raw of (t.thought ?? '').split(/\\s+/)) {\n\t\t\t\tconst token = this._normalizeToken(raw);\n\t\t\t\tif (token === null) continue;\n\t\t\t\tif (!counts.has(token)) order.set(token, position++);\n\t\t\t\tcounts.set(token, (counts.get(token) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t\tif (counts.size === 0) return [];\n\t\tconst entries = Array.from(counts.entries());\n\t\tentries.sort((a, b) => {\n\t\t\tif (b[1] !== a[1]) return b[1] - a[1];\n\t\t\treturn (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0);\n\t\t});\n\t\treturn entries.slice(0, TOP_TOPICS).map(([word]) => word);\n\t}\n\n\t/** Lowercase + strip non-alphanumerics; reject if too short or a stopword. */\n\tprivate _normalizeToken(raw: string): string | null {\n\t\tconst cleaned = raw.toLowerCase().replace(/[^a-z0-9]+/g, '');\n\t\tif (cleaned.length < MIN_TOKEN_LENGTH) return null;\n\t\tif (STOPWORDS.has(cleaned)) return null;\n\t\treturn cleaned;\n\t}\n}\n"],"names":["STOPWORDS","Set","MIN_TOKEN_LENGTH","TOP_TOPICS","CompressionService","deps","GraphView","sessionId","branchId","rootThoughtId","existing","coveredIds","thoughts","summary","generateUlid","Date","s","descendants","history","byId","Map","t","undefined","out","id","sum","withSignals","calibrated","min","Number","max","n","counts","order","position","raw","token","entries","Array","a","b","word","cleaned"],"mappings":"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"}
1
+ {"version":3,"file":"core/compression/CompressionService.js","sources":["../../../src/core/compression/CompressionService.ts"],"sourcesContent":["/**\n * CompressionService — deterministic branch summarization.\n *\n * Compresses a terminated branch (or any subtree rooted at a thought) into a\n * single {@link Summary} record. Pure offline computation: no LLM, no I/O.\n *\n * Topic extraction is unigram frequency with stopword filtering; aggregate\n * confidence is the arithmetic mean of per-thought calibrated (or raw)\n * confidence values. The covered set is identified via\n * {@link GraphView.descendants}. Compression is **additive**: original\n * thoughts are never mutated or deleted.\n *\n * @module core/compression/CompressionService\n */\n\nimport type { ISummaryStore, Summary } from '../../contracts/summary.js';\nimport type { IEdgeStore } from '../../contracts/interfaces.js';\nimport type { Logger } from '../../logger/StructuredLogger.js';\nimport type { HistorySessionSnapshot, IHistoryManager } from '../IHistoryManager.js';\nimport type { ThoughtData } from '../thought.js';\nimport type { ConfidenceSignals } from '../reasoning.js';\nimport { GraphView } from '../graph/GraphView.js';\nimport { generateUlid } from '../ids.js';\nimport type { BranchId, SessionId, ThoughtId } from '../../contracts/ids.js';\n\n/** Stopwords excluded from topic extraction (lowercase). */\nconst STOPWORDS: ReadonlySet<string> = new Set([\n\t'the', 'this', 'that', 'then', 'than', 'with', 'from', 'have', 'will', 'been',\n\t'they', 'them', 'their', 'there', 'about', 'would', 'could', 'should', 'which',\n\t'where', 'when', 'what', 'some', 'into', 'also', 'just', 'like', 'over', 'such',\n\t'after', 'only', 'most', 'very', 'much', 'well', 'even', 'still', 'since',\n\t'being', 'doing', 'going', 'using',\n]);\n\nconst MIN_TOKEN_LENGTH = 4;\nconst TOP_TOPICS = 3;\n\n/** Local view of {@link ThoughtData} that exposes optional confidence signals. */\ntype ThoughtWithSignals = ThoughtData & { readonly confidence_signals?: ConfidenceSignals };\n\n/** Dependencies required by {@link CompressionService}. */\nexport interface CompressionDeps {\n\treadonly historyManager: IHistoryManager;\n\treadonly edgeStore: IEdgeStore;\n\treadonly summaryStore: ISummaryStore;\n\treadonly onSummaryCreated?: (summary: Summary) => void;\n\treadonly logger?: Logger;\n}\n\n/**\n * Deterministic, offline compression of branch subtrees into {@link Summary}.\n *\n * @example\n * ```typescript\n * const svc = new CompressionService({ historyManager, edgeStore, summaryStore });\n * const summary = svc.compressBranch('s1', 'alt-1', 'thought-root');\n * ```\n */\nexport class CompressionService {\n\tprivate readonly _deps: CompressionDeps;\n\tprivate readonly _graph: GraphView;\n\n\tconstructor(deps: CompressionDeps) {\n\t\tthis._deps = deps;\n\t\tthis._graph = new GraphView(deps.edgeStore);\n\t}\n\n\t/**\n\t * Compress the subtree rooted at `rootThoughtId` into a {@link Summary}.\n\t *\n\t * Idempotency: if a summary for the same `(branchId, rootThoughtId)` already\n\t * exists in the store, the existing summary is returned unchanged — same\n\t * inputs always yield the same Summary identity.\n\t */\n\tcompressBranch(sessionId: SessionId, branchId: BranchId, rootThoughtId: ThoughtId): Summary {\n\t\tconst existing = this._findExistingForRoot(sessionId, branchId, rootThoughtId);\n\t\tif (existing) return existing;\n\n\t\tconst snapshot = this._deps.historyManager.inspectSession(sessionId);\n\t\tconst candidateIds = this._collectCovered(sessionId, rootThoughtId);\n\t\tconst thoughts = this._resolveThoughts(snapshot, candidateIds);\n\t\tconst coveredIds = thoughts.flatMap((thought) =>\n\t\t\tthought.id === undefined ? [] : [thought.id]\n\t\t);\n\t\tconst summary: Summary = {\n\t\t\tid: generateUlid(),\n\t\t\tsessionId,\n\t\t\tbranchId,\n\t\t\trootThoughtId,\n\t\t\tcoveredIds,\n\t\t\tcoveredRange: this._coveredRange(thoughts),\n\t\t\ttopics: this._extractTopics(thoughts),\n\t\t\taggregateConfidence: this._meanConfidence(thoughts),\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tthis._deps.summaryStore.add(summary);\n\t\tthis._deps.onSummaryCreated?.(summary);\n\t\tthis._deps.logger?.debug('compression.branch.compressed', {\n\t\t\tsessionId,\n\t\t\tbranchId,\n\t\t\trootThoughtId,\n\t\t\tcovered: coveredIds.length,\n\t\t\ttopics: summary.topics,\n\t\t});\n\t\treturn summary;\n\t}\n\n\t/** Look up an existing summary on this branch with a matching root. */\n\tprivate _findExistingForRoot(\n\t\tsessionId: SessionId,\n\t\tbranchId: BranchId,\n\t\trootThoughtId: ThoughtId\n\t): Summary | undefined {\n\t\tfor (const s of this._deps.summaryStore.forBranch(sessionId, branchId)) {\n\t\t\tif (s.rootThoughtId === rootThoughtId) return s;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/** Collect the root and all of its descendants (chronological BFS). */\n\tprivate _collectCovered(sessionId: SessionId, rootThoughtId: ThoughtId): readonly ThoughtId[] {\n\t\tconst descendants = this._graph.descendants(sessionId, rootThoughtId);\n\t\treturn [rootThoughtId, ...descendants];\n\t}\n\n\t/**\n\t * Resolve candidate ids against one stable session snapshot. Main-history\n\t * records take precedence over branch copies; branch-only records are added\n\t * in declared branch order. Graph ids absent from the snapshot are omitted.\n\t */\n\tprivate _resolveThoughts(\n\t\tsnapshot: HistorySessionSnapshot,\n\t\tcandidateIds: readonly ThoughtId[]\n\t): ThoughtData[] {\n\t\tconst byId = new Map<ThoughtId, ThoughtData>();\n\t\tfor (const thought of snapshot.history) {\n\t\t\tif (thought.id !== undefined && !byId.has(thought.id)) byId.set(thought.id, thought);\n\t\t}\n\t\tfor (const branchId of snapshot.branchIds) {\n\t\t\tfor (const thought of snapshot.branches[branchId] ?? []) {\n\t\t\t\tif (thought.id !== undefined && !byId.has(thought.id)) byId.set(thought.id, thought);\n\t\t\t}\n\t\t}\n\n\t\tconst resolved: ThoughtData[] = [];\n\t\tconst seen = new Set<ThoughtId>();\n\t\tfor (const id of candidateIds) {\n\t\t\tif (seen.has(id)) continue;\n\t\t\tconst thought = byId.get(id);\n\t\t\tif (thought === undefined) continue;\n\t\t\tseen.add(id);\n\t\t\tresolved.push(thought);\n\t\t}\n\t\treturn resolved;\n\t}\n\n\t/**\n\t * Mean of (`calibrated_confidence` ?? `confidence` ?? 0) across covered.\n\t * Returns 0 when no thoughts are covered.\n\t */\n\tprivate _meanConfidence(thoughts: readonly ThoughtData[]): number {\n\t\tif (thoughts.length === 0) return 0;\n\t\tlet sum = 0;\n\t\tfor (const t of thoughts) {\n\t\t\tconst withSignals = t as ThoughtWithSignals;\n\t\t\tconst calibrated = withSignals.confidence_signals?.calibrated_confidence;\n\t\t\tsum += calibrated ?? t.confidence ?? 0;\n\t\t}\n\t\treturn sum / thoughts.length;\n\t}\n\n\t/**\n\t * Inclusive `[min, max]` of `thought_number` across covered thoughts.\n\t * Returns `[0, 0]` when no thoughts are covered.\n\t */\n\tprivate _coveredRange(thoughts: readonly ThoughtData[]): readonly [number, number] {\n\t\tif (thoughts.length === 0) return [0, 0];\n\t\tlet min = Number.POSITIVE_INFINITY;\n\t\tlet max = Number.NEGATIVE_INFINITY;\n\t\tfor (const t of thoughts) {\n\t\t\tconst n = t.thought_number;\n\t\t\tif (n < min) min = n;\n\t\t\tif (n > max) max = n;\n\t\t}\n\t\treturn [min, max];\n\t}\n\n\t/**\n\t * Extract top-3 unigrams by frequency from concatenated thought text.\n\t * Ties broken by first-occurrence order (stable).\n\t */\n\tprivate _extractTopics(thoughts: readonly ThoughtData[]): readonly string[] {\n\t\tif (thoughts.length === 0) return [];\n\t\tconst counts = new Map<string, number>();\n\t\tconst order = new Map<string, number>();\n\t\tlet position = 0;\n\t\tfor (const t of thoughts) {\n\t\t\tfor (const raw of (t.thought ?? '').split(/\\s+/)) {\n\t\t\t\tconst token = this._normalizeToken(raw);\n\t\t\t\tif (token === null) continue;\n\t\t\t\tif (!counts.has(token)) order.set(token, position++);\n\t\t\t\tcounts.set(token, (counts.get(token) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t\tif (counts.size === 0) return [];\n\t\tconst entries = Array.from(counts.entries());\n\t\tentries.sort((a, b) => {\n\t\t\tif (b[1] !== a[1]) return b[1] - a[1];\n\t\t\treturn (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0);\n\t\t});\n\t\treturn entries.slice(0, TOP_TOPICS).map(([word]) => word);\n\t}\n\n\t/** Lowercase + strip non-alphanumerics; reject if too short or a stopword. */\n\tprivate _normalizeToken(raw: string): string | null {\n\t\tconst cleaned = raw.toLowerCase().replace(/[^a-z0-9]+/g, '');\n\t\tif (cleaned.length < MIN_TOKEN_LENGTH) return null;\n\t\tif (STOPWORDS.has(cleaned)) return null;\n\t\treturn cleaned;\n\t}\n}\n"],"names":["STOPWORDS","Set","MIN_TOKEN_LENGTH","TOP_TOPICS","CompressionService","deps","GraphView","sessionId","branchId","rootThoughtId","existing","snapshot","candidateIds","thoughts","coveredIds","thought","undefined","summary","generateUlid","Date","s","descendants","byId","Map","resolved","seen","id","sum","t","withSignals","calibrated","min","Number","max","n","counts","order","position","raw","token","entries","Array","a","b","word","cleaned"],"mappings":"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"}
@@ -9,8 +9,8 @@
9
9
  * Pure and **non-mutating**: the input history array is never modified, and
10
10
  * neither the summary store nor any thought record is altered. When a cold
11
11
  * thought has no matching summary, the original {@link ThoughtData} is emitted
12
- * unchanged. Consecutive cold thoughts that fall within the same summary's
13
- * `coveredRange` are deduplicated to a single `SummaryRef`.
12
+ * unchanged. Consecutive cold thoughts listed in the same summary's stable
13
+ * `coveredIds` are deduplicated to a single `SummaryRef`.
14
14
  *
15
15
  * @module core/compression/DehydrationPolicy
16
16
  */
@@ -56,8 +56,8 @@ export declare class DehydrationPolicy {
56
56
  *
57
57
  * Returns a new array of `(ThoughtData | SummaryRef)` where:
58
58
  * - The last `keepLastK` thoughts are preserved verbatim.
59
- * - Older thoughts whose `thought_number` falls within an existing
60
- * summary's `coveredRange` are replaced by a single {@link SummaryRef}.
59
+ * - Older thoughts whose stable `id` appears in an existing summary's
60
+ * `coveredIds` are replaced by a single {@link SummaryRef}.
61
61
  * - Consecutive cold thoughts covered by the same summary collapse to one
62
62
  * `SummaryRef`.
63
63
  * - Cold thoughts with no matching summary are emitted verbatim.
@@ -1 +1 @@
1
- {"version":3,"file":"DehydrationPolicy.d.ts","sourceRoot":"","sources":["../../../src/core/compression/DehydrationPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAW,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAMxD,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjD;AAED,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAErD;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IACjB,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,aAAa;IAEzD;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CACJ,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,SAAS,EAAE,SAAS,EACpB,IAAI,CAAC,EAAE,kBAAkB,GACvB,aAAa,EAAE;CAgClB"}
1
+ {"version":3,"file":"DehydrationPolicy.d.ts","sourceRoot":"","sources":["../../../src/core/compression/DehydrationPolicy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAW,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAa,MAAM,wBAAwB,CAAC;AAKnE,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjD;AAED,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAErD;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IACjB,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAAb,aAAa,EAAE,aAAa;IAEzD;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CACJ,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,SAAS,EAAE,SAAS,EACpB,IAAI,CAAC,EAAE,kBAAkB,GACvB,aAAa,EAAE;CAiClB"}
@@ -15,7 +15,7 @@ class DehydrationPolicy {
15
15
  const out = [];
16
16
  let lastEmittedSummaryId;
17
17
  for (const thought of cold){
18
- const match = findCoveringSummary(summaries, thought.thought_number);
18
+ const match = void 0 === thought.id ? void 0 : findCoveringSummary(summaries, thought.id);
19
19
  if (void 0 === match) {
20
20
  out.push(thought);
21
21
  lastEmittedSummaryId = void 0;
@@ -34,11 +34,8 @@ class DehydrationPolicy {
34
34
  return out;
35
35
  }
36
36
  }
37
- function findCoveringSummary(summaries, thoughtNumber) {
38
- for (const s of summaries){
39
- const [lo, hi] = s.coveredRange;
40
- if (thoughtNumber >= lo && thoughtNumber <= hi) return s;
41
- }
37
+ function findCoveringSummary(summaries, thoughtId) {
38
+ for (const s of summaries)if (s.coveredIds.includes(thoughtId)) return s;
42
39
  }
43
40
  export { DehydrationPolicy };
44
41