lynkr 9.9.1 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +50 -9
  2. package/bin/cli.js +2 -0
  3. package/bin/lynkr-init.js +11 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/public/dashboard.html +29 -0
  8. package/scripts/validate-difficulty-classifier.js +27 -6
  9. package/src/api/providers-handler.js +0 -1
  10. package/src/api/router.js +309 -160
  11. package/src/clients/databricks.js +215 -71
  12. package/src/clients/gpt-utils.js +8 -1
  13. package/src/clients/ollama-utils.js +19 -10
  14. package/src/clients/prompt-cache-injection.js +93 -30
  15. package/src/config/index.js +82 -43
  16. package/src/context/compression.js +27 -8
  17. package/src/context/distill.js +2 -1
  18. package/src/dashboard/api.js +5 -1
  19. package/src/memory/distiller.js +350 -0
  20. package/src/memory/extractor.js +0 -8
  21. package/src/memory/format.js +17 -2
  22. package/src/memory/index.js +10 -0
  23. package/src/memory/retriever.js +7 -2
  24. package/src/memory/skills-cache.js +180 -0
  25. package/src/memory/tencentdb-launcher.js +459 -0
  26. package/src/memory/wiki.js +171 -0
  27. package/src/orchestrator/index.js +202 -1237
  28. package/src/orchestrator/passthrough-stream.js +382 -0
  29. package/src/orchestrator/sse-transformer.js +408 -0
  30. package/src/prompts/system.js +20 -6
  31. package/src/routing/affinity-store.js +85 -4
  32. package/src/routing/agentic-detector.js +9 -1
  33. package/src/routing/cache-economics.js +123 -0
  34. package/src/routing/cache-switch-cost.js +194 -0
  35. package/src/routing/calibration.js +8 -1
  36. package/src/routing/complexity-analyzer.js +7 -1
  37. package/src/routing/context-compressor.js +170 -0
  38. package/src/routing/difficulty-classifier.js +161 -17
  39. package/src/routing/index.js +196 -56
  40. package/src/routing/intent-score.js +20 -2
  41. package/src/routing/model-registry.js +35 -10
  42. package/src/routing/session-affinity.js +68 -1
  43. package/src/routing/side-channel-detector.js +103 -0
  44. package/src/routing/telemetry.js +139 -2
  45. package/src/routing/verifier.js +41 -2
  46. package/src/server.js +36 -49
  47. package/src/tools/code-graph.js +6 -4
  48. package/src/agents/context-manager.js +0 -236
  49. package/src/agents/decomposition/dispatcher.js +0 -185
  50. package/src/agents/decomposition/gate.js +0 -136
  51. package/src/agents/decomposition/index.js +0 -183
  52. package/src/agents/decomposition/model-call.js +0 -75
  53. package/src/agents/decomposition/planner.js +0 -223
  54. package/src/agents/decomposition/synthesizer.js +0 -89
  55. package/src/agents/decomposition/telemetry.js +0 -55
  56. package/src/agents/definitions/loader.js +0 -653
  57. package/src/agents/executor.js +0 -457
  58. package/src/agents/index.js +0 -165
  59. package/src/agents/parallel-coordinator.js +0 -68
  60. package/src/agents/reflector.js +0 -331
  61. package/src/agents/skillbook.js +0 -331
  62. package/src/agents/store.js +0 -259
  63. package/src/edits/index.js +0 -171
  64. package/src/indexer/babel-parser.js +0 -213
  65. package/src/indexer/index.js +0 -1629
  66. package/src/indexer/navigation/index.js +0 -32
  67. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  68. package/src/indexer/parser.js +0 -443
  69. package/src/tasks/store.js +0 -349
  70. package/src/tests/coverage.js +0 -173
  71. package/src/tests/index.js +0 -171
  72. package/src/tests/store.js +0 -213
  73. package/src/tools/agent-task.js +0 -145
  74. package/src/tools/code-mode.js +0 -304
  75. package/src/tools/decompose.js +0 -91
  76. package/src/tools/edits.js +0 -94
  77. package/src/tools/execution.js +0 -171
  78. package/src/tools/git.js +0 -1346
  79. package/src/tools/index.js +0 -306
  80. package/src/tools/indexer.js +0 -360
  81. package/src/tools/lazy-loader.js +0 -366
  82. package/src/tools/mcp-remote.js +0 -88
  83. package/src/tools/mcp.js +0 -116
  84. package/src/tools/process.js +0 -167
  85. package/src/tools/smart-selection.js +0 -180
  86. package/src/tools/stubs.js +0 -55
  87. package/src/tools/tasks.js +0 -260
  88. package/src/tools/tests.js +0 -132
  89. package/src/tools/tinyfish.js +0 -358
  90. package/src/tools/truncate.js +0 -106
  91. package/src/tools/web-client.js +0 -71
  92. package/src/tools/web.js +0 -415
  93. package/src/tools/workspace.js +0 -204
