lynkr 8.0.1 → 9.0.2

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.
Files changed (55) hide show
  1. package/README.md +238 -315
  2. package/bin/cli.js +16 -3
  3. package/index.js +7 -3
  4. package/install.sh +3 -3
  5. package/lynkr-skill.tar.gz +0 -0
  6. package/native/Cargo.toml +26 -0
  7. package/native/index.js +29 -0
  8. package/native/lynkr-native.node +0 -0
  9. package/native/src/lib.rs +321 -0
  10. package/package.json +8 -6
  11. package/src/api/files-multipart.js +30 -0
  12. package/src/api/files-router.js +81 -0
  13. package/src/api/openai-router.js +379 -308
  14. package/src/api/providers-handler.js +171 -3
  15. package/src/api/router.js +109 -5
  16. package/src/cache/prompt.js +13 -0
  17. package/src/clients/circuit-breaker.js +10 -247
  18. package/src/clients/codex-process.js +342 -0
  19. package/src/clients/codex-utils.js +143 -0
  20. package/src/clients/databricks.js +243 -76
  21. package/src/clients/ollama-utils.js +21 -17
  22. package/src/clients/openai-format.js +20 -6
  23. package/src/clients/openrouter-utils.js +42 -37
  24. package/src/clients/prompt-cache-injection.js +140 -0
  25. package/src/clients/provider-capabilities.js +41 -0
  26. package/src/clients/resilience.js +540 -0
  27. package/src/clients/responses-format.js +8 -7
  28. package/src/clients/retry.js +22 -167
  29. package/src/clients/standard-tools.js +1 -1
  30. package/src/clients/xml-tool-extractor.js +307 -0
  31. package/src/cluster.js +82 -0
  32. package/src/config/index.js +66 -0
  33. package/src/context/compression.js +42 -9
  34. package/src/context/distill.js +507 -0
  35. package/src/context/tool-result-compressor.js +563 -0
  36. package/src/memory/extractor.js +22 -0
  37. package/src/orchestrator/index.js +147 -205
  38. package/src/routing/complexity-analyzer.js +258 -5
  39. package/src/routing/index.js +15 -34
  40. package/src/routing/latency-tracker.js +148 -0
  41. package/src/routing/model-tiers.js +2 -0
  42. package/src/routing/quality-scorer.js +113 -0
  43. package/src/routing/telemetry.js +502 -0
  44. package/src/server.js +23 -0
  45. package/src/stores/file-store.js +69 -0
  46. package/src/stores/response-store.js +25 -0
  47. package/src/tools/code-graph.js +538 -0
  48. package/src/tools/code-mode.js +304 -0
  49. package/src/tools/index.js +1 -1
  50. package/src/tools/lazy-loader.js +11 -0
  51. package/src/tools/mcp-remote.js +7 -0
  52. package/src/tools/smart-selection.js +11 -0
  53. package/src/tools/web.js +1 -1
  54. package/src/utils/payload.js +206 -0
  55. package/src/utils/perf-timer.js +80 -0
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Routing Telemetry Module
3
+ *
4
+ * Persists per-request routing telemetry into a dedicated SQLite database
5
+ * at .lynkr/telemetry.db. Provides query helpers for dashboards, accuracy
6
+ * analysis, and automated routing feedback loops.
7
+ *
8
+ * Uses lazy initialisation so the proxy starts even when better-sqlite3 is
9
+ * not installed (it is an optionalDependency).
10
+ *
11
+ * @module routing/telemetry
12
+ */
13
+
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const logger = require("../logger");
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Lazy database initialisation
20
+ // ---------------------------------------------------------------------------
21
+
22
+ let Database;
23
+ try {
24
+ Database = require("better-sqlite3");
25
+ } catch {
26
+ Database = null;
27
+ }
28
+
29
+ /** @type {import('better-sqlite3').Database|null} */
30
+ let db = null;
31
+
32
+ /** @type {boolean} */
33
+ let initialised = false;
34
+
35
+ /** Default retention: 30 days */
36
+ const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
37
+
38
+ /**
39
+ * Initialise the telemetry database (singleton, idempotent).
40
+ * @returns {boolean} true if the DB is usable
41
+ */
42
+ function init() {
43
+ if (initialised) return db !== null;
44
+ initialised = true;
45
+
46
+ if (!Database) {
47
+ logger.debug("Telemetry: better-sqlite3 not available, telemetry disabled");
48
+ return false;
49
+ }
50
+
51
+ try {
52
+ const dbDir = path.resolve(process.cwd(), ".lynkr");
53
+ if (!fs.existsSync(dbDir)) {
54
+ fs.mkdirSync(dbDir, { recursive: true });
55
+ }
56
+
57
+ const dbPath = path.join(dbDir, "telemetry.db");
58
+ db = new Database(dbPath, {
59
+ verbose: process.env.DEBUG_SQL ? console.log : null,
60
+ fileMustExist: false,
61
+ });
62
+
63
+ // Performance pragmas (same pattern as src/db/index.js)
64
+ db.pragma("journal_mode = WAL");
65
+ db.pragma("synchronous = NORMAL");
66
+ db.pragma("cache_size = -16000");
67
+ db.pragma("temp_store = MEMORY");
68
+ db.pragma("busy_timeout = 3000");
69
+
70
+ db.exec(`
71
+ CREATE TABLE IF NOT EXISTS routing_telemetry (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ request_id TEXT NOT NULL,
74
+ session_id TEXT,
75
+ timestamp INTEGER NOT NULL,
76
+ complexity_score REAL,
77
+ tier TEXT,
78
+ agentic_type TEXT,
79
+ tool_count INTEGER,
80
+ input_tokens INTEGER,
81
+ message_count INTEGER,
82
+ request_type TEXT,
83
+ provider TEXT NOT NULL,
84
+ model TEXT,
85
+ routing_method TEXT,
86
+ was_fallback INTEGER DEFAULT 0,
87
+ output_tokens INTEGER,
88
+ latency_ms INTEGER,
89
+ status_code INTEGER,
90
+ error_type TEXT,
91
+ cost_usd REAL,
92
+ tool_calls_made INTEGER,
93
+ retry_count INTEGER DEFAULT 0,
94
+ circuit_breaker_state TEXT,
95
+ quality_score REAL,
96
+ tokens_per_second REAL,
97
+ cost_efficiency REAL
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_telemetry_provider
101
+ ON routing_telemetry(provider);
102
+
103
+ CREATE INDEX IF NOT EXISTS idx_telemetry_tier
104
+ ON routing_telemetry(tier);
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_telemetry_timestamp
107
+ ON routing_telemetry(timestamp);
108
+ `);
109
+
110
+ logger.info({ dbPath }, "Routing telemetry database initialised");
111
+ return true;
112
+ } catch (err) {
113
+ logger.warn({ err: err.message }, "Failed to initialise telemetry database");
114
+ db = null;
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Prepared statements (created lazily after init)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /** @type {Map<string, import('better-sqlite3').Statement>} */
124
+ const stmts = new Map();
125
+
126
+ /**
127
+ * Get or create a prepared statement.
128
+ * @param {string} key
129
+ * @param {string} sql
130
+ * @returns {import('better-sqlite3').Statement|null}
131
+ */
132
+ function stmt(key, sql) {
133
+ if (!db) return null;
134
+ if (!stmts.has(key)) {
135
+ stmts.set(key, db.prepare(sql));
136
+ }
137
+ return stmts.get(key);
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Public API
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Record a telemetry data point. Executes asynchronously via setImmediate
146
+ * so it never blocks the request path.
147
+ *
148
+ * @param {Object} data - Telemetry fields (see table schema)
149
+ */
150
+ function record(data) {
151
+ if (!init()) return;
152
+
153
+ setImmediate(() => {
154
+ try {
155
+ const insert = stmt(
156
+ "insert",
157
+ `INSERT INTO routing_telemetry (
158
+ request_id, session_id, timestamp, complexity_score, tier,
159
+ agentic_type, tool_count, input_tokens, message_count, request_type,
160
+ provider, model, routing_method, was_fallback, output_tokens,
161
+ latency_ms, status_code, error_type, cost_usd, tool_calls_made,
162
+ retry_count, circuit_breaker_state, quality_score, tokens_per_second,
163
+ cost_efficiency
164
+ ) VALUES (
165
+ @request_id, @session_id, @timestamp, @complexity_score, @tier,
166
+ @agentic_type, @tool_count, @input_tokens, @message_count, @request_type,
167
+ @provider, @model, @routing_method, @was_fallback, @output_tokens,
168
+ @latency_ms, @status_code, @error_type, @cost_usd, @tool_calls_made,
169
+ @retry_count, @circuit_breaker_state, @quality_score, @tokens_per_second,
170
+ @cost_efficiency
171
+ )`
172
+ );
173
+ if (!insert) return;
174
+
175
+ insert.run({
176
+ request_id: data.request_id ?? null,
177
+ session_id: data.session_id ?? null,
178
+ timestamp: data.timestamp ?? Date.now(),
179
+ complexity_score: data.complexity_score ?? null,
180
+ tier: data.tier ?? null,
181
+ agentic_type: data.agentic_type ?? null,
182
+ tool_count: data.tool_count ?? null,
183
+ input_tokens: data.input_tokens ?? null,
184
+ message_count: data.message_count ?? null,
185
+ request_type: data.request_type ?? null,
186
+ provider: data.provider,
187
+ model: data.model ?? null,
188
+ routing_method: data.routing_method ?? null,
189
+ was_fallback: data.was_fallback ? 1 : 0,
190
+ output_tokens: data.output_tokens ?? null,
191
+ latency_ms: data.latency_ms ?? null,
192
+ status_code: data.status_code ?? null,
193
+ error_type: data.error_type ?? null,
194
+ cost_usd: data.cost_usd ?? null,
195
+ tool_calls_made: data.tool_calls_made ?? null,
196
+ retry_count: data.retry_count ?? 0,
197
+ circuit_breaker_state: data.circuit_breaker_state ?? null,
198
+ quality_score: data.quality_score ?? null,
199
+ tokens_per_second: data.tokens_per_second ?? null,
200
+ cost_efficiency: data.cost_efficiency ?? null,
201
+ });
202
+ } catch (err) {
203
+ logger.debug({ err: err.message }, "Telemetry record failed");
204
+ }
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Query telemetry records with optional filters.
210
+ *
211
+ * @param {Object} [filters]
212
+ * @param {string} [filters.provider] - Filter by provider name
213
+ * @param {string} [filters.tier] - Filter by tier
214
+ * @param {number} [filters.since] - Only records after this timestamp (ms)
215
+ * @param {number} [filters.limit] - Max rows to return (default 100)
216
+ * @returns {Object[]} Matching telemetry rows
217
+ */
218
+ function query(filters = {}) {
219
+ if (!init()) return [];
220
+
221
+ const clauses = [];
222
+ const params = {};
223
+
224
+ if (filters.provider) {
225
+ clauses.push("provider = @provider");
226
+ params.provider = filters.provider;
227
+ }
228
+ if (filters.tier) {
229
+ clauses.push("tier = @tier");
230
+ params.tier = filters.tier;
231
+ }
232
+ if (filters.since) {
233
+ clauses.push("timestamp >= @since");
234
+ params.since = filters.since;
235
+ }
236
+
237
+ const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
238
+ const limit = filters.limit ?? 100;
239
+
240
+ try {
241
+ const sql = `SELECT * FROM routing_telemetry ${where} ORDER BY timestamp DESC LIMIT ${Number(limit)}`;
242
+ return db.prepare(sql).all(params);
243
+ } catch (err) {
244
+ logger.debug({ err: err.message }, "Telemetry query failed");
245
+ return [];
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Get aggregate statistics over a time range.
251
+ *
252
+ * @param {Object} [timeRange]
253
+ * @param {number} [timeRange.since] - Start timestamp (ms). Defaults to 24 hours ago.
254
+ * @param {number} [timeRange.until] - End timestamp (ms). Defaults to now.
255
+ * @returns {Object|null} Aggregated statistics
256
+ */
257
+ function getStats(timeRange = {}) {
258
+ if (!init()) return null;
259
+
260
+ const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
261
+ const until = timeRange.until ?? Date.now();
262
+
263
+ try {
264
+ // Total requests
265
+ const total = db
266
+ .prepare("SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ?")
267
+ .get(since, until);
268
+
269
+ if (!total || total.cnt === 0) return null;
270
+
271
+ // Average latency per provider
272
+ const latencyRows = db
273
+ .prepare(
274
+ `SELECT provider, AVG(latency_ms) as avg_latency, COUNT(*) as cnt
275
+ FROM routing_telemetry
276
+ WHERE timestamp BETWEEN ? AND ? AND latency_ms IS NOT NULL
277
+ GROUP BY provider`
278
+ )
279
+ .all(since, until);
280
+
281
+ const avgLatencyByProvider = {};
282
+ for (const row of latencyRows) {
283
+ avgLatencyByProvider[row.provider] = Math.round(row.avg_latency);
284
+ }
285
+
286
+ // Average quality per tier
287
+ const qualityRows = db
288
+ .prepare(
289
+ `SELECT tier, AVG(quality_score) as avg_quality, COUNT(*) as cnt
290
+ FROM routing_telemetry
291
+ WHERE timestamp BETWEEN ? AND ? AND quality_score IS NOT NULL AND tier IS NOT NULL
292
+ GROUP BY tier`
293
+ )
294
+ .all(since, until);
295
+
296
+ const avgQualityByTier = {};
297
+ for (const row of qualityRows) {
298
+ avgQualityByTier[row.tier] = Math.round(row.avg_quality * 10) / 10;
299
+ }
300
+
301
+ // Error rate
302
+ const errors = db
303
+ .prepare(
304
+ "SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ? AND error_type IS NOT NULL"
305
+ )
306
+ .get(since, until);
307
+
308
+ const errorRate = Math.round((errors.cnt / total.cnt) * 1000) / 10; // one decimal %
309
+
310
+ // Over/under provisioned percentages
311
+ const accuracy = getRoutingAccuracy({ since, until });
312
+
313
+ return {
314
+ totalRequests: total.cnt,
315
+ avgLatencyByProvider,
316
+ avgQualityByTier,
317
+ errorRate,
318
+ overProvisionedPct: accuracy ? accuracy.overProvisionedPct : 0,
319
+ underProvisionedPct: accuracy ? accuracy.underProvisionedPct : 0,
320
+ };
321
+ } catch (err) {
322
+ logger.debug({ err: err.message }, "Telemetry getStats failed");
323
+ return null;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Get aggregated statistics for a specific provider.
329
+ *
330
+ * @param {string} provider - Provider name
331
+ * @param {Object} [timeRange]
332
+ * @param {number} [timeRange.since]
333
+ * @param {number} [timeRange.until]
334
+ * @returns {Object|null}
335
+ */
336
+ function getProviderStats(provider, timeRange = {}) {
337
+ if (!init()) return null;
338
+
339
+ const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
340
+ const until = timeRange.until ?? Date.now();
341
+
342
+ try {
343
+ const row = db
344
+ .prepare(
345
+ `SELECT
346
+ COUNT(*) as total,
347
+ AVG(latency_ms) as avg_latency,
348
+ AVG(quality_score) as avg_quality,
349
+ AVG(output_tokens) as avg_output_tokens,
350
+ SUM(CASE WHEN error_type IS NOT NULL THEN 1 ELSE 0 END) as errors,
351
+ SUM(CASE WHEN was_fallback = 1 THEN 1 ELSE 0 END) as fallbacks,
352
+ AVG(tokens_per_second) as avg_tps,
353
+ SUM(cost_usd) as total_cost
354
+ FROM routing_telemetry
355
+ WHERE provider = ? AND timestamp BETWEEN ? AND ?`
356
+ )
357
+ .get(provider, since, until);
358
+
359
+ if (!row || row.total === 0) return null;
360
+
361
+ return {
362
+ total: row.total,
363
+ avgLatency: row.avg_latency ? Math.round(row.avg_latency) : null,
364
+ avgQuality: row.avg_quality ? Math.round(row.avg_quality * 10) / 10 : null,
365
+ avgOutputTokens: row.avg_output_tokens ? Math.round(row.avg_output_tokens) : null,
366
+ errorRate: Math.round((row.errors / row.total) * 1000) / 10,
367
+ fallbackRate: Math.round((row.fallbacks / row.total) * 1000) / 10,
368
+ avgTokensPerSecond: row.avg_tps ? Math.round(row.avg_tps * 10) / 10 : null,
369
+ totalCost: row.total_cost ? Math.round(row.total_cost * 10000) / 10000 : null,
370
+ };
371
+ } catch (err) {
372
+ logger.debug({ err: err.message }, "Telemetry getProviderStats failed");
373
+ return null;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Calculate routing accuracy: percentage of requests that were over- or
379
+ * under-provisioned.
380
+ *
381
+ * Over-provisioned: quality_score > 80 AND output_tokens < 50 on REASONING or COMPLEX tier.
382
+ * Under-provisioned: quality_score < 45 on SIMPLE tier.
383
+ *
384
+ * @param {Object} [timeRange]
385
+ * @param {number} [timeRange.since]
386
+ * @param {number} [timeRange.until]
387
+ * @returns {Object|null}
388
+ */
389
+ function getRoutingAccuracy(timeRange = {}) {
390
+ if (!init()) return null;
391
+
392
+ const since = timeRange.since ?? Date.now() - 24 * 60 * 60 * 1000;
393
+ const until = timeRange.until ?? Date.now();
394
+
395
+ try {
396
+ const total = db
397
+ .prepare("SELECT COUNT(*) as cnt FROM routing_telemetry WHERE timestamp BETWEEN ? AND ?")
398
+ .get(since, until);
399
+
400
+ if (!total || total.cnt === 0) return null;
401
+
402
+ const overProvisioned = db
403
+ .prepare(
404
+ `SELECT COUNT(*) as cnt FROM routing_telemetry
405
+ WHERE timestamp BETWEEN ? AND ?
406
+ AND quality_score > 80
407
+ AND output_tokens < 50
408
+ AND tier IN ('REASONING', 'COMPLEX')`
409
+ )
410
+ .get(since, until);
411
+
412
+ const underProvisioned = db
413
+ .prepare(
414
+ `SELECT COUNT(*) as cnt FROM routing_telemetry
415
+ WHERE timestamp BETWEEN ? AND ?
416
+ AND quality_score < 45
417
+ AND tier = 'SIMPLE'`
418
+ )
419
+ .get(since, until);
420
+
421
+ return {
422
+ totalRequests: total.cnt,
423
+ overProvisioned: overProvisioned.cnt,
424
+ underProvisioned: underProvisioned.cnt,
425
+ overProvisionedPct: Math.round((overProvisioned.cnt / total.cnt) * 1000) / 10,
426
+ underProvisionedPct: Math.round((underProvisioned.cnt / total.cnt) * 1000) / 10,
427
+ };
428
+ } catch (err) {
429
+ logger.debug({ err: err.message }, "Telemetry getRoutingAccuracy failed");
430
+ return null;
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Delete telemetry records older than a given threshold.
436
+ *
437
+ * @param {number} [olderThanMs] - Age threshold in ms. Defaults to 30 days.
438
+ * @returns {number} Number of rows deleted
439
+ */
440
+ function cleanup(olderThanMs) {
441
+ if (!init()) return 0;
442
+
443
+ const threshold = Date.now() - (olderThanMs ?? DEFAULT_RETENTION_MS);
444
+
445
+ try {
446
+ const del = stmt("cleanup", "DELETE FROM routing_telemetry WHERE timestamp < ?");
447
+ if (!del) return 0;
448
+ const result = del.run(threshold);
449
+ logger.debug({ deleted: result.changes }, "Telemetry cleanup complete");
450
+ return result.changes;
451
+ } catch (err) {
452
+ logger.debug({ err: err.message }, "Telemetry cleanup failed");
453
+ return 0;
454
+ }
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // In-memory stats cache (avoids SQLite queries on every /v1/routing/stats hit)
459
+ // ---------------------------------------------------------------------------
460
+
461
+ const STATS_CACHE_TTL = 5000; // 5 seconds
462
+ let statsCache = null;
463
+ let statsCacheTs = 0;
464
+
465
+ function getStatsCached(timeRange = {}) {
466
+ const now = Date.now();
467
+ // Use cache for default time range (last 24h) — custom ranges bypass cache
468
+ if (!timeRange.since && !timeRange.until && statsCache && now - statsCacheTs < STATS_CACHE_TTL) {
469
+ return statsCache;
470
+ }
471
+ const result = getStats(timeRange);
472
+ if (!timeRange.since && !timeRange.until) {
473
+ statsCache = result;
474
+ statsCacheTs = now;
475
+ }
476
+ return result;
477
+ }
478
+
479
+ let providerStatsCache = new Map();
480
+ let providerStatsCacheTs = 0;
481
+
482
+ function getProviderStatsCached(provider, timeRange = {}) {
483
+ const now = Date.now();
484
+ if (!timeRange.since && !timeRange.until && providerStatsCache.has(provider) && now - providerStatsCacheTs < STATS_CACHE_TTL) {
485
+ return providerStatsCache.get(provider);
486
+ }
487
+ const result = getProviderStats(provider, timeRange);
488
+ if (!timeRange.since && !timeRange.until) {
489
+ providerStatsCache.set(provider, result);
490
+ providerStatsCacheTs = now;
491
+ }
492
+ return result;
493
+ }
494
+
495
+ module.exports = {
496
+ record,
497
+ query,
498
+ getStats: getStatsCached,
499
+ getProviderStats: getProviderStatsCached,
500
+ getRoutingAccuracy,
501
+ cleanup,
502
+ };
package/src/server.js CHANGED
@@ -147,6 +147,10 @@ function createApp() {
147
147
 
148
148
  app.use(router);
149
149
 
150
+ // Files API
151
+ const filesRouter = require("./api/files-router");
152
+ app.use("/v1", filesRouter);
153
+
150
154
  // 404 handler (must be after all routes)
151
155
  app.use(notFoundHandler);
152
156
 
@@ -195,6 +199,14 @@ async function start() {
195
199
  const provider = config.modelProvider?.type?.toLowerCase();
196
200
  if (provider === "ollama" || config.tiersReferenceOllama()) {
197
201
  await waitForOllama();
202
+
203
+ // Pre-probe Ollama's Anthropic API at startup (avoids 1-3s cold-start on first request)
204
+ try {
205
+ const { hasAnthropicEndpoint } = require("./clients/ollama-utils");
206
+ await hasAnthropicEndpoint(config.ollama.endpoint);
207
+ } catch (err) {
208
+ logger.debug({ err: err.message }, "Ollama Anthropic endpoint probe failed at startup");
209
+ }
198
210
  }
199
211
 
200
212
  const server = app.listen(config.port, () => {
@@ -228,6 +240,17 @@ async function start() {
228
240
  });
229
241
  }
230
242
 
243
+ // Register Codex process shutdown callback
244
+ shutdownManager.onShutdown(async () => {
245
+ try {
246
+ const { getCodexProcess } = require("./clients/codex-process");
247
+ const codex = getCodexProcess();
248
+ if (codex.child) {
249
+ await codex.shutdown();
250
+ }
251
+ } catch { /* ignore if codex never started */ }
252
+ });
253
+
231
254
  // Initialize hot reload config watcher
232
255
  if (config.hotReload?.enabled !== false) {
233
256
  const watcher = initConfigWatcher({
@@ -0,0 +1,69 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const crypto = require("crypto");
4
+ const logger = require("../logger");
5
+
6
+ const STORAGE_DIR = path.resolve(process.env.FILES_STORAGE_PATH || "./data/files");
7
+ const MAX_FILES = parseInt(process.env.FILES_MAX_COUNT || "1000", 10);
8
+
9
+ const metadata = new Map();
10
+
11
+ function ensureStorageDir() {
12
+ if (!fs.existsSync(STORAGE_DIR)) {
13
+ fs.mkdirSync(STORAGE_DIR, { recursive: true });
14
+ }
15
+ }
16
+
17
+ function storeFile(buffer, { filename, purpose, mimeType }) {
18
+ ensureStorageDir();
19
+ if (metadata.size >= MAX_FILES) {
20
+ const oldest = metadata.keys().next().value;
21
+ deleteFile(oldest);
22
+ }
23
+ const id = `file-${crypto.randomUUID()}`;
24
+ const storagePath = path.join(STORAGE_DIR, id);
25
+ fs.writeFileSync(storagePath, buffer);
26
+ const entry = {
27
+ id,
28
+ object: "file",
29
+ filename: filename || "upload",
30
+ purpose: purpose || "assistants",
31
+ bytes: buffer.length,
32
+ mime_type: mimeType || "application/octet-stream",
33
+ created_at: Math.floor(Date.now() / 1000),
34
+ storage_path: storagePath,
35
+ };
36
+ metadata.set(id, entry);
37
+ logger.info({ fileId: id, bytes: buffer.length, filename }, "File stored");
38
+ return entry;
39
+ }
40
+
41
+ function getFile(id) {
42
+ return metadata.get(id) || null;
43
+ }
44
+
45
+ function getFileContent(id) {
46
+ const entry = metadata.get(id);
47
+ if (!entry) return null;
48
+ try {
49
+ return fs.readFileSync(entry.storage_path);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function deleteFile(id) {
56
+ const entry = metadata.get(id);
57
+ if (!entry) return false;
58
+ try { fs.unlinkSync(entry.storage_path); } catch {}
59
+ metadata.delete(id);
60
+ return true;
61
+ }
62
+
63
+ function listFiles({ purpose } = {}) {
64
+ const files = Array.from(metadata.values());
65
+ if (purpose) return files.filter((f) => f.purpose === purpose);
66
+ return files;
67
+ }
68
+
69
+ module.exports = { storeFile, getFile, getFileContent, deleteFile, listFiles };
@@ -0,0 +1,25 @@
1
+ const MAX_ENTRIES = 1000;
2
+
3
+ const store = new Map();
4
+
5
+ function storeResponse(id, data) {
6
+ if (store.size >= MAX_ENTRIES) {
7
+ const oldest = store.keys().next().value;
8
+ store.delete(oldest);
9
+ }
10
+ store.set(id, { ...data, createdAt: Date.now() });
11
+ }
12
+
13
+ function getResponse(id) {
14
+ return store.get(id) || null;
15
+ }
16
+
17
+ function deleteResponse(id) {
18
+ return store.delete(id);
19
+ }
20
+
21
+ function size() {
22
+ return store.size;
23
+ }
24
+
25
+ module.exports = { storeResponse, getResponse, deleteResponse, size };