kiku-agent 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +69 -12
  2. package/package.json +2 -1
  3. package/server/agent/agentCore.js +319 -46
  4. package/server/agent/appPaths.js +31 -0
  5. package/server/agent/audioPlayer.js +339 -12
  6. package/server/agent/autoContext.js +193 -0
  7. package/server/agent/catalogEntityResolver.js +170 -28
  8. package/server/agent/clarifyingQuestions.js +137 -35
  9. package/server/agent/cli.js +177 -48
  10. package/server/agent/cloudAdapters.js +116 -0
  11. package/server/agent/commandRegistry.js +409 -80
  12. package/server/agent/commands.js +1056 -125
  13. package/server/agent/config.js +1088 -690
  14. package/server/agent/contextCompactor.js +213 -21
  15. package/server/agent/cpuMetrics.js +199 -0
  16. package/server/agent/engineService.js +591 -0
  17. package/server/agent/eval/modules/evaluation/runner.js +4 -14
  18. package/server/agent/eval/modules/evaluation/scoring.js +15 -3
  19. package/server/agent/eval/modules/server/evalApi.js +5 -5
  20. package/server/agent/eval/modules/ui/evalUi.js +7 -4
  21. package/server/agent/eventBus.js +1 -0
  22. package/server/agent/ink/main.jsx +196 -191
  23. package/server/agent/jobManager.js +11 -1
  24. package/server/agent/lanDiscovery.js +721 -23
  25. package/server/agent/llmClient.js +317 -71
  26. package/server/agent/lmStudioWakeup.js +517 -65
  27. package/server/agent/locationService.js +797 -0
  28. package/server/agent/mcpQuizData.js +509 -0
  29. package/server/agent/memory/contextAllocator.js +19 -7
  30. package/server/agent/memory.js +3 -1
  31. package/server/agent/modelCompatibility.js +352 -0
  32. package/server/agent/modelDefaults.js +1 -0
  33. package/server/agent/modelManager.js +46 -2
  34. package/server/agent/newsWatch.js +9 -3
  35. package/server/agent/processManager.js +229 -0
  36. package/server/agent/prompts.js +174 -8
  37. package/server/agent/routingInterceptor.js +9 -1
  38. package/server/agent/sanitizer.js +41 -6
  39. package/server/agent/searchQueryPlanner.js +202 -0
  40. package/server/agent/session.js +59 -4
  41. package/server/agent/tests/agentFollowUps.test.js +128 -2
  42. package/server/agent/tests/agentMusicControl.test.js +245 -0
  43. package/server/agent/tests/apiRouter.test.js +1 -1
  44. package/server/agent/tests/autoContext.test.js +176 -0
  45. package/server/agent/tests/bilibiliEmbed.test.js +45 -0
  46. package/server/agent/tests/chatHardening.test.js +121 -0
  47. package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
  48. package/server/agent/tests/clarifyingQuestions.test.js +33 -0
  49. package/server/agent/tests/cliAutomation.test.js +38 -1
  50. package/server/agent/tests/cloudProviders.test.js +91 -3
  51. package/server/agent/tests/commands.test.js +92 -3
  52. package/server/agent/tests/contextIntelligence.test.js +50 -3
  53. package/server/agent/tests/cpuMetrics.test.js +101 -0
  54. package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
  55. package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
  56. package/server/agent/tests/engineService.test.js +436 -0
  57. package/server/agent/tests/engineUnload.test.js +94 -0
  58. package/server/agent/tests/envCommand.test.js +69 -0
  59. package/server/agent/tests/evalMetrics.test.js +8 -0
  60. package/server/agent/tests/fetchApi.test.js +99 -0
  61. package/server/agent/tests/hybridWebSearch.test.js +2 -0
  62. package/server/agent/tests/lanDiscovery.test.js +5 -0
  63. package/server/agent/tests/lanInterfaces.test.js +510 -0
  64. package/server/agent/tests/lmStudioWakeup.test.js +278 -0
  65. package/server/agent/tests/locationTool.test.js +278 -0
  66. package/server/agent/tests/memoryRoute.test.js +173 -0
  67. package/server/agent/tests/memoryUiNormalization.test.js +75 -0
  68. package/server/agent/tests/modelCompatibility.test.js +148 -0
  69. package/server/agent/tests/modelManager.test.js +32 -0
  70. package/server/agent/tests/musicControlUiBridge.test.js +113 -0
  71. package/server/agent/tests/nextGenArchitecture.test.js +4 -2
  72. package/server/agent/tests/openDomainPrompt.test.js +197 -0
  73. package/server/agent/tests/openDomainSearch.test.js +121 -0
  74. package/server/agent/tests/processManager.test.js +144 -0
  75. package/server/agent/tests/providerPersistence.test.js +10 -5
  76. package/server/agent/tests/quickActionsAndModals.test.js +180 -0
  77. package/server/agent/tests/searchQueryPlanner.test.js +108 -0
  78. package/server/agent/tests/securityAuditRemediation.test.js +16 -9
  79. package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
  80. package/server/agent/tests/securityHardening.test.js +4 -4
  81. package/server/agent/tests/sessionAndCopy.test.js +39 -4
  82. package/server/agent/tests/sidebarControls.test.js +34 -0
  83. package/server/agent/tests/statefulMemory.test.js +8 -1
  84. package/server/agent/tests/textProtocolLoop.test.js +71 -0
  85. package/server/agent/tests/textToolParsing.test.js +48 -0
  86. package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
  87. package/server/agent/tests/unifiedEndpoints.test.js +13 -6
  88. package/server/agent/tests/v110Features.test.js +1 -1
  89. package/server/agent/tests/vramEstimator.test.js +75 -0
  90. package/server/agent/tests/webChatDocs.test.js +199 -0
  91. package/server/agent/tests/webChatInferenceControl.test.js +298 -0
  92. package/server/agent/tests/webChatInspection.test.js +256 -5
  93. package/server/agent/tests/webChatMcp.test.js +94 -0
  94. package/server/agent/tests/webExtract.test.js +23 -0
  95. package/server/agent/tests/webSources.test.js +46 -12
  96. package/server/agent/textToolProtocol.js +173 -46
  97. package/server/agent/toolRouter.js +318 -44
  98. package/server/agent/tools.js +877 -96
  99. package/server/agent/ui.js +39 -34
  100. package/server/agent/vramService.js +35 -54
  101. package/server/agent/webSearch.js +441 -162
  102. package/server/agent/websearch/orchestrator.js +15 -5
  103. package/server/agent/websearch/resilience/cache.js +9 -2
  104. package/server/agent/websearch/security/ssrf.js +41 -10
  105. package/server/agent/youtubeAuth.js +5 -20
  106. package/server/apiRouter.js +5558 -4422
  107. package/server/mcp/data/discography.js +2 -2
  108. package/server/mcp/examples/antigravity.example.json +17 -0
  109. package/server/mcp/handler.js +8 -4
  110. package/server/mcp/security.js +220 -21
  111. package/server/mcp/server.js +2 -1
  112. package/server/mcp/tests/connection.test.js +14 -8
  113. package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
  114. package/server/mcp/tools.js +7 -1
  115. package/server/supabase_migration_prefix.sql +102 -0
  116. package/server/supabase_schema.sql +18 -18
  117. package/server/web-chat/core/connection.js +179 -218
  118. package/server/web-chat/core/conversationContext.js +9 -2
  119. package/server/web-chat/core/directCatalogEngine.js +52 -28
  120. package/server/web-chat/core/dramaKnowledge.js +33 -22
  121. package/server/web-chat/core/followUpEngine.js +735 -81
  122. package/server/web-chat/core/futureEventGuard.js +5 -1
  123. package/server/web-chat/core/musicControlAction.js +70 -0
  124. package/server/web-chat/core/security.js +32 -21
  125. package/server/web-chat/core/toolInspector.js +6 -6
  126. package/server/web-chat/core/urlGuard.js +45 -0
  127. package/server/web-chat/router.js +204 -157
  128. package/server/web-chat/routes/chatRoute.js +885 -488
  129. package/server/web-chat/routes/engineRoute.js +270 -0
  130. package/server/web-chat/routes/exportRoute.js +1 -1
  131. package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
  132. package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
  133. package/server/web-chat/routes/mcpRoute.js +386 -0
  134. package/server/web-chat/routes/memoryRoute.js +253 -0
  135. package/server/web-chat/routes/providerRoute.js +49 -16
  136. package/server/web-chat/routes/staticRoute.js +306 -17
  137. package/server/web-chat/routes/statusRoute.js +194 -73
  138. package/server/web-chat/routes/suggestRoute.js +11 -3
  139. package/server/web-chat/routes/verifyRoute.js +26 -4
  140. package/server/web-chat/server.js +7 -6
  141. package/server/web-chat/ui/docsBuilder.js +3558 -0
  142. package/server/web-chat/ui/htmlBuilder.js +12 -0
  143. package/server/web-chat/ui/scripts/client.js +3401 -1302
  144. package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
  145. package/server/web-chat/ui/settingsBuilder.js +7870 -0
  146. package/server/web-chat/ui/styles/lan-card.css +165 -0
  147. package/server/web-chat/ui/styles/main.css +4289 -447
  148. package/server/web-chat/ui/sw.js +64 -0
  149. package/server/web-chat/ui/templates/body.html +407 -281
  150. package/src/data/awardsData.js +146 -146
  151. package/src/data/musicVideoData.js +6 -4
  152. package/src/lib/vram/estimator.js +195 -19