@@ -20,11 +20,26 @@ function formatMemoriesForContext(memories, format = 'compact') {
20
20
  return formatCompact(memories);
21
21
  }
22
22
 
23
+ /**
24
+ * Deterministic render order (cache-aware routing, Phase 5): retrieval
25
+ * returns equal-relevance memories in unstable order, and the rendered
26
+ * bullets sit in the system prompt — the very front of the provider's
27
+ * prompt-cache prefix. A reordered bullet list is a byte change that
28
+ * silently invalidates the entire cached prefix (observed live: identical
29
+ * memory sets flipping order between consecutive requests). Sorting by
30
+ * content pins the bytes; presentation order carries no meaning here.
31
+ */
32
+ function stableOrder(memories) {
33
+ return [...memories].sort((a, b) =>
34
+ String(a.content) < String(b.content) ? -1 : String(a.content) > String(b.content) ? 1 : 0
35
+ );
36
+ }
37
+
23
38
  /**
24
39
  * Compact memory format - 75% fewer tokens
25
40
  */
26
41
  function formatCompact(memories) {
27
- const items = memories
42
+ const items = stableOrder(memories)
28
43
  .map(mem => `- ${mem.content}`)
29
44
  .join('\n');
30
45
 
@@ -35,7 +50,7 @@ function formatCompact(memories) {
35
50
  * Verbose XML format (original)
36
51
  */
37
52
  function formatVerbose(memories) {
38
- const items = memories.map((mem, idx) => {
53
+ const items = stableOrder(memories).map((mem, idx) => {
39
54
  const age = formatAge(mem.createdAt);
40
55
  const type = mem.type ? `[${mem.type}] ` : '';
41
56
  return `${idx + 1}. ${type}${mem.content} (${age})`;
@@ -15,6 +15,9 @@ const retriever = require("./retriever");
15
15
  const extractor = require("./extractor");
16
16
  const surprise = require("./surprise");
17
17
  const tools = require("./tools");
18
+ const distiller = require("./distiller");
19
+ const wiki = require("./wiki");
20
+ const skillsCache = require("./skills-cache");
18
21
 
19
22
  module.exports = {
20
23
  // Store operations
@@ -52,4 +55,11 @@ module.exports = {
52
55
  // Tools
53
56
  tools,
54
57
  MEMORY_TOOLS: tools.MEMORY_TOOLS,
58
+
59
+ // TencentDB-inspired L0-L3 token optimization
60
+ distiller,
61
+ needsDistillation: distiller.needsDistillation,
62
+ distillMessages: distiller.distillMessages,
63
+ wiki,
64
+ skillsCache,
55
65
  };
@@ -3,6 +3,10 @@ const search = require("./search");
3
3
  const logger = require("../logger");
4
4
  const format = require("./format");
5
5
 
6
+ // Infrastructure memory types (wiki registry, compression skills) are used
7
+ // by the distillation pipeline, never injected as conversational context
8
+ const INTERNAL_MEMORY_TYPES = new Set(["wiki", "skill"]);
9
+
6
10
  /**
7
11
  * Retrieve relevant memories using multi-signal ranking
8
12
  *
@@ -41,8 +45,9 @@ function retrieveRelevantMemories(query, options = {}) {
41
45
  sessionId: includeGlobal ? null : sessionId,
42
46
  });
43
47
 
44
- // 4. Merge and deduplicate
45
- const candidates = mergeUnique([ftsResults, recentMemories, importantMemories]);
48
+ // 4. Merge and deduplicate, dropping infrastructure entries
49
+ const candidates = mergeUnique([ftsResults, recentMemories, importantMemories])
50
+ .filter(m => !INTERNAL_MEMORY_TYPES.has(m.type));
46
51
 
47
52
  // 5. Score and rank
48
53
  const scored = candidates.map(memory => ({
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Skills Cache — Compression Strategy Memory (TencentDB-Agent-Memory inspired)
3
+ *
4
+ * Remembers which compression method worked (and how well) for each
5
+ * structural shape of tool output. Validated skills (savings >= minSavings)
6
+ * persist in the memories table (type='skill') and let future compressions:
7
+ * - skip section-dedup for shapes where it never helps (latency win)
8
+ * - compress proven-compressible shapes more aggressively (token win)
9
+ */
10
+
11
+ const store = require("./store");
12
+ const distill = require("../context/distill");
13
+ const config = require("../config");
14
+ const logger = require("../logger");
15
+
16
+ const MAX_CACHED_SKILLS = 1000;
17
+ const SIGNATURE_LINES = 30;
18
+
19
+ // signature -> { method, avgSavings, hits, persistedId }
20
+ let skillMap = null;
21
+
22
+ function skillsConfig() {
23
+ return config.memory?.skills ?? {};
24
+ }
25
+
26
+ /**
27
+ * Structural shape signature for a tool output: leading token of each of
28
+ * the first N normalized lines. Two grep outputs, two test runs, or two
29
+ * JSON blobs with the same shape produce the same signature even when the
30
+ * values differ.
31
+ */
32
+ function shapeSignature(text) {
33
+ if (!text) return "empty";
34
+ const lines = distill.normalizeText(text).split("\n").slice(0, SIGNATURE_LINES);
35
+ const shape = lines
36
+ .map(l => {
37
+ const trimmed = l.trim();
38
+ if (!trimmed) return "";
39
+ // Leading structural token: punctuation kept, words folded
40
+ const lead = trimmed.match(/^[{}[\]"'\-+*#>|]|^[A-Za-z_$]+|^\d+/);
41
+ return lead ? (/^\d+$/.test(lead[0]) ? "N" : lead[0]) : trimmed[0];
42
+ })
43
+ .join(",");
44
+ // djb2 hash keeps keys short
45
+ let hash = 5381;
46
+ for (let i = 0; i < shape.length; i++) {
47
+ hash = ((hash << 5) + hash + shape.charCodeAt(i)) | 0;
48
+ }
49
+ return `s${lines.length}_${(hash >>> 0).toString(36)}`;
50
+ }
51
+
52
+ function loadCache() {
53
+ if (skillMap) return skillMap;
54
+
55
+ skillMap = new Map();
56
+ try {
57
+ const rows = store.getMemoriesByType("skill", MAX_CACHED_SKILLS);
58
+ for (const row of rows) {
59
+ const meta = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
60
+ if (!meta?.signature) continue;
61
+ skillMap.set(meta.signature, {
62
+ method: meta.method,
63
+ avgSavings: meta.avgSavings ?? 0,
64
+ hits: meta.hits ?? 1,
65
+ persistedId: row.id,
66
+ });
67
+ }
68
+ } catch (err) {
69
+ logger.warn({ err }, "[skills] Failed to load skills, starting empty");
70
+ }
71
+ return skillMap;
72
+ }
73
+
74
+ /**
75
+ * Look up a known compression skill for a shape signature.
76
+ * @returns {{method: string, avgSavings: number, hits: number}|null}
77
+ */
78
+ function lookup(signature) {
79
+ if (skillsConfig().enabled === false) return null;
80
+ return loadCache().get(signature) ?? null;
81
+ }
82
+
83
+ /**
84
+ * Record a compression outcome. Kept in memory always (so known-futile
85
+ * shapes can skip dedup work); persisted only when the running average
86
+ * savings clears minSavings — the "validated skill" bar.
87
+ *
88
+ * @param {string} signature - shapeSignature() of the original text
89
+ * @param {string} method - compression method that ran ('delta'|'distill'|'passthrough')
90
+ * @param {number} savings - fraction saved, 0..1
91
+ */
92
+ function record(signature, method, savings) {
93
+ if (skillsConfig().enabled === false || !signature) return;
94
+
95
+ const cache = loadCache();
96
+ const existing = cache.get(signature);
97
+ const entry = existing
98
+ ? {
99
+ ...existing,
100
+ method,
101
+ hits: existing.hits + 1,
102
+ avgSavings: (existing.avgSavings * existing.hits + savings) / (existing.hits + 1),
103
+ }
104
+ : { method, avgSavings: savings, hits: 1, persistedId: null };
105
+
106
+ cache.set(signature, entry);
107
+ if (cache.size > MAX_CACHED_SKILLS) {
108
+ cache.delete(cache.keys().next().value);
109
+ }
110
+
111
+ const minSavings = skillsConfig().minSavings ?? 0.6;
112
+ if (entry.avgSavings < minSavings) return;
113
+
114
+ // Persist validated skill off the hot path
115
+ setImmediate(() => {
116
+ try {
117
+ const metadata = {
118
+ signature,
119
+ method: entry.method,
120
+ avgSavings: entry.avgSavings,
121
+ hits: entry.hits,
122
+ };
123
+ if (entry.persistedId) {
124
+ const current = store.getMemory(entry.persistedId);
125
+ if (current) {
126
+ store.updateMemory(entry.persistedId, { metadata });
127
+ return;
128
+ }
129
+ }
130
+ const memory = store.createMemory({
131
+ sessionId: null,
132
+ content: `Compression skill: ${entry.method} saves ${(entry.avgSavings * 100).toFixed(0)}% on shape ${signature}`,
133
+ type: "skill",
134
+ category: "optimization",
135
+ importance: 0.3,
136
+ metadata,
137
+ });
138
+ entry.persistedId = memory.id;
139
+ } catch (err) {
140
+ logger.warn({ err, signature }, "[skills] Failed to persist skill");
141
+ }
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Compression hints for a given text, derived from validated skills.
147
+ *
148
+ * @param {string} text - Tool result text about to be compressed
149
+ * @returns {{signature: string, skipDedup: boolean, maxLengthFactor: number}}
150
+ */
151
+ function getCompressionHints(text) {
152
+ const signature = shapeSignature(text);
153
+ const skill = lookup(signature);
154
+ const minSavings = skillsConfig().minSavings ?? 0.6;
155
+
156
+ if (!skill || skill.hits < 2) {
157
+ return { signature, skipDedup: false, maxLengthFactor: 1 };
158
+ }
159
+
160
+ return {
161
+ signature,
162
+ // Dedup/delta repeatedly achieved almost nothing — don't burn CPU on it
163
+ skipDedup: skill.method === "passthrough" || skill.avgSavings < 0.05,
164
+ // Proven highly-compressible shape — compress harder
165
+ maxLengthFactor: skill.avgSavings >= minSavings ? 0.8 : 1,
166
+ };
167
+ }
168
+
169
+ /** Reset the in-memory cache (test support). */
170
+ function resetCache() {
171
+ skillMap = null;
172
+ }
173
+
174
+ module.exports = {
175
+ shapeSignature,
176
+ lookup,
177
+ record,
178
+ getCompressionHints,
179
+ resetCache,
180
+ };