@mazees/mark 5.0.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 (227) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +209 -0
  3. package/bin/mark.js +27 -0
  4. package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
  5. package/out/renderer/assets/index-G2fADWO-.js +4768 -0
  6. package/out/renderer/assets/index-f3mVse3c.css +1 -0
  7. package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
  8. package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  9. package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  10. package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
  11. package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
  12. package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
  13. package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
  14. package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
  15. package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
  16. package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  17. package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  18. package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  19. package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  20. package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  21. package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  22. package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  23. package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  24. package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  25. package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  26. package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
  27. package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
  28. package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
  29. package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
  30. package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
  31. package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
  32. package/out/renderer/favicon.ico +0 -0
  33. package/out/renderer/icon-256.png +0 -0
  34. package/out/renderer/icon.svg +1 -0
  35. package/out/renderer/index.html +25 -0
  36. package/out/renderer/manifest.json +25 -0
  37. package/out/renderer/robot-simulator.html +1640 -0
  38. package/package.json +106 -0
  39. package/src/cli/index.js +332 -0
  40. package/src/cli/monitor.js +413 -0
  41. package/src/cli/theme.js +347 -0
  42. package/src/launcher/MarkTray.cs +227 -0
  43. package/src/main/browser-agent.js +1132 -0
  44. package/src/main/git-service.js +90 -0
  45. package/src/main/google/google-calendar.js +69 -0
  46. package/src/main/google/google-drive.js +263 -0
  47. package/src/main/google/google-gmail.js +131 -0
  48. package/src/main/google/google-service.js +241 -0
  49. package/src/main/node-tools.js +23 -0
  50. package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
  51. package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
  52. package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
  53. package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
  54. package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
  55. package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
  56. package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
  57. package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
  58. package/src/main/plugins/plugin-loader.js +306 -0
  59. package/src/main/skills/skill-manager.js +265 -0
  60. package/src/main/syntax-validator.js +455 -0
  61. package/src/main/task-daemon.js +117 -0
  62. package/src/main/telegram/telegram-service.js +835 -0
  63. package/src/main/tools/browser-tools.js +516 -0
  64. package/src/main/tools/file-tools.js +1126 -0
  65. package/src/main/tools/git-tools.js +79 -0
  66. package/src/main/tools/google-tools.js +566 -0
  67. package/src/main/tools/index.js +36 -0
  68. package/src/main/tools/media-tools.js +42 -0
  69. package/src/main/tools/system-tools.js +295 -0
  70. package/src/main/tools/task-tools.js +73 -0
  71. package/src/main/tools/telegram-tools.js +101 -0
  72. package/src/renderer/index.html +24 -0
  73. package/src/renderer/public/favicon.ico +0 -0
  74. package/src/renderer/public/icon-256.png +0 -0
  75. package/src/renderer/public/icon.svg +1 -0
  76. package/src/renderer/public/manifest.json +25 -0
  77. package/src/renderer/public/robot-simulator.html +1640 -0
  78. package/src/renderer/src/App.jsx +304 -0
  79. package/src/renderer/src/api/ai/awareness.js +294 -0
  80. package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
  81. package/src/renderer/src/api/ai/contextCompactor.js +129 -0
  82. package/src/renderer/src/api/ai/contextManager.js +1130 -0
  83. package/src/renderer/src/api/ai/core.js +167 -0
  84. package/src/renderer/src/api/ai/index.js +4 -0
  85. package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
  86. package/src/renderer/src/api/ai/persona.js +115 -0
  87. package/src/renderer/src/api/ai/planning.js +317 -0
  88. package/src/renderer/src/api/ai/relationship.js +177 -0
  89. package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
  90. package/src/renderer/src/api/ai/tools.js +167 -0
  91. package/src/renderer/src/api/ai/utils.js +359 -0
  92. package/src/renderer/src/api/db.js +1013 -0
  93. package/src/renderer/src/api/oramaStore.js +592 -0
  94. package/src/renderer/src/api/ragPipeline.js +87 -0
  95. package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
  96. package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
  97. package/src/renderer/src/api/subagent/subagentStore.js +173 -0
  98. package/src/renderer/src/api/taskExecutor.js +18 -0
  99. package/src/renderer/src/api/taskStore.js +315 -0
  100. package/src/renderer/src/api/tools/core-tools.js +707 -0
  101. package/src/renderer/src/api/tools/index.js +151 -0
  102. package/src/renderer/src/api/turnPairMigrator.js +200 -0
  103. package/src/renderer/src/api/vectorMemory.js +136 -0
  104. package/src/renderer/src/api/wakeWord.js +101 -0
  105. package/src/renderer/src/api/web-bridge.js +885 -0
  106. package/src/renderer/src/api/webSpeech.js +178 -0
  107. package/src/renderer/src/api/workspaceRag.js +76 -0
  108. package/src/renderer/src/assets/electron.svg +10 -0
  109. package/src/renderer/src/assets/icon.svg +1 -0
  110. package/src/renderer/src/assets/main.css +589 -0
  111. package/src/renderer/src/assets/music-cover.png +0 -0
  112. package/src/renderer/src/assets/wavy-lines.svg +25 -0
  113. package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
  114. package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
  115. package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
  116. package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
  117. package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
  118. package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
  119. package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
  120. package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
  121. package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
  122. package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
  123. package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
  124. package/src/renderer/src/components/Chat/index.js +11 -0
  125. package/src/renderer/src/components/ChatList.jsx +434 -0
  126. package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
  127. package/src/renderer/src/components/Versions.jsx +15 -0
  128. package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
  129. package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
  130. package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
  131. package/src/renderer/src/components/config/CameraSection.jsx +100 -0
  132. package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
  133. package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
  134. package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
  135. package/src/renderer/src/components/config/index.js +5 -0
  136. package/src/renderer/src/components/core/Avatar.jsx +2197 -0
  137. package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
  138. package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
  139. package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
  140. package/src/renderer/src/components/core/HoloCard.jsx +64 -0
  141. package/src/renderer/src/components/core/InputBar.jsx +1346 -0
  142. package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
  143. package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
  144. package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
  145. package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
  146. package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
  147. package/src/renderer/src/components/core/native-skills.js +43 -0
  148. package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
  149. package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
  150. package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
  151. package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
  152. package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
  153. package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
  154. package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
  155. package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
  156. package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
  157. package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
  158. package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
  159. package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
  160. package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
  161. package/src/renderer/src/contexts/ChatContext.jsx +61 -0
  162. package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
  163. package/src/renderer/src/data/faqData.js +202 -0
  164. package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
  165. package/src/renderer/src/hooks/agent/index.js +4 -0
  166. package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
  167. package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
  168. package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
  169. package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
  170. package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
  171. package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
  172. package/src/renderer/src/hooks/useAwareness.js +289 -0
  173. package/src/renderer/src/hooks/useChatArchiver.js +52 -0
  174. package/src/renderer/src/hooks/useConfirm.jsx +68 -0
  175. package/src/renderer/src/hooks/useMarkAgent.js +454 -0
  176. package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
  177. package/src/renderer/src/hooks/useVAD.js +511 -0
  178. package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
  179. package/src/renderer/src/main.jsx +12 -0
  180. package/src/renderer/src/pages/ChatStudio.jsx +922 -0
  181. package/src/renderer/src/pages/Configuration.jsx +360 -0
  182. package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
  183. package/src/renderer/src/pages/Guidebook.jsx +917 -0
  184. package/src/renderer/src/pages/Knowledge.jsx +241 -0
  185. package/src/renderer/src/pages/MarkHome.jsx +491 -0
  186. package/src/renderer/src/pages/NeuralCore.jsx +786 -0
  187. package/src/renderer/src/pages/Plugins.jsx +630 -0
  188. package/src/renderer/src/pages/SkillEditor.jsx +380 -0
  189. package/src/renderer/src/pages/Skills.jsx +219 -0
  190. package/src/renderer/src/pages/Subagents.jsx +342 -0
  191. package/src/renderer/src/pages/TelegramBot.jsx +343 -0
  192. package/src/renderer/src/utils/chatContentParser.js +203 -0
  193. package/src/renderer/src/utils/toolClusters.js +201 -0
  194. package/src/server/bin/sha3_wasm_bg.wasm +0 -0
  195. package/src/server/config-manager.js +57 -0
  196. package/src/server/index.js +195 -0
  197. package/src/server/launcher.js +87 -0
  198. package/src/server/memory/db-store.js +758 -0
  199. package/src/server/memory/orama-store.js +177 -0
  200. package/src/server/memory/vector-engine.js +109 -0
  201. package/src/server/routes/ai.routes.js +280 -0
  202. package/src/server/routes/awareness.routes.js +62 -0
  203. package/src/server/routes/chat.routes.js +61 -0
  204. package/src/server/routes/config.routes.js +41 -0
  205. package/src/server/routes/index.js +35 -0
  206. package/src/server/routes/integrations.routes.js +116 -0
  207. package/src/server/routes/memory.routes.js +256 -0
  208. package/src/server/routes/plugins.routes.js +72 -0
  209. package/src/server/routes/skills.routes.js +139 -0
  210. package/src/server/routes/subagents.routes.js +70 -0
  211. package/src/server/routes/tasks.routes.js +175 -0
  212. package/src/server/routes/workspace.routes.js +127 -0
  213. package/src/server/services/ai/ai-utils.js +161 -0
  214. package/src/server/services/ai/openai-provider.js +757 -0
  215. package/src/server/services/ai/web-provider.js +729 -0
  216. package/src/server/services/ai-bridge.js +46 -0
  217. package/src/server/services/deepseek-web.js +956 -0
  218. package/src/server/services/gemini-web.js +170 -0
  219. package/src/server/services/notification-service.js +91 -0
  220. package/src/server/services/updater.js +125 -0
  221. package/src/server/tools/awareness-tracker.js +292 -0
  222. package/src/server/tools/group-tools.js +1164 -0
  223. package/src/server/tools/media-tools.js +244 -0
  224. package/src/server/tools/pc-agent.js +327 -0
  225. package/src/server/tools/screen-service.js +51 -0
  226. package/src/server/ws-hub.js +168 -0
  227. package/src/shared/parsers/mark-tag-parser.js +275 -0