@@ -1,4 +1,179 @@
1
- import { dynamicToolRouter } from './toolRouter.js';
1
+ import { dynamicToolRouter } from './toolRouter.js';
2
+
3
+ const graphemeSegmenter = typeof Intl?.Segmenter === 'function'
4
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
5
+ : null;
6
+
7
+ export function sanitizePromptString(input = '') {
8
+ const value = String(input);
9
+ let clean = '';
10
+ for (let i = 0; i < value.length; i++) {
11
+ const code = value.charCodeAt(i);
12
+ if (code >= 0xD800 && code <= 0xDBFF) {
13
+ const next = value.charCodeAt(i + 1);
14
+ if (next >= 0xDC00 && next <= 0xDFFF) {
15
+ clean += value[i] + value[i + 1];
16
+ i++;
17
+ }
18
+ continue;
19
+ }
20
+ if (code >= 0xDC00 && code <= 0xDFFF) continue;
21
+ clean += value[i];
22
+ }
23
+ return clean.normalize('NFC');
24
+ }
25
+
26
+ export function truncatePromptString(input, maxLength, suffix = '') {
27
+ const clean = sanitizePromptString(input);
28
+ const limit = Math.max(0, Math.floor(Number(maxLength) || 0));
29
+ if (clean.length <= limit) return clean;
30
+ if (limit === 0) return '';
31
+
32
+ let output = '';
33
+ const segments = graphemeSegmenter
34
+ ? Array.from(graphemeSegmenter.segment(clean), part => part.segment)
35
+ : Array.from(clean);
36
+ for (const segment of segments) {
37
+ if (output.length + segment.length > limit) break;
38
+ output += segment;
39
+ }
40
+ return output + suffix;
41
+ }
42
+
43
+ export function sanitizePromptValue(value) {
44
+ if (typeof value === 'string') return sanitizePromptString(value);
45
+ if (Array.isArray(value)) return value.map(sanitizePromptValue);
46
+ if (!value || typeof value !== 'object') return value;
47
+ const clean = {};
48
+ for (const [key, item] of Object.entries(value)) {
49
+ clean[sanitizePromptString(key)] = sanitizePromptValue(item);
50
+ }
51
+ return clean;
52
+ }
53
+
54
+ function prependUserContext(content, preamble) {
55
+ const prefix = `[Previous Session Context Summary: ${sanitizePromptString(preamble)}]\n\n`;
56
+ if (typeof content === 'string') return prefix + sanitizePromptString(content);
57
+ if (Array.isArray(content)) {
58
+ return [{ type: 'text', text: prefix.trim() }, ...sanitizePromptValue(content)];
59
+ }
60
+ return prefix + sanitizePromptString(content == null ? '' : JSON.stringify(content));
61
+ }
62
+
63
+ export function alignChatMessageRoles(messages = []) {
64
+ if (!Array.isArray(messages)) return [];
65
+ const cleanMessages = sanitizePromptValue(messages).filter(message => message && typeof message === 'object');
66
+ const firstSystemIndex = cleanMessages[0]?.role === 'system' ? 0 : -1;
67
+ const leadingSystem = firstSystemIndex === 0 ? cleanMessages[0] : null;
68
+ const contextualSystemMessages = cleanMessages
69
+ .filter((message, index) => message.role === 'system' && index !== firstSystemIndex)
70
+ .map(message => message.content)
71
+ .filter(Boolean);
72
+ const aligned = cleanMessages.filter(message => message.role !== 'system');
73
+
74
+ if (contextualSystemMessages.length > 0) {
75
+ let targetIndex = -1;
76
+ for (let i = aligned.length - 1; i >= 0; i--) {
77
+ if (aligned[i].role === 'user') {
78
+ targetIndex = i;
79
+ break;
80
+ }
81
+ }
82
+ const preamble = contextualSystemMessages.join('\n\n');
83
+ if (targetIndex >= 0) {
84
+ aligned[targetIndex] = {
85
+ ...aligned[targetIndex],
86
+ content: prependUserContext(aligned[targetIndex].content, preamble),
87
+ };
88
+ } else {
89
+ aligned.unshift({ role: 'user', content: prependUserContext('', preamble).trim() });
90
+ }
91
+ }
92
+
93
+ return leadingSystem ? [leadingSystem, ...aligned] : aligned;
94
+ }
95
+
96
+ function messageTokens(messages) {
97
+ return estimateTokenCount(messages);
98
+ }
99
+
100
+ function omissionMessage(message) {
101
+ return {
102
+ ...message,
103
+ content: JSON.stringify({
104
+ status: 'omitted',
105
+ authoritative: false,
106
+ message: 'Older tool output omitted by the context budget guard.',
107
+ }),
108
+ _omittedForContext: true,
109
+ };
110
+ }
111
+
112
+ export function prepareChatMessagesForDispatch(messages = [], options = {}) {
113
+ const aligned = alignChatMessageRoles(messages);
114
+ const sanitizedTools = sanitizePromptValue(Array.isArray(options.tools) ? options.tools : []);
115
+ const maxContextTokens = Math.max(1024, Math.floor(Number(options.maxContextTokens) || 32768));
116
+ const generationReserve = Math.max(0, Math.floor(Number(options.generationReserve) || 0));
117
+ const systemTokens = aligned[0]?.role === 'system' ? messageTokens([aligned[0]]) : 0;
118
+ const toolDefinitionTokens = sanitizedTools.length > 0
119
+ ? estimateTextTokens(JSON.stringify(sanitizedTools))
120
+ : 0;
121
+ const allocation = Math.max(256, maxContextTokens - systemTokens - generationReserve - toolDefinitionTokens);
122
+ const threshold = Math.max(128, Math.floor(allocation * Math.min(1, Math.max(0.5, Number(options.pruneAt) || 0.85))));
123
+ const prepared = aligned.map(message => ({ ...message }));
124
+ const prefixCount = prepared[0]?.role === 'system' ? 1 : 0;
125
+ const historyUsage = () => messageTokens(prepared.slice(prefixCount));
126
+
127
+ let pruned = false;
128
+ while (historyUsage() > threshold) {
129
+ const userIndexes = [];
130
+ for (let i = prefixCount; i < prepared.length; i++) {
131
+ if (prepared[i].role === 'user') userIndexes.push(i);
132
+ }
133
+ if (userIndexes.length <= 1) break;
134
+ const nextTurn = userIndexes[1];
135
+ prepared.splice(prefixCount, nextTurn - prefixCount);
136
+ pruned = true;
137
+ }
138
+
139
+ for (let i = prefixCount; i < prepared.length && historyUsage() > threshold; i++) {
140
+ if (prepared[i].role !== 'tool' || prepared[i]._omittedForContext) continue;
141
+ prepared[i] = omissionMessage(prepared[i]);
142
+ pruned = true;
143
+ }
144
+
145
+ if (historyUsage() > threshold) {
146
+ for (let i = prefixCount; i < prepared.length && historyUsage() > threshold; i++) {
147
+ const message = prepared[i];
148
+ if (typeof message.content !== 'string' || message.content.length <= 256) continue;
149
+ const currentTokens = estimateTextTokens(message.content);
150
+ const overflow = historyUsage() - threshold;
151
+ const targetTokens = Math.max(64, currentTokens - overflow);
152
+ const targetChars = Math.max(256, Math.floor(message.content.length * (targetTokens / Math.max(1, currentTokens))));
153
+ prepared[i] = {
154
+ ...message,
155
+ content: truncatePromptString(message.content, targetChars, '…'),
156
+ _truncatedForContext: true,
157
+ };
158
+ pruned = true;
159
+ }
160
+ }
161
+
162
+ return {
163
+ messages: sanitizePromptValue(prepared),
164
+ tools: sanitizedTools,
165
+ budget: {
166
+ maxContextTokens,
167
+ systemTokens,
168
+ generationReserve,
169
+ toolDefinitionTokens,
170
+ allocation,
171
+ threshold,
172
+ promptTokens: messageTokens(prepared),
173
+ pruned,
174
+ },
175
+ };
176
+ }
2
177
 
3
178
  export function estimateTextTokens(text) {
4
179
  if (!text) return 0;
@@ -56,10 +231,10 @@ export class ContextCompactor {
56
231
  const points = [];
57
232
  for (const msg of messages) {
58
233
  if (msg.role === 'user' && typeof msg.content === 'string' && msg.content.trim()) {
59
- const text = msg.content.slice(0, 100).replace(/\n+/g, ' ').trim();
234
+ const text = truncatePromptString(msg.content, 100).replace(/\n+/g, ' ').trim();
60
235
  points.push(`User asked: "${text}"`);
61
236
  } else if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.trim()) {
62
- const text = msg.content.slice(0, 120).replace(/\n+/g, ' ').trim();
237
+ const text = truncatePromptString(msg.content, 120).replace(/\n+/g, ' ').trim();
63
238
  points.push(`Assistant covered: "${text}"`);
64
239
  } else if (msg.role === 'tool') {
65
240
  const toolName = msg.name || 'tool';
@@ -86,13 +261,15 @@ export class ContextCompactor {
86
261
  };
87
262
  }
88
263
 
89
- _assemble(systemMessage, olderMessages, recentMessages, summaryText) {
90
- const text = summaryText ?? this.summarizeHistorySync(olderMessages);
91
- const compacted = [];
92
- if (systemMessage) compacted.push(systemMessage);
93
- if (text) compacted.push({ role: 'system', content: `[CONVERSATION CONTEXT SUMMARY OF EARLIER MESSAGES: ${text}]` });
94
- compacted.push(...recentMessages);
95
- return compacted;
264
+ _assemble(systemMessage, olderMessages, recentMessages, summaryText) {
265
+ const text = summaryText ?? this.summarizeHistorySync(olderMessages);
266
+ const compacted = systemMessage ? [systemMessage, ...recentMessages] : [...recentMessages];
267
+ if (!text) return alignChatMessageRoles(compacted);
268
+ const summaryDirective = {
269
+ role: 'system',
270
+ content: text,
271
+ };
272
+ return alignChatMessageRoles([...compacted, summaryDirective]);
96
273
  }
97
274
 
98
275
  _shouldCompact(conversationHistory, options) {
@@ -147,11 +324,6 @@ export function compactToolsForPrompt(query = '', options = {}) {
147
324
 
148
325
 
149
326
  if (/photoshoot|magazine|editorial|port|大片|九月刊|写真|นิตยสาร|แฟชั่นเซ็ต/i.test(rawQuery)) {
150
-
151
- if (!/contract|ambassador|commercial|代言|พรีเซนเตอร์|ทูต/i.test(rawQuery)) {
152
- routed = routed.filter(t => (t.function?.name || t.schema?.name || t.name) !== 'get_brand_endorsements');
153
- }
154
-
155
327
  const hasGallery = routed.some(t => (t.function?.name || t.schema?.name || t.name) === 'get_gallery_photos');
156
328
  if (!hasGallery) {
157
329
  const galleryTool = router.allTools?.find(t => (t.function?.name || t.schema?.name) === 'get_gallery_photos');
@@ -159,6 +331,9 @@ export function compactToolsForPrompt(query = '', options = {}) {
159
331
  routed.unshift(galleryTool);
160
332
  }
161
333
  }
334
+ if (options.schemaStrategy === 'domain_grouped' || !/แบรนด์|พรีเซนเตอร์|ambassador|endorsement/i.test(rawQuery)) {
335
+ routed = routed.filter(t => (t.function?.name || t.schema?.name || t.name) !== 'get_brand_endorsements');
336
+ }
162
337
  }
163
338
 
164
339
  return routed.map(t => {
@@ -193,6 +368,17 @@ const ESSENTIAL_SONG_KEYS = new Set([
193
368
  const ESSENTIAL_ROLE_KEYS = new Set([
194
369
  'workTitle', 'year', 'role', 'roleCn'
195
370
  ]);
371
+ const ESSENTIAL_WEB_KEYS = new Set([
372
+ 'title', 'name', 'alt', 'url', 'link', 'snippet', 'description', 'content', 'source', 'date', 'author',
373
+ 'imageUrl', 'thumbUrl', 'sourcePageUrl', 'width', 'height', 'relevanceScore',
374
+ ]);
375
+ const LINK_VALUE_KEYS = /(url|link|href|image|thumb|pic|cover)$/i;
376
+
377
+ function isLinkValue(key, value) {
378
+ return typeof value === 'string'
379
+ && LINK_VALUE_KEYS.test(String(key || ''))
380
+ && /^https?:\/\//i.test(value);
381
+ }
196
382
 
197
383
  function stripSearchUrls(obj) {
198
384
  if (Array.isArray(obj)) {
@@ -239,6 +425,12 @@ export function compactToolResultForContext(result, options = {}) {
239
425
  if (Array.isArray(stripped.videos)) stripped.videos = projectList(stripped.videos, ESSENTIAL_VIDEO_KEYS);
240
426
  if (Array.isArray(stripped.musicVideos)) stripped.musicVideos = projectList(stripped.musicVideos, ESSENTIAL_VIDEO_KEYS);
241
427
  if (Array.isArray(stripped.roles)) stripped.roles = projectList(stripped.roles, ESSENTIAL_ROLE_KEYS);
428
+ if (Array.isArray(stripped.results)) stripped.results = projectList(stripped.results, ESSENTIAL_WEB_KEYS);
429
+
430
+ if (stripped.scrapedPages) delete stripped.scrapedPages;
431
+ if (typeof stripped.combinedContent === 'string' && stripped.combinedContent.length > 2000) {
432
+ stripped.combinedContent = truncatePromptString(stripped.combinedContent, 2000, '...');
433
+ }
242
434
 
243
435
 
244
436
  let serialized = JSON.stringify(stripped);
@@ -255,15 +447,15 @@ export function compactToolResultForContext(result, options = {}) {
255
447
  if (node && typeof node === 'object') {
256
448
  const out = {};
257
449
  for (const [k, v] of Object.entries(node)) {
258
- out[k] = truncateNode(v);
450
+ out[k] = isLinkValue(k, v) ? v : truncateNode(v);
259
451
  }
260
452
  return out;
261
453
  }
262
454
  if (typeof node === 'string' && node.length > 240) {
263
- return `${node.slice(0, 240)}…`;
455
+ return truncatePromptString(node, 240, '…');
264
456
  }
265
457
  return node;
266
- };
458
+ };
267
459
 
268
460
  stripped = truncateNode(stripped);
269
461
  serialized = JSON.stringify(stripped);
@@ -275,12 +467,12 @@ export function compactToolResultForContext(result, options = {}) {
275
467
  if (node && typeof node === 'object') {
276
468
  const out = {};
277
469
  for (const [k, v] of Object.entries(node)) {
278
- out[k] = aggressive(v);
470
+ out[k] = isLinkValue(k, v) ? v : aggressive(v);
279
471
  }
280
472
  return out;
281
473
  }
282
- if (typeof node === 'string' && node.length > 120) {
283
- return `${node.slice(0, 120)}…`;
474
+ if (typeof node === 'string' && node.length > 120) {
475
+ return truncatePromptString(node, 120, '…');
284
476
  }
285
477
  return node;
286
478
  };
@@ -0,0 +1,199 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+
4
+ function runProcessCapture(cmd, args, timeoutMs = 2500) {
5
+ return new Promise((resolve) => {
6
+ let stdout = '';
7
+ let stderr = '';
8
+ let settled = false;
9
+
10
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
11
+
12
+ const finish = (result) => {
13
+ if (settled) return;
14
+ settled = true;
15
+ clearTimeout(timer);
16
+ resolve(result);
17
+ };
18
+
19
+ const timer = setTimeout(() => {
20
+ try { child.kill('SIGKILL'); } catch {}
21
+ finish({ ok: false, stdout: '', stderr: 'timeout' });
22
+ }, timeoutMs);
23
+
24
+ child.stdout?.on('data', d => { stdout += d.toString(); });
25
+ child.stderr?.on('data', d => { stderr += d.toString(); });
26
+
27
+ child.on('error', (err) => finish({ ok: false, stdout: '', stderr: err.message }));
28
+ child.on('close', (code) => finish({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() }));
29
+ });
30
+ }
31
+
32
+ function detectCpuVendor(model = '') {
33
+ const m = String(model).toLowerCase();
34
+ if (m.includes('intel')) return 'Intel';
35
+ if (m.includes('amd') || m.includes('ryzen') || m.includes('epyc')) return 'AMD';
36
+ if (m.includes('apple') || m.includes('m1') || m.includes('m2') || m.includes('m3') || m.includes('m4')) return 'Apple';
37
+ if (m.includes('arm') || m.includes('cortex') || m.includes('snapdragon')) return 'ARM';
38
+ return 'Generic';
39
+ }
40
+
41
+ let cachedCpuMetrics = null;
42
+ const CPU_METRICS_TTL_MS = 60_000;
43
+
44
+ export function clearCpuMetricsCache() {
45
+ cachedCpuMetrics = null;
46
+ }
47
+
48
+ export async function getCpuMetrics(force = false) {
49
+ if (!force && cachedCpuMetrics && (Date.now() - cachedCpuMetrics.timestamp < CPU_METRICS_TTL_MS)) {
50
+ return cachedCpuMetrics.data;
51
+ }
52
+
53
+ const cpus = os.cpus() || [];
54
+ const rawModel = cpus[0]?.model?.trim() || 'Generic CPU';
55
+ const logicalCores = Math.max(1, cpus.length || 1);
56
+ let physicalCores = logicalCores;
57
+ let maxClockMhz = cpus[0]?.speed ? Math.round(cpus[0].speed) : null;
58
+ let socket = null;
59
+ let source = 'os.cpus';
60
+
61
+ if (process.platform === 'win32') {
62
+ try {
63
+ const psScript = 'Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, SocketDesignation | ConvertTo-Json -Compress';
64
+ const res = await runProcessCapture('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], 2500);
65
+ if (res.ok && res.stdout) {
66
+ const parsed = JSON.parse(res.stdout);
67
+ const item = Array.isArray(parsed) ? parsed[0] : parsed;
68
+ if (item) {
69
+ if (item.NumberOfCores && Number.isFinite(Number(item.NumberOfCores))) {
70
+ physicalCores = Math.max(1, Number(item.NumberOfCores));
71
+ }
72
+ if (item.MaxClockSpeed && Number.isFinite(Number(item.MaxClockSpeed))) {
73
+ maxClockMhz = Math.round(Number(item.MaxClockSpeed));
74
+ }
75
+ if (item.SocketDesignation) {
76
+ socket = String(item.SocketDesignation).trim();
77
+ }
78
+ source = 'Win32_Processor';
79
+ }
80
+ }
81
+ } catch {}
82
+ } else if (process.platform === 'darwin') {
83
+ try {
84
+ const [physRes, logRes] = await Promise.all([
85
+ runProcessCapture('sysctl', ['-n', 'hw.physicalcpu'], 1500),
86
+ runProcessCapture('sysctl', ['-n', 'hw.logicalcpu'], 1500),
87
+ ]);
88
+ if (physRes.ok && physRes.stdout) {
89
+ const n = parseInt(physRes.stdout, 10);
90
+ if (Number.isFinite(n) && n > 0) physicalCores = n;
91
+ source = 'sysctl';
92
+ }
93
+ } catch {}
94
+ } else if (process.platform === 'linux') {
95
+ try {
96
+ const res = await runProcessCapture('lscpu', ['-J'], 1500);
97
+ if (res.ok && res.stdout) {
98
+ const json = JSON.parse(res.stdout);
99
+ const list = json?.lscpu || [];
100
+ const findField = (name) => list.find(x => x.field?.toLowerCase()?.includes(name))?.data;
101
+ const coresPerSocket = parseInt(findField('core(s) per socket'), 10);
102
+ const sockets = parseInt(findField('socket(s)'), 10) || 1;
103
+ if (Number.isFinite(coresPerSocket) && coresPerSocket > 0) {
104
+ physicalCores = coresPerSocket * sockets;
105
+ source = 'lscpu';
106
+ }
107
+ }
108
+ } catch {}
109
+ }
110
+
111
+
112
+ if (physicalCores === logicalCores && logicalCores >= 4) {
113
+ const vendor = detectCpuVendor(rawModel);
114
+ if (vendor === 'Intel' || vendor === 'AMD') {
115
+
116
+ physicalCores = Math.max(1, Math.floor(logicalCores / 2));
117
+ source = 'heuristic-x86-smt';
118
+ }
119
+ }
120
+
121
+ const hyperThreading = logicalCores > physicalCores;
122
+ const smtMultiplier = physicalCores > 0 ? Number((logicalCores / physicalCores).toFixed(2)) : 1;
123
+ const vendor = detectCpuVendor(rawModel);
124
+
125
+
126
+ const cleanModel = rawModel
127
+ .replace(/\(R\)/gi, '')
128
+ .replace(/\(TM\)/gi, '')
129
+ .replace(/\s+/g, ' ')
130
+ .trim();
131
+
132
+
133
+ const recommendedComputeThreads = physicalCores;
134
+ const recommendedBatchThreads = logicalCores;
135
+
136
+ const data = {
137
+ model: cleanModel,
138
+ rawModel,
139
+ vendor,
140
+ physicalCores,
141
+ logicalCores,
142
+ hyperThreading,
143
+ smtMultiplier,
144
+ maxClockMhz,
145
+ socket,
146
+ source,
147
+ recommendedComputeThreads,
148
+ recommendedBatchThreads,
149
+ statusText: hyperThreading
150
+ ? `Active (${smtMultiplier}x SMT • ${physicalCores}C/${logicalCores}T)`
151
+ : `Single-Threaded (${physicalCores}C/${logicalCores}T)`,
152
+ };
153
+
154
+ cachedCpuMetrics = { data, timestamp: Date.now() };
155
+ return data;
156
+ }
157
+
158
+ export function resolveOptimalThreadBudget({ mode = 'auto', requested = null, cpuInfo = null } = {}) {
159
+ const cpu = cpuInfo || { physicalCores: 6, logicalCores: 12, hyperThreading: true };
160
+ const req = Number(requested);
161
+ const hasExplicit = Number.isFinite(req) && req >= 1 && req <= 256;
162
+
163
+ switch (String(mode).toLowerCase()) {
164
+ case 'physical':
165
+ return {
166
+ threads: cpu.physicalCores,
167
+ mode: 'physical',
168
+ explanation: `Locked to ${cpu.physicalCores} physical cores (avoids SMT cache thrashing for optimal LLM generation speed)`,
169
+ };
170
+ case 'smt':
171
+ case 'all':
172
+ case 'hyperthreading':
173
+ return {
174
+ threads: cpu.logicalCores,
175
+ mode: 'smt',
176
+ explanation: `Utilizing all ${cpu.logicalCores} logical threads (maximum parallel prediction throughput)`,
177
+ };
178
+ case 'custom':
179
+ return {
180
+ threads: hasExplicit ? Math.floor(req) : cpu.physicalCores,
181
+ mode: 'custom',
182
+ explanation: `Custom thread count: ${hasExplicit ? Math.floor(req) : cpu.physicalCores}`,
183
+ };
184
+ case 'auto':
185
+ default:
186
+ if (hasExplicit) {
187
+ return {
188
+ threads: Math.min(Math.floor(req), cpu.logicalCores),
189
+ mode: 'custom',
190
+ explanation: `User requested ${Math.floor(req)} threads (within ${cpu.logicalCores} thread hardware ceiling)`,
191
+ };
192
+ }
193
+ return {
194
+ threads: cpu.physicalCores,
195
+ mode: 'auto',
196
+ explanation: `Auto-tuned to ${cpu.physicalCores} physical cores for optimal single-stream latency`,
197
+ };
198
+ }
199
+ }