@@ -0,0 +1,1130 @@
1
+ /**
2
+ * Context Manager Engine (MARK v5.0.0)
3
+ * Mengelola siklus context management per-session:
4
+ * - Batas global konstanta 256.000 tokens (MAX_CONTEXT_TOKENS) via gpt-tokenizer BPE
5
+ * - Penghitungan token presisi in-memory per sesi (system prompt + multimodal + messages)
6
+ * - Tahap 1: Pruning output tool lama di memori (0ms delay, tanpa AI)
7
+ * - Tahap 2: AI Summarization inkremental (Gemini Web -> fallback active provider)
8
+ * - Pembersihan orphan tool pairs
9
+ * - Perakitan payload prompt berformat [ COMPACTED MESSAGE SUMMARY ]
10
+ */
11
+
12
+ import { encode } from 'gpt-tokenizer'
13
+ import { fetchAI } from './core.js'
14
+ import { getSessionCompact, saveSessionCompact, saveSession } from '../db.js'
15
+ import { compactCodeBlocks } from './contextCompactor.js'
16
+
17
+ export const MAX_CONTEXT_TOKENS = 256000 // 256K tokens
18
+ export const MAX_CONTEXT_CHARS = 256000 // Legacy backward-compatibility alias disinkronkan ke 256K
19
+ export const GATEWAY_HYGIENE_THRESHOLD = 0.85 // Jaring pengaman pra-turn (85% kapasitas = ~217.6K tokens)
20
+ export const IN_LOOP_COMPACT_THRESHOLD = 0.5 // Ambang pemicu in-loop ReAct (50% kapasitas = ~128K tokens)
21
+ export const OLD_TOOL_PRUNE_CHAR_LIMIT = 200 // Batas karakter output tool lama untuk dipangkas
22
+ export const CLEARED_TOOL_PLACEHOLDER = '[Old tool output cleared to save context space]'
23
+
24
+ /**
25
+ * Helper menghitung token teks murni via BPE tokenizer (gpt-tokenizer)
26
+ */
27
+ export function countTokens(text) {
28
+ if (!text || typeof text !== 'string') return 0
29
+ try {
30
+ return encode(text).length
31
+ } catch {
32
+ return Math.ceil(text.length / 4)
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Mendapatkan ID unik dari sebuah objek pesan
38
+ */
39
+ export function getMessageId(msg, fallbackIndex = 0) {
40
+ if (!msg) return `msg-${fallbackIndex}`
41
+ return String(msg.id || msg.created_at || msg.timestamp || `msg-${fallbackIndex}`)
42
+ }
43
+
44
+ /**
45
+ * Menghitung panjang karakter representasi sebuah pesan (legacy helper)
46
+ */
47
+ export function calculateMessageChars(msg) {
48
+ if (!msg) return 0
49
+ let total = 0
50
+
51
+ if (typeof msg.content === 'string') {
52
+ if (msg.content.includes('data:image/')) {
53
+ const normalized = msg.content.replace(
54
+ /data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
55
+ ''
56
+ )
57
+ total += normalized.length + 2000
58
+ } else {
59
+ total += msg.content.length
60
+ }
61
+ } else if (Array.isArray(msg.content)) {
62
+ for (const part of msg.content) {
63
+ if (!part) continue
64
+ if (typeof part === 'string') {
65
+ if (part.includes('data:image/')) {
66
+ const normalized = part.replace(/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g, '')
67
+ total += normalized.length + 2000
68
+ } else {
69
+ total += part.length
70
+ }
71
+ } else if (part.type === 'text') {
72
+ const textStr = part.text || ''
73
+ if (textStr.includes('data:image/')) {
74
+ const normalized = textStr.replace(
75
+ /data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
76
+ ''
77
+ )
78
+ total += normalized.length + 2000
79
+ } else {
80
+ total += textStr.length
81
+ }
82
+ } else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
83
+ total += 2000
84
+ } else {
85
+ total += JSON.stringify(part).length
86
+ }
87
+ }
88
+ } else if (msg.content && typeof msg.content === 'object') {
89
+ if (msg.content.type === 'image_url' || msg.content.image_url) {
90
+ total += 2000
91
+ } else {
92
+ total += JSON.stringify(msg.content).length
93
+ }
94
+ }
95
+
96
+ if (typeof msg.reasoning === 'string') total += msg.reasoning.length
97
+ if (typeof msg.thought === 'string') total += msg.thought.length
98
+
99
+ const steps = msg.executionSteps || msg.executedTools
100
+ if (Array.isArray(steps) && steps.length > 0) {
101
+ const sanitizedTools = steps.map((t) => {
102
+ if (!t) return t
103
+ const copy = { ...t }
104
+ delete copy.preview
105
+ return copy
106
+ })
107
+ total += JSON.stringify(sanitizedTools).length
108
+ }
109
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
110
+ total += JSON.stringify(msg.tool_calls).length
111
+ }
112
+
113
+ return total
114
+ }
115
+
116
+ /**
117
+ * Menghitung estimasi token presisi sebuah pesan.
118
+ * Prioritas:
119
+ * 1. Token yang sudah tersimpan di database/objek pesan (msg.tokens)
120
+ * 2. Token completion resmi dari API (msg.usage.completion_tokens)
121
+ * 3. Fallback hitung lokal via BPE tokenizer (gpt-tokenizer) lalu simpan ke msg.tokens
122
+ */
123
+ export function calculateMessageTokens(msg) {
124
+ if (!msg) return 0
125
+
126
+ // 1. Jika token sudah tersimpan di database/objek pesan, langsung gunakan tanpa hitung ulang
127
+ if (typeof msg.tokens === 'number' && msg.tokens > 0) {
128
+ return msg.tokens
129
+ }
130
+
131
+ // 2. Jika pesan memiliki usage completion_tokens resmi dari API
132
+ if (
133
+ msg.usage &&
134
+ typeof msg.usage.completion_tokens === 'number' &&
135
+ msg.usage.completion_tokens > 0
136
+ ) {
137
+ msg.tokens = msg.usage.completion_tokens
138
+ return msg.tokens
139
+ }
140
+
141
+ let total = 0
142
+
143
+ // Konten teks & multimodal (normalisasi bobot gambar Base64)
144
+ if (typeof msg.content === 'string') {
145
+ if (msg.content.includes('data:image/')) {
146
+ const normalized = msg.content.replace(
147
+ /data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
148
+ ''
149
+ )
150
+ total += countTokens(normalized) + 1000
151
+ } else {
152
+ total += countTokens(msg.content)
153
+ }
154
+ } else if (Array.isArray(msg.content)) {
155
+ for (const part of msg.content) {
156
+ if (!part) continue
157
+ if (typeof part === 'string') {
158
+ if (part.includes('data:image/')) {
159
+ const normalized = part.replace(/data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g, '')
160
+ total += countTokens(normalized) + 1000
161
+ } else {
162
+ total += countTokens(part)
163
+ }
164
+ } else if (part.type === 'text') {
165
+ const textStr = part.text || ''
166
+ if (textStr.includes('data:image/')) {
167
+ const normalized = textStr.replace(
168
+ /data:image\/[a-zA-Z0-9+]+;base64,[A-Za-z0-9+/=]+/g,
169
+ ''
170
+ )
171
+ total += countTokens(normalized) + 1000
172
+ } else {
173
+ total += countTokens(textStr)
174
+ }
175
+ } else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
176
+ // Satu gambar pada LLM bernilai ~1000 token
177
+ total += 1000
178
+ } else {
179
+ total += countTokens(JSON.stringify(part))
180
+ }
181
+ }
182
+ } else if (msg.content && typeof msg.content === 'object') {
183
+ if (msg.content.type === 'image_url' || msg.content.image_url) {
184
+ total += 1000
185
+ } else {
186
+ total += countTokens(JSON.stringify(msg.content))
187
+ }
188
+ }
189
+
190
+ // Reasoning / Thought
191
+ if (typeof msg.reasoning === 'string') total += countTokens(msg.reasoning)
192
+ if (typeof msg.thought === 'string') total += countTokens(msg.thought)
193
+
194
+ // Tool calls & executed tools / execution steps (abaikan preview dataUrl base64 karena hanya untuk rendering UI)
195
+ const steps = msg.executionSteps || msg.executedTools
196
+ if (Array.isArray(steps) && steps.length > 0) {
197
+ const sanitizedTools = steps.map((t) => {
198
+ if (!t) return t
199
+ const copy = { ...t }
200
+ delete copy.preview
201
+ return copy
202
+ })
203
+ total += countTokens(JSON.stringify(sanitizedTools))
204
+ }
205
+ if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
206
+ total += countTokens(JSON.stringify(msg.tool_calls))
207
+ }
208
+
209
+ // Simpan hasil hitungan ke objek pesan agar tidak dihitung ulang lagi
210
+ msg.tokens = total
211
+ return total
212
+ }
213
+
214
+ /**
215
+ * Menghitung total token pesan satu sesi + ringkasan aktif + system prompt di memori.
216
+ * Prioritas utama: Menggunakan usage.total_tokens dari DB/API jika ada.
217
+ * Fallback: BPE tokenizer (gpt-tokenizer) dengan caching per-pesan.
218
+ */
219
+ export function calculateSessionTokens(
220
+ messages = [],
221
+ summaryBlock = '',
222
+ lastCompactedMessageId = null,
223
+ systemPrompt = '',
224
+ lastCompactedAt = null
225
+ ) {
226
+ let startIndex = 0
227
+ let isBoundaryFound = false
228
+ if (lastCompactedMessageId) {
229
+ const targetId = String(lastCompactedMessageId)
230
+ for (let i = 0; i < messages.length; i++) {
231
+ const msg = messages[i]
232
+ if (
233
+ getMessageId(msg, i) === targetId ||
234
+ String(msg?.id) === targetId ||
235
+ String(msg?.timestamp) === targetId ||
236
+ String(msg?.created_at) === targetId
237
+ ) {
238
+ startIndex = i + 1
239
+ isBoundaryFound = true
240
+ break
241
+ }
242
+ }
243
+ }
244
+
245
+ // 1. Cek apakah ada pesan asisten terakhir yang memiliki usage.total_tokens resmi dari DB / API
246
+ // Jika ada pemadatan aktif (lastCompactedAt), hanya gunakan usage dari pesan yang dibuat SETELAH pemadatan
247
+ let lastUsageAssistantIdx = -1
248
+ for (let i = messages.length - 1; i >= startIndex; i--) {
249
+ const msg = messages[i]
250
+ if (
251
+ msg &&
252
+ (msg.role === 'ai' || msg.role === 'assistant') &&
253
+ msg.usage &&
254
+ typeof msg.usage.total_tokens === 'number' &&
255
+ msg.usage.total_tokens > 0
256
+ ) {
257
+ if (lastCompactedAt && msg.created_at && Number(msg.created_at) < Number(lastCompactedAt)) {
258
+ continue
259
+ }
260
+ lastUsageAssistantIdx = i
261
+ break
262
+ }
263
+ }
264
+
265
+ if (lastUsageAssistantIdx !== -1) {
266
+ let total = messages[lastUsageAssistantIdx].usage.total_tokens
267
+ for (let i = lastUsageAssistantIdx + 1; i < messages.length; i++) {
268
+ const msg = messages[i]
269
+ if (
270
+ !msg ||
271
+ msg.isThinking ||
272
+ msg.isSearching ||
273
+ msg.isSummarizing ||
274
+ msg.role === 'command'
275
+ ) {
276
+ continue
277
+ }
278
+ total += calculateMessageTokens(msg)
279
+ }
280
+ return total
281
+ }
282
+
283
+ // 2. Fallback: Hitung token via BPE tokenizer (gpt-tokenizer) dengan caching per-pesan
284
+ let total = countTokens(systemPrompt)
285
+
286
+ if (isBoundaryFound && typeof summaryBlock === 'string') {
287
+ total += countTokens(summaryBlock)
288
+ }
289
+
290
+ for (let i = startIndex; i < messages.length; i++) {
291
+ const msg = messages[i]
292
+ if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
293
+ continue
294
+ }
295
+ total += calculateMessageTokens(msg)
296
+ }
297
+
298
+ return total
299
+ }
300
+
301
+ /**
302
+ * Kompatibilitas mundur: Menghitung session tokens (dialihkan langsung ke calculateSessionTokens)
303
+ */
304
+ export function calculateSessionChars(
305
+ messages = [],
306
+ summaryBlock = '',
307
+ lastCompactedMessageId = null,
308
+ systemPrompt = '',
309
+ lastCompactedAt = null
310
+ ) {
311
+ return calculateSessionTokens(
312
+ messages,
313
+ summaryBlock,
314
+ lastCompactedMessageId,
315
+ systemPrompt,
316
+ lastCompactedAt
317
+ )
318
+ }
319
+
320
+ /**
321
+ * Fase 1 Hermes: Prune Old Tool Results (murah, O(n), tanpa panggilan LLM)
322
+ * Mengganti output tool lama (> 200 karakter) di luar tail dengan placeholder:
323
+ * "[Old tool output cleared to save context space]"
324
+ */
325
+ export function pruneOldToolResultsInLoop(messages = [], protectLastN = 6) {
326
+ if (!Array.isArray(messages) || messages.length === 0) return []
327
+
328
+ const cloned = messages.map((m) => {
329
+ if (!m) return m
330
+ return {
331
+ ...m,
332
+ executionSteps: Array.isArray(m.executionSteps)
333
+ ? m.executionSteps.map((t) => ({ ...t }))
334
+ : m.executionSteps,
335
+ executedTools: Array.isArray(m.executedTools)
336
+ ? m.executedTools.map((t) => ({ ...t }))
337
+ : m.executedTools
338
+ }
339
+ })
340
+
341
+ // Tentukan batas ekor (tail) yang dilindungi 100%
342
+ const tailBoundary = Math.max(0, cloned.length - protectLastN)
343
+
344
+ for (let i = 0; i < tailBoundary; i++) {
345
+ const msg = cloned[i]
346
+ if (!msg) continue
347
+
348
+ // 1. Pesan role: 'tool' (Format native OpenAI/ReAct)
349
+ if (msg.role === 'tool') {
350
+ let shouldPrune = false
351
+ let parsed = null
352
+
353
+ if (typeof msg.content === 'string') {
354
+ if (msg.content.length > OLD_TOOL_PRUNE_CHAR_LIMIT) {
355
+ shouldPrune = true
356
+ try {
357
+ parsed = JSON.parse(msg.content)
358
+ } catch {
359
+ parsed = null
360
+ }
361
+ }
362
+ } else if (typeof msg.content === 'object' && msg.content !== null) {
363
+ shouldPrune = true
364
+ parsed = msg.content
365
+ }
366
+
367
+ if (shouldPrune) {
368
+ if (parsed && typeof parsed === 'object') {
369
+ if (parsed.data || parsed.output || parsed.result) {
370
+ parsed.data = CLEARED_TOOL_PLACEHOLDER
371
+ if (parsed.output) parsed.output = CLEARED_TOOL_PLACEHOLDER
372
+ if (parsed.result) parsed.result = CLEARED_TOOL_PLACEHOLDER
373
+ msg.content = JSON.stringify(parsed)
374
+ } else {
375
+ msg.content = CLEARED_TOOL_PLACEHOLDER
376
+ }
377
+ } else {
378
+ msg.content = CLEARED_TOOL_PLACEHOLDER
379
+ }
380
+ }
381
+ }
382
+
383
+ // 2. Pesan dengan executionSteps / executedTools (Format historis chat MARK)
384
+ const targetSteps = msg.executionSteps || msg.executedTools
385
+ if (Array.isArray(targetSteps) && targetSteps.length > 0) {
386
+ const pruned = targetSteps.map((t) => {
387
+ if (!t || t.type === 'narration') return t
388
+ const next = { ...t }
389
+ if (typeof t.fullResult === 'string' && t.fullResult.length > OLD_TOOL_PRUNE_CHAR_LIMIT) {
390
+ next.fullResult = t.resultSummary || CLEARED_TOOL_PLACEHOLDER
391
+ }
392
+ // Bersihkan data URL base64 preview dari pesan lama untuk menghemat RAM dan storage
393
+ if (typeof next.preview === 'string' && next.preview.startsWith('data:image/')) {
394
+ next.preview = null
395
+ }
396
+ return next
397
+ })
398
+ if (msg.executionSteps) msg.executionSteps = pruned
399
+ if (msg.executedTools) msg.executedTools = pruned
400
+ }
401
+
402
+ // 3. Kompaksi blok kode panjang di teks lama jika > 500 char
403
+ if (typeof msg.content === 'string' && msg.content.length > 500) {
404
+ msg.content = compactCodeBlocks(msg.content)
405
+ }
406
+ }
407
+
408
+ return cloned
409
+ }
410
+
411
+ /**
412
+ * Kompatibilitas mundur: Pruning output tool lama di memori
413
+ */
414
+ export function pruneOldToolResultsInMemory(messages = [], preserveRecentTurns = 4) {
415
+ return pruneOldToolResultsInLoop(messages, preserveRecentTurns * 2)
416
+ }
417
+
418
+ /**
419
+ * Fase 2 Hermes: Penyelarasan batas mundur (Boundary Backward Alignment)
420
+ * Menjaga agar pasangan tool_calls pada role assistant dan tool_result pada role tool
421
+ * tidak pernah terputus/terbelah di antara batas potongan konteks.
422
+ */
423
+ export function alignBoundaryBackward(messages = [], splitIndex = 0) {
424
+ if (splitIndex <= 0 || splitIndex >= messages.length) return splitIndex
425
+
426
+ let idx = splitIndex
427
+
428
+ // Jika elemen di splitIndex adalah role: 'tool', mundur hingga ke role assistant pemanggilnya
429
+ while (idx > 0 && messages[idx]?.role === 'tool') {
430
+ idx--
431
+ }
432
+
433
+ // Jika elemen sebelumnya adalah assistant dengan tool_calls yang hasilnya berada di/setelah splitIndex,
434
+ // mundur agar assistant ini berada di blok yang sama dengan hasil tool-nya
435
+ if (idx > 0 && messages[idx]?.role === 'assistant' && Array.isArray(messages[idx]?.tool_calls)) {
436
+ return idx
437
+ }
438
+
439
+ return idx
440
+ }
441
+
442
+ /**
443
+ * Tahap 2: AI Summarizer inkremental
444
+ * Merangkum zona pesan lama + menyertakan existingSummaryBlock sebelumnya
445
+ */
446
+ export async function summarizeMiddle(
447
+ messagesToSummarize = [],
448
+ existingSummaryBlock = '',
449
+ activeConfig = {}
450
+ ) {
451
+ if (!Array.isArray(messagesToSummarize) || messagesToSummarize.length === 0) {
452
+ return existingSummaryBlock || ''
453
+ }
454
+
455
+ // Format percakapan baru yang meluap menjadi dialog terstruktur
456
+ const formattedLines = []
457
+ for (let i = 0; i < messagesToSummarize.length; i++) {
458
+ const msg = messagesToSummarize[i]
459
+ if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
460
+ continue
461
+ }
462
+
463
+ const sender = msg.role === 'user' ? 'User' : 'Mark'
464
+ let text = ''
465
+ if (typeof msg.content === 'string') {
466
+ text = msg.content
467
+ } else if (Array.isArray(msg.content)) {
468
+ const textParts = []
469
+ for (const part of msg.content) {
470
+ if (!part) continue
471
+ if (typeof part === 'string') textParts.push(part)
472
+ else if (part.type === 'text') textParts.push(part.text || '')
473
+ else if (part.type === 'image_url' || part.image_url || part.type === 'image') {
474
+ textParts.push('[Gambar terlampir oleh pengguna]')
475
+ } else {
476
+ textParts.push(JSON.stringify(part))
477
+ }
478
+ }
479
+ text = textParts.join('\n')
480
+ } else {
481
+ text = JSON.stringify(msg.content || '')
482
+ }
483
+
484
+ // Ringkas teks jika per giliran terlalu panjang agar tidak overload summarizer
485
+ if (text.length > 2500) {
486
+ text =
487
+ text.substring(0, 1200) + '\n... [potongan teks panjang diringkas] ...\n' + text.slice(-800)
488
+ }
489
+
490
+ formattedLines.push(`[Turn ${i + 1}] ${sender}: ${text}`)
491
+ }
492
+
493
+ let messagesText = formattedLines.join('\n\n')
494
+
495
+ // Safety buffer: jika pesan lama jutaan karakter (legacy backlog), ambil jendela potongan terdekat max ~90.000 karakter
496
+ if (messagesText.length > 90000) {
497
+ messagesText = messagesText.slice(-90000)
498
+ }
499
+
500
+ const systemPrompt = `Kamu adalah Context Compressor untuk MARK AI OS (Handover Engine).
501
+ Tugasmu: Hasilkan DOKUMEN SERAH-TERIMA TEKNIS yang padat, terstruktur, dan akurat dengan format markdown berikut:
502
+
503
+ ## Goal
504
+ [Apa yang ingin dicapai pengguna dalam tugas atau percakapan ini]
505
+
506
+ ## Constraints & Preferences
507
+ [Aturan teknis, batasan pengguna, preferensi gaya koding atau arsitektur]
508
+
509
+ ## Progress
510
+ ### Done
511
+ [Pekerjaan yang telah selesai dilakukan: berkas yang diedit/dibuat, perintah shell, status verifikasi]
512
+ ### In Progress
513
+ [Pekerjaan atau sub-langkah yang sedang berjalan saat ini]
514
+ ### Blocked
515
+ [Hambatan, bug, atau kendala jika ada]
516
+
517
+ ## Key Decisions
518
+ [Keputusan arsitektural/teknis penting dan alasannya]
519
+
520
+ ## Relevant Files
521
+ [Daftar berkas yang dibaca, dimodifikasi, atau dibuat beserta catatan singkat 1 baris mengenai fungsinya]
522
+
523
+ ## Next Steps
524
+ [Langkah kerja konkret berikutnya yang harus dilanjutkan]
525
+
526
+ ## Critical Context
527
+ [Nilai konfigurasi, port, nama variabel, ID penting, atau pesan error mentah yang esensial]
528
+
529
+ Aturan Mutlak:
530
+ 1. Jika terdapat [RINGKASAN SERAH-TERIMA SEBELUMNYA], PERBARUI dokumen tersebut: pindahkan item dari "In Progress" ke "Done", tambahkan berkas baru ke "Relevant Files", perbarui "Next Steps", dan singkirkan detail usang.
531
+ 2. Pertahankan akurasi path berkas, nama fungsi, dan detail teknis tanpa halusinasi.
532
+ 3. HANYA cetak dokumen markdown serah-terima di atas secara langsung tanpa kalimat pembuka atau penutup basa-basi.`
533
+
534
+ const userPrompt = `[RINGKASAN SERAH-TERIMA SEBELUMNYA]:
535
+ ${existingSummaryBlock ? existingSummaryBlock.trim() : '(Belum ada ringkasan sebelumnya / inisiasi pertama kali)'}
536
+
537
+ [PERCAKAPAN BARU YANG HARUS DIRANGKUM KE DOKUMEN SERAH-TERIMA]:
538
+ ${messagesText}`
539
+
540
+ const promptPayload = [
541
+ { role: 'system', content: systemPrompt },
542
+ { role: 'user', content: userPrompt }
543
+ ]
544
+
545
+ // 1. Coba Gemini Web terlebih dahulu (gratis, hemat kuota)
546
+ try {
547
+ const response = await fetchAI(promptPayload, false, {
548
+ isSmallTask: true,
549
+ configOverride: { aiProvider: 'gemini-web', geminiWebModel: 'gemini-3.5-flash-thinking' }
550
+ })
551
+
552
+ if (response && response.content && !response.error) {
553
+ return response.content.trim()
554
+ }
555
+ } catch (err) {
556
+ console.warn(
557
+ '[contextManager] Gemini Web summarizer gagal, beralih ke active provider:',
558
+ err?.message
559
+ )
560
+ }
561
+
562
+ // 2. Fallback ke active provider sesi jika offline / Gemini Web gagal
563
+ try {
564
+ const response = await fetchAI(promptPayload, false, {
565
+ isSmallTask: true,
566
+ config: activeConfig
567
+ })
568
+
569
+ if (response && response.content && !response.error) {
570
+ return response.content.trim()
571
+ }
572
+ } catch (err) {
573
+ console.error('[contextManager] Fallback active provider summarizer gagal:', err?.message)
574
+ }
575
+
576
+ // 3. Jika AI sama sekali tidak dapat dijangkau, buat stub ringkasan darurat berbasis teks
577
+ return `${existingSummaryBlock ? existingSummaryBlock + '\n\n' : ''}[Arsip percakapan lampau: ${messagesToSummarize.length} pesan telah dikompaksi pada ${new Date().toLocaleString('id-ID')}]`
578
+ }
579
+
580
+ /**
581
+ * Tahap 3: Pembersihan pasangan tool yang orphan
582
+ */
583
+ export function cleanOrphanToolPairs(messages = []) {
584
+ if (!Array.isArray(messages)) return []
585
+
586
+ const valid = []
587
+ const toolCallIds = new Set()
588
+
589
+ // Kumpulkan semua tool_call IDs yang valid
590
+ for (const msg of messages) {
591
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
592
+ for (const tc of msg.tool_calls) {
593
+ if (tc.id) toolCallIds.add(tc.id)
594
+ }
595
+ }
596
+ }
597
+
598
+ for (const msg of messages) {
599
+ // Jika ada tool response tetapi tool_call pasangannya hilang, abaikan agar tidak error 400
600
+ if (msg.role === 'tool' && msg.tool_call_id && !toolCallIds.has(msg.tool_call_id)) {
601
+ continue
602
+ }
603
+ valid.push(msg)
604
+ }
605
+
606
+ return valid
607
+ }
608
+
609
+ /**
610
+ * Orkestrator Utama: Menjalankan Hybrid Compaction untuk satu sesi
611
+ */
612
+ export async function executeSessionCompaction({
613
+ sessionId = '1',
614
+ messages = [],
615
+ activeConfig = {},
616
+ onProgress = null,
617
+ force = false,
618
+ systemPrompt = ''
619
+ }) {
620
+ // Ambil summary & pointer sebelumnya dari tabel session_compact jika ada
621
+ let existingSummaryBlock = ''
622
+ let existingLastCompactedId = null
623
+ try {
624
+ const existingData = await getSessionCompact(sessionId)
625
+ if (existingData) {
626
+ existingSummaryBlock = existingData.summaryBlock || existingData.summary_block || ''
627
+ existingLastCompactedId =
628
+ existingData.lastCompactedMessageId || existingData.last_compacted_message_id || null
629
+ }
630
+ } catch (err) {
631
+ console.warn('[contextManager] Gagal mengambil session_compact lama:', err)
632
+ }
633
+
634
+ const currentTokens = calculateSessionTokens(
635
+ messages,
636
+ existingSummaryBlock,
637
+ existingLastCompactedId,
638
+ systemPrompt
639
+ )
640
+ if (!force && currentTokens < MAX_CONTEXT_TOKENS) {
641
+ return {
642
+ success: true,
643
+ isCompacted: false,
644
+ compactedMessages: messages,
645
+ currentTokens,
646
+ currentChars: currentTokens
647
+ }
648
+ }
649
+
650
+ // -------------------------------------------------------------
651
+ // TAHAP 1: Pruning Tool Output di Memori
652
+ // -------------------------------------------------------------
653
+ if (typeof onProgress === 'function') {
654
+ onProgress({ stage: 'pruning', text: 'Memangkas log tool di memori...' })
655
+ }
656
+
657
+ const prunedMessages = pruneOldToolResultsInMemory(messages, 4)
658
+ const prunedTokens = calculateSessionTokens(
659
+ prunedMessages,
660
+ existingSummaryBlock,
661
+ existingLastCompactedId,
662
+ systemPrompt
663
+ )
664
+
665
+ // Jika Tahap 1 saja sudah cukup membawa token di bawah batas (hanya saat auto-compact / !force):
666
+ if (!force && prunedTokens < MAX_CONTEXT_TOKENS) {
667
+ // Simpan hasil prune ke tabel sessions
668
+ try {
669
+ await saveSession(sessionId, prunedMessages)
670
+ } catch (e) {
671
+ console.warn('[contextManager] Gagal menyimpan pruned messages ke sessions:', e)
672
+ }
673
+
674
+ return {
675
+ success: true,
676
+ isCompacted: true,
677
+ prunedOnly: true,
678
+ compactedMessages: prunedMessages,
679
+ currentTokens: prunedTokens,
680
+ currentChars: prunedTokens
681
+ }
682
+ }
683
+
684
+ // -------------------------------------------------------------
685
+ // TAHAP 2: AI Summarization (Sliding Window & Delta)
686
+ // -------------------------------------------------------------
687
+ if (typeof onProgress === 'function') {
688
+ onProgress({ stage: 'summarizing', text: 'Merangkum konteks percakapan lama...' })
689
+ }
690
+
691
+ // Hanya pertahankan 1 pesan terakhir (prompt user yang baru dikirim pada index N)
692
+ // Seluruh riwayat pesan sebelumnya (0 s/d N-1) dirangkum ke dalam summary block.
693
+ const tailStartIndex = Math.max(0, prunedMessages.length - 1)
694
+
695
+ // Cari posisi batas pemadatan sebelumnya jika ada
696
+ let startIndexToSummarize = 0
697
+ if (existingLastCompactedId) {
698
+ const targetId = String(existingLastCompactedId)
699
+ for (let i = 0; i < prunedMessages.length; i++) {
700
+ const msg = prunedMessages[i]
701
+ if (
702
+ getMessageId(msg, i) === targetId ||
703
+ String(msg?.id) === targetId ||
704
+ String(msg?.timestamp) === targetId
705
+ ) {
706
+ startIndexToSummarize = i + 1
707
+ break
708
+ }
709
+ }
710
+ }
711
+
712
+ const effectiveTailIndex = Math.max(tailStartIndex, startIndexToSummarize)
713
+ const messagesToSummarize = prunedMessages.slice(startIndexToSummarize, effectiveTailIndex)
714
+ const tailMessages = prunedMessages.slice(effectiveTailIndex)
715
+
716
+ let newSummaryBlock = existingSummaryBlock
717
+ let lastCompactedMessageId = existingLastCompactedId
718
+
719
+ if (messagesToSummarize.length > 0) {
720
+ const lastCompactedMsg = messagesToSummarize[messagesToSummarize.length - 1]
721
+ if (!lastCompactedMsg.id && !lastCompactedMsg.timestamp && !lastCompactedMsg.created_at) {
722
+ lastCompactedMsg.created_at = Date.now()
723
+ lastCompactedMsg.id = `msg_${lastCompactedMsg.created_at}`
724
+ }
725
+ lastCompactedMessageId = getMessageId(lastCompactedMsg, effectiveTailIndex - 1)
726
+
727
+ newSummaryBlock = await summarizeMiddle(messagesToSummarize, existingSummaryBlock, activeConfig)
728
+ }
729
+
730
+ // Simpan metadata ke tabel session_compact
731
+ try {
732
+ await saveSessionCompact(sessionId, {
733
+ summaryBlock: newSummaryBlock,
734
+ lastCompactedMessageId,
735
+ lastCompactedAt: Date.now()
736
+ })
737
+ } catch (err) {
738
+ console.error('[contextManager] Gagal menyimpan session_compact:', err)
739
+ }
740
+
741
+ // Simpan juga versi pesan yang sudah dipangkas ke sessions agar database ramping
742
+ try {
743
+ await saveSession(sessionId, prunedMessages)
744
+ } catch (e) {
745
+ console.warn('[contextManager] Gagal menyimpan sessions:', e)
746
+ }
747
+
748
+ const finalTokens = calculateSessionTokens(
749
+ tailMessages,
750
+ newSummaryBlock,
751
+ null, // tailMessages sudah merupakan pesan setelah cutIndex
752
+ systemPrompt
753
+ )
754
+
755
+ return {
756
+ success: true,
757
+ isCompacted: true,
758
+ prunedOnly: false,
759
+ compactedMessages: prunedMessages,
760
+ tailMessages,
761
+ lastCompactedMessageId,
762
+ newSummaryBlock,
763
+ currentTokens: finalTokens,
764
+ currentChars: finalTokens
765
+ }
766
+ }
767
+
768
+ /**
769
+ * Memformat pesan dengan riwayat executionSteps/executedTools utuh (100% fullResult tanpa batasan turn)
770
+ */
771
+ export function formatMessageWithToolLogs(msg) {
772
+ if (!msg) return ''
773
+ const steps = msg.executionSteps || msg.executedTools
774
+ if (Array.isArray(msg.content)) {
775
+ let newContent = [...msg.content]
776
+ if (Array.isArray(steps) && steps.length > 0) {
777
+ const toolLog = steps
778
+ .map((t) => {
779
+ if (t.type === 'intervention') {
780
+ return ` * [Intervensi Pengguna]: "${t.text || ''}"`
781
+ }
782
+ if (t.type === 'narration' || (!t.tool && t.text)) {
783
+ return ` * [Catatan Narasi AI]: "${t.text || ''}"`
784
+ }
785
+ const res = t.fullResult || t.resultSummary || 'OK'
786
+ const reasonStr = t.reason ? ` (${t.reason})` : ''
787
+ return ` * [Tool: ${t.tool || t.task || 'tool'}]${reasonStr} query: "${t.query || ''}"\n Hasil:\n${res}`
788
+ })
789
+ .join('\n\n')
790
+ if (toolLog) {
791
+ newContent.push({ type: 'text', text: `\n\n[RIWAYAT LANGKAH EKSEKUSI TURN INI]:\n${toolLog}` })
792
+ }
793
+ }
794
+ const role = (msg.role || '').toLowerCase()
795
+ if ((role === 'ai' || role === 'assistant') && msg.mood) {
796
+ const textItemIdx = newContent.findIndex((p) => p && p.type === 'text')
797
+ if (textItemIdx >= 0) {
798
+ if (!/<mark\b/i.test(newContent[textItemIdx].text || '')) {
799
+ newContent[textItemIdx] = {
800
+ ...newContent[textItemIdx],
801
+ text: `<mark mood="${msg.mood}" done="true" /> ${newContent[textItemIdx].text || ''}`
802
+ }
803
+ }
804
+ } else {
805
+ newContent.unshift({ type: 'text', text: `<mark mood="${msg.mood}" done="true" />` })
806
+ }
807
+ }
808
+ return newContent
809
+ }
810
+
811
+ let content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || '')
812
+ if (Array.isArray(steps) && steps.length > 0) {
813
+ const toolLog = steps
814
+ .map((t) => {
815
+ if (t.type === 'intervention') {
816
+ return ` * [Intervensi Pengguna]: "${t.text || ''}"`
817
+ }
818
+ if (t.type === 'narration' || (!t.tool && t.text)) {
819
+ return ` * [Catatan Narasi AI]: "${t.text || ''}"`
820
+ }
821
+ const res = t.fullResult || t.resultSummary || 'OK'
822
+ const reasonStr = t.reason ? ` (${t.reason})` : ''
823
+ return ` * [Tool: ${t.tool || t.task || 'tool'}]${reasonStr} query: "${t.query || ''}"\n Hasil:\n${res}`
824
+ })
825
+ .join('\n\n')
826
+ if (toolLog) {
827
+ content = `[RIWAYAT LANGKAH EKSEKUSI TURN INI]:\n${toolLog}\n\n[JAWABAN]:\n${content}`
828
+ }
829
+ }
830
+
831
+ // Sisipkan tag mood pada pesan asisten jika ada dan belum tersemat
832
+ const role = (msg.role || '').toLowerCase()
833
+ if ((role === 'ai' || role === 'assistant') && msg.mood && !/<mark\b/i.test(content)) {
834
+ content = `<mark mood="${msg.mood}" done="true" /> ${content}`
835
+ }
836
+
837
+ return content
838
+ }
839
+
840
+ /**
841
+ * Merakit payload prompt LLM secara non-destructive dengan summary block jika tersedia,
842
+ * atau seluruh pesan 100% utuh beserta log tool jika belum melewati 525K karakter.
843
+ */
844
+ export function assembleCompactedPayload({
845
+ messages = [],
846
+ sessionCompact = null,
847
+ systemPrompt = ''
848
+ }) {
849
+ const payload = []
850
+
851
+ if (systemPrompt) {
852
+ payload.push({ role: 'system', content: systemPrompt })
853
+ }
854
+
855
+ const summaryBlock = sessionCompact?.summaryBlock || sessionCompact?.summary_block
856
+ const lastCompactedId =
857
+ sessionCompact?.lastCompactedMessageId || sessionCompact?.last_compacted_message_id
858
+
859
+ if (summaryBlock && lastCompactedId) {
860
+ // Cari index dari pesan dengan id lastCompactedId
861
+ let cutIndex = -1
862
+ for (let i = 0; i < messages.length; i++) {
863
+ const msg = messages[i]
864
+ if (
865
+ getMessageId(msg, i) === String(lastCompactedId) ||
866
+ String(msg?.id) === String(lastCompactedId) ||
867
+ String(msg?.timestamp) === String(lastCompactedId) ||
868
+ String(msg?.created_at) === String(lastCompactedId)
869
+ ) {
870
+ cutIndex = i
871
+ break
872
+ }
873
+ }
874
+
875
+ if (cutIndex !== -1) {
876
+ // Jika pesan batas ditemukan, ambil pesan setelah cutIndex
877
+ const activeSlice = messages.slice(cutIndex + 1)
878
+
879
+ // Sisipkan summary block sebagai pesan user pembuka konteks
880
+ payload.push({
881
+ role: 'user',
882
+ content: `[ COMPACTED MESSAGE SUMMARY ] ${summaryBlock}`
883
+ })
884
+
885
+ // Masukkan pesan-pesan tail terkini dengan log tool utuh
886
+ for (const msg of activeSlice) {
887
+ if (
888
+ !msg ||
889
+ msg.isThinking ||
890
+ msg.isSearching ||
891
+ msg.isSummarizing ||
892
+ msg.role === 'command'
893
+ ) {
894
+ continue
895
+ }
896
+ payload.push({
897
+ role: msg.role === 'ai' || msg.role === 'planSteps' ? 'assistant' : msg.role,
898
+ content: formatMessageWithToolLogs(msg),
899
+ mood: msg.mood || undefined
900
+ })
901
+ }
902
+
903
+ return cleanOrphanToolPairs(payload)
904
+ }
905
+
906
+ // Jika cutIndex === -1 (ID batas tidak ditemukan di riwayat aktif),
907
+ // artinya pointer kompaksi sudah kadaluarsa. JANGAN sisipkan ringkasan basi
908
+ // dan JANGAN memenggal pesan ke slice(-1)! Alirkan seluruh riwayat pesan utuh.
909
+ console.warn(
910
+ `[contextManager] Pointer lastCompactedId "${lastCompactedId}" tidak ditemukan di array pesan aktif. Mengabaikan ringkasan basi dan mengirim seluruh riwayat pesan utuh.`
911
+ )
912
+ }
913
+
914
+ // Jika belum ada summary (konteks < 525K), susun SELURUH pesan dan log tool 100% UTUH
915
+ for (const msg of messages) {
916
+ if (!msg || msg.isThinking || msg.isSearching || msg.isSummarizing || msg.role === 'command') {
917
+ continue
918
+ }
919
+ payload.push({
920
+ role: msg.role === 'ai' || msg.role === 'planSteps' ? 'assistant' : msg.role,
921
+ content: formatMessageWithToolLogs(msg),
922
+ mood: msg.mood || undefined
923
+ })
924
+ }
925
+
926
+ return cleanOrphanToolPairs(payload)
927
+ }
928
+
929
+ /**
930
+ * In-Flight Pruning untuk loop ReAct.
931
+ * Berbasis kapasitas ambang batas 256K tokens (MAX_CONTEXT_TOKENS).
932
+ * Jika selama giliran panjang akumulasi loopMessages >= MAX_CONTEXT_TOKENS,
933
+ * pangkas data output tool terlama di dalam loopMessages sampai total token kembali < MAX_CONTEXT_TOKENS.
934
+ */
935
+ export function pruneInFlightMessages(messages = [], maxTokens = MAX_CONTEXT_TOKENS) {
936
+ if (!Array.isArray(messages) || messages.length === 0) return messages
937
+
938
+ let totalTokens = 0
939
+ for (const m of messages) {
940
+ totalTokens += calculateMessageTokens(m)
941
+ }
942
+
943
+ if (totalTokens < maxTokens) return messages
944
+
945
+ // Pangkas output tool terlama satu per satu sampai di bawah maxTokens
946
+ for (let i = 0; i < messages.length; i++) {
947
+ if (totalTokens < maxTokens) break
948
+ const m = messages[i]
949
+ if (m && m.role === 'tool' && m.content) {
950
+ const origTokens = calculateMessageTokens(m)
951
+ let parsed = null
952
+ try {
953
+ parsed = typeof m.content === 'string' ? JSON.parse(m.content) : m.content
954
+ } catch (err) {
955
+ void err
956
+ }
957
+
958
+ if (parsed && (parsed.data || parsed.output)) {
959
+ const prunedText = '[Output dipangkas: kapasitas sesi mencapai 256K tokens]'
960
+ parsed.data = prunedText
961
+ if (parsed.output) parsed.output = prunedText
962
+ m.content = JSON.stringify(parsed)
963
+ m.tokens = null // invalidate cache
964
+ const newTokens = calculateMessageTokens(m)
965
+ totalTokens -= origTokens - newTokens
966
+ } else if (typeof m.content === 'string' && m.content.length > 200) {
967
+ m.content = '[Output tool lama dipangkas karena kapasitas 256K tokens]'
968
+ m.tokens = null
969
+ const newTokens = calculateMessageTokens(m)
970
+ totalTokens -= origTokens - newTokens
971
+ }
972
+ }
973
+ }
974
+
975
+ return messages
976
+ }
977
+
978
+ /**
979
+ * In-Loop Context Guard untuk ReAct loop (Lead Agent & Sub-Agent).
980
+ * Dipanggil di setiap iterasi ReAct loop sebelum mengirim prompt ke LLM.
981
+ *
982
+ * Mengimplementasikan 4 Fase Hermes secara in-place:
983
+ * - Fase 1: Zero-cost O(n) pruning pada tool output lama di luar tail (>200 karakter)
984
+ * - Fase 2: Backward boundary alignment untuk menjaga pasangan tool_calls dan tool_result
985
+ * - Fase 3: Structured Handover Summary dengan update iteratif
986
+ * - Fase 4: Perakitan pesan in-place stabil pada session ID yang sama
987
+ */
988
+ export async function checkAndCompressInLoop({
989
+ loopMessages = [],
990
+ sessionId = '1',
991
+ maxTokens = MAX_CONTEXT_TOKENS,
992
+ maxChars = null, // backward compatibility
993
+ thresholdRatio = IN_LOOP_COMPACT_THRESHOLD,
994
+ protectLastN = 6,
995
+ protectFirstN = 2,
996
+ activeConfig = {},
997
+ onProgress = null,
998
+ systemPrompt = ''
999
+ }) {
1000
+ if (!Array.isArray(loopMessages) || loopMessages.length <= protectLastN + protectFirstN) {
1001
+ return { compressed: false, loopMessages }
1002
+ }
1003
+
1004
+ const effectiveMaxTokens = maxTokens || MAX_CONTEXT_TOKENS
1005
+ const triggerLimit = effectiveMaxTokens * thresholdRatio
1006
+ let totalTokens = countTokens(systemPrompt)
1007
+ let sessionTokens = 0
1008
+ for (const m of loopMessages) {
1009
+ const tokens = calculateMessageTokens(m)
1010
+ totalTokens += tokens
1011
+ if (m.role !== 'system') {
1012
+ sessionTokens += tokens
1013
+ }
1014
+ }
1015
+
1016
+ // Jika masih di bawah ambang batas (50% dari 256K) dan jumlah pesan belum terlalu panjang (< 24),
1017
+ // tidak memerlukan kompresi
1018
+ if (totalTokens < triggerLimit && loopMessages.length < 24) {
1019
+ return {
1020
+ compressed: false,
1021
+ loopMessages,
1022
+ totalTokens: sessionTokens,
1023
+ totalChars: sessionTokens
1024
+ }
1025
+ }
1026
+
1027
+ if (typeof onProgress === 'function') {
1028
+ onProgress({ stage: 'pruning', text: 'In-loop: Memangkas log tool lama...' })
1029
+ }
1030
+
1031
+ // FASE 1: Zero-cost O(n) Tool Pruning
1032
+ const prunedMessages = pruneOldToolResultsInLoop(loopMessages, protectLastN)
1033
+ let prunedTokens = countTokens(systemPrompt)
1034
+ let prunedSessionTokens = 0
1035
+ for (const m of prunedMessages) {
1036
+ m.tokens = null // invalidate cache for pruned
1037
+ const tokens = calculateMessageTokens(m)
1038
+ prunedTokens += tokens
1039
+ if (m.role !== 'system') {
1040
+ prunedSessionTokens += tokens
1041
+ }
1042
+ }
1043
+
1044
+ // Jika Fase 1 saja sudah cukup membawa konteks di bawah ambang batas 50%:
1045
+ if (prunedTokens < triggerLimit) {
1046
+ const didPrune = prunedTokens < totalTokens
1047
+ return {
1048
+ compressed: didPrune,
1049
+ prunedOnly: true,
1050
+ loopMessages: prunedMessages,
1051
+ totalTokens: prunedSessionTokens,
1052
+ totalChars: prunedSessionTokens
1053
+ }
1054
+ }
1055
+
1056
+ // FASE 2: Tentukan Batas Head, Middle, dan Tail dengan Backward Alignment
1057
+ if (typeof onProgress === 'function') {
1058
+ onProgress({ stage: 'summarizing', text: 'In-loop: Merangkum serah-terima teknis...' })
1059
+ }
1060
+
1061
+ const head = prunedMessages.slice(0, Math.min(protectFirstN, prunedMessages.length))
1062
+ const rawTailStart = Math.max(head.length, prunedMessages.length - protectLastN)
1063
+ const alignedTailStart = alignBoundaryBackward(prunedMessages, rawTailStart)
1064
+ const middle = prunedMessages.slice(head.length, alignedTailStart)
1065
+ const tail = prunedMessages.slice(alignedTailStart)
1066
+
1067
+ if (middle.length === 0) {
1068
+ return {
1069
+ compressed: true,
1070
+ prunedOnly: true,
1071
+ loopMessages: prunedMessages,
1072
+ totalTokens: prunedTokens,
1073
+ totalChars: prunedTokens
1074
+ }
1075
+ }
1076
+
1077
+ // Ambil summary serah-terima sebelumnya jika ada
1078
+ let existingSummary = ''
1079
+ try {
1080
+ const compactRec = await getSessionCompact(sessionId)
1081
+ if (compactRec) {
1082
+ existingSummary = compactRec.summaryBlock || compactRec.summary_block || ''
1083
+ }
1084
+ } catch {
1085
+ /* ignore */
1086
+ }
1087
+
1088
+ // FASE 3: Generate Structured Handover Summary (Iteratif)
1089
+ const newSummary = await summarizeMiddle(middle, existingSummary, activeConfig)
1090
+
1091
+ // Simpan ringkasan baru secara in-place ke session_compact
1092
+ try {
1093
+ const lastMiddleMsg = middle[middle.length - 1]
1094
+ await saveSessionCompact(sessionId, {
1095
+ summaryBlock: newSummary,
1096
+ lastCompactedMessageId: getMessageId(lastMiddleMsg, alignedTailStart - 1),
1097
+ lastCompactedAt: Date.now()
1098
+ })
1099
+ } catch (err) {
1100
+ console.warn('[contextManager] In-loop saveSessionCompact gagal:', err)
1101
+ }
1102
+
1103
+ // FASE 4: Perakitan Pesan In-Place [Head] + [Handover Summary] + [Tail]
1104
+ const handoverContent = `[ CONTEXT COMPACTION (IN-LOOP HANDOVER) ]\n${newSummary}`
1105
+ const assembled = [
1106
+ ...head,
1107
+ {
1108
+ role: 'user',
1109
+ content: handoverContent
1110
+ },
1111
+ ...tail
1112
+ ]
1113
+
1114
+ const sanitized = cleanOrphanToolPairs(assembled)
1115
+ let finalSessionTokens = 0
1116
+ for (const m of sanitized) {
1117
+ if (m.role !== 'system') {
1118
+ finalSessionTokens += calculateMessageTokens(m)
1119
+ }
1120
+ }
1121
+
1122
+ return {
1123
+ compressed: true,
1124
+ prunedOnly: false,
1125
+ loopMessages: sanitized,
1126
+ totalTokens: finalSessionTokens,
1127
+ totalChars: finalSessionTokens,
1128
+ newSummaryBlock: newSummary
1129
+ }
1130
+ }