@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,2739 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { buildPlanningSystemPrompt } from '../../api/ai/planning'
3
+ import { getYoutubeSummary } from '../../api/ai/tools'
4
+ import { fetchAI } from '../../api/ai/core'
5
+ import { playVoice, speechQueue, getCurrentTimeInfo } from '../../api/ai/utils'
6
+ import { parseMarkTag, stripMarkTags } from '@shared/parsers/mark-tag-parser.js'
7
+ import { executeAgentTool } from './executeAgentTool.js'
8
+ import { webApi } from '../../api/web-bridge.js'
9
+ import {
10
+ deleteMemory,
11
+ getAllMemory,
12
+ insertMemory,
13
+ updateMemory,
14
+ saveSession,
15
+ getChatData,
16
+ getSessionCompact,
17
+ db
18
+ } from '../../api/db'
19
+ import { checkTools, getActiveToolsSchema } from '../../api/tools/index'
20
+ import { startAgentTaskStep, transitionAgentTask } from '../../api/taskStore'
21
+ import { getUnifiedContext, generateVector, executeMemorySearch } from '../../api/vectorMemory'
22
+ import { searchMemoriesInOrama } from '../../api/oramaStore'
23
+ import {
24
+ MAX_CONTEXT_TOKENS,
25
+ MAX_CONTEXT_CHARS,
26
+ GATEWAY_HYGIENE_THRESHOLD,
27
+ IN_LOOP_COMPACT_THRESHOLD,
28
+ calculateSessionTokens,
29
+ calculateMessageTokens,
30
+ calculateSessionChars,
31
+ executeSessionCompaction,
32
+ assembleCompactedPayload,
33
+ pruneInFlightMessages,
34
+ checkAndCompressInLoop
35
+ } from '../../api/ai/contextManager'
36
+ import { saveWorkspaceWorkingMemory } from '../../api/workspaceRag'
37
+ import { synthesizeSkillAndSave } from '../../api/ai/skillSynthesizer'
38
+
39
+ // ============================================================================
40
+ // HELPER UTILITIES
41
+ // ============================================================================
42
+
43
+ const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
44
+
45
+ const isImagePath = (filePath = '') => {
46
+ if (typeof filePath === 'string' && filePath.startsWith('data:image/')) return true
47
+ const ext = String(filePath || '')
48
+ .split('.')
49
+ .pop()
50
+ .toLowerCase()
51
+ return IMAGE_EXTS.includes(`.${ext}`)
52
+ }
53
+
54
+ const convertFilePathToBase64 = async (filePath) => {
55
+ try {
56
+ if (typeof filePath === 'string' && filePath.startsWith('data:image/')) {
57
+ return filePath
58
+ }
59
+ if (window.api?.executeNativeTool) {
60
+ try {
61
+ const toolRes = await window.api.executeNativeTool('read-file', { path: filePath })
62
+ if (toolRes && toolRes.success && toolRes.dataUrl) {
63
+ return toolRes.dataUrl
64
+ }
65
+ } catch (_) {}
66
+ }
67
+ const formattedUrl = filePath.startsWith('file://')
68
+ ? filePath
69
+ : `file:///${filePath.replace(/\\/g, '/')}`
70
+ const res = await fetch(formattedUrl)
71
+ const blob = await res.blob()
72
+ return await new Promise((resolve, reject) => {
73
+ const reader = new FileReader()
74
+ reader.onloadend = () => resolve(reader.result)
75
+ reader.onerror = reject
76
+ reader.readAsDataURL(blob)
77
+ })
78
+ } catch (err) {
79
+ console.error('[useMarkPlan] Failed to convert image file to Base64:', filePath, err)
80
+ return null
81
+ }
82
+ }
83
+
84
+ // ============================================================================
85
+ // MAIN HOOK: useMarkPlan
86
+ // ============================================================================
87
+
88
+ export const useMarkPlan = ({
89
+ chatData,
90
+ setChatData,
91
+ config,
92
+ isSpeak,
93
+ abortControllerRef,
94
+ setIsLoading,
95
+ setIsAgentBusy,
96
+ runningSessionId,
97
+ setRunningSessionId,
98
+ runningSessionIds,
99
+ setRunningSessionIds,
100
+ addRunningSessionId,
101
+ removeRunningSessionId,
102
+ setMessage,
103
+ handleYoutubeSearch,
104
+ handleSearchCommand,
105
+ handleYoutubeSummary,
106
+ handleMusic,
107
+ getYoutubeData,
108
+ pushProcess,
109
+ dismissProcess,
110
+ activeTopic,
111
+ setActiveTopic,
112
+ currentMusicTrack,
113
+ requestApproval,
114
+ requestCameraCapture
115
+ }) => {
116
+ const chatDataRef = useRef(chatData)
117
+ useEffect(() => {
118
+ chatDataRef.current = chatData
119
+ }, [chatData])
120
+
121
+ // Map menyimpan sesi yang sedang berjalan: key = sessionId, value = { abortController, startTime, prompt }
122
+ const activeSessionsRef = useRef(new Map())
123
+ // Map menyimpan updater fungsi setChatData per sesi untuk IPC status AI
124
+ const activeSessionUpdatersRef = useRef(new Map())
125
+
126
+ // Listener event status AI dari Main Process (IPC)
127
+ useEffect(() => {
128
+ if (window.api && window.api.onAiStatus) {
129
+ window.api.onAiStatus((msg) => {
130
+ if (activeSessionUpdatersRef.current.size > 0) {
131
+ for (const updater of activeSessionUpdatersRef.current.values()) {
132
+ try {
133
+ updater((prev) => {
134
+ const filtered = prev.filter((item) => !item.isThinking)
135
+ return [...filtered, { role: 'ai', content: msg, isThinking: true }]
136
+ })
137
+ } catch (e) {}
138
+ }
139
+ } else {
140
+ setChatData((prev) => {
141
+ const filtered = prev.filter((item) => !item.isThinking)
142
+ return [...filtered, { role: 'ai', content: msg, isThinking: true }]
143
+ })
144
+ }
145
+ })
146
+ }
147
+
148
+ if (window.api && window.api.onAiAbort) {
149
+ const unsub = window.api.onAiAbort((payload) => {
150
+ console.warn('[useMarkPlan] Sinyal ai:abort diterima:', payload)
151
+ handleStop()
152
+ })
153
+ return () => {
154
+ if (typeof unsub === 'function') unsub()
155
+ }
156
+ }
157
+ }, [setChatData])
158
+
159
+ const activeTaskObjectiveRef = useRef(null)
160
+ const lastUserPromptRef = useRef('')
161
+ const activeRunningSessionIdRef = useRef(1)
162
+
163
+ const targetPushProcess = (proc) => {
164
+ if (
165
+ (activeRunningSessionIdRef.current === 1 || !activeRunningSessionIdRef.current) &&
166
+ pushProcess
167
+ ) {
168
+ pushProcess(proc)
169
+ }
170
+ }
171
+
172
+ // Menampung arahan/intervensi user saat ReAct loop sedang berjalan
173
+ const handleIntervention = (msg, targetSessionId = null, opts = {}) => {
174
+ if (!msg || (typeof msg === 'string' && !msg.trim())) return
175
+ const textMsg = typeof msg === 'string' ? msg.trim() : String(msg)
176
+ const displayMsg = opts.displayPrompt || textMsg
177
+ const sId =
178
+ targetSessionId !== null && targetSessionId !== undefined
179
+ ? Number(targetSessionId)
180
+ : activeRunningSessionIdRef.current || 1
181
+
182
+ const interventionItem = {
183
+ type: 'intervention',
184
+ text: displayMsg,
185
+ timestamp: Date.now()
186
+ }
187
+
188
+ const session = activeSessionsRef.current.get(sId)
189
+ if (session) {
190
+ if (!session.interventions) session.interventions = []
191
+ session.interventions.push(textMsg)
192
+ if (session.executedToolsList) {
193
+ session.executedToolsList.push(interventionItem)
194
+ }
195
+ }
196
+
197
+ const updater = activeSessionUpdatersRef.current.get(sId) || (sId === 1 ? setChatData : null)
198
+ if (updater) {
199
+ updater((prev) => {
200
+ const thinkingIndex = prev.findIndex((item) => item.isThinking)
201
+ if (thinkingIndex === -1) return prev
202
+ const thinkingItem = prev[thinkingIndex]
203
+ const currentTools = Array.isArray(thinkingItem.executedTools)
204
+ ? thinkingItem.executedTools
205
+ : []
206
+ const updatedThinking = {
207
+ ...thinkingItem,
208
+ executedTools: [...currentTools, interventionItem]
209
+ }
210
+ const newArr = [...prev]
211
+ newArr[thinkingIndex] = updatedThinking
212
+ return newArr
213
+ })
214
+ }
215
+ }
216
+
217
+ // Penghentian tugas per-sesi secara independen
218
+ const handleStop = async (targetSessionId = null) => {
219
+ try {
220
+ speechQueue.reset()
221
+ } catch (_) {}
222
+
223
+ const numId =
224
+ targetSessionId !== null && targetSessionId !== undefined ? Number(targetSessionId) : null
225
+ const strId =
226
+ targetSessionId !== null && targetSessionId !== undefined ? String(targetSessionId) : null
227
+
228
+ const sessionsToAbort = []
229
+ if (numId !== null) {
230
+ const s = activeSessionsRef.current.get(numId) || activeSessionsRef.current.get(strId)
231
+ if (s) sessionsToAbort.push(s)
232
+ }
233
+
234
+ if (sessionsToAbort.length === 0) {
235
+ for (const s of activeSessionsRef.current.values()) {
236
+ sessionsToAbort.push(s)
237
+ }
238
+ }
239
+
240
+ for (const session of sessionsToAbort) {
241
+ if (session.abortController) {
242
+ try {
243
+ session.abortController.abort()
244
+ } catch (_) {}
245
+ }
246
+ }
247
+
248
+ if (abortControllerRef?.current) {
249
+ try {
250
+ abortControllerRef.current.abort()
251
+ } catch (_) {}
252
+ }
253
+
254
+ if (window.api && window.api.abortFetchAI) {
255
+ try {
256
+ window.api.abortFetchAI()
257
+ } catch (_) {}
258
+ }
259
+
260
+ if (window.api && window.api.browserClose) {
261
+ const closeId = numId === 1 || !numId ? 'main' : `workspace-${numId}`
262
+ window.api.browserClose({ sessionId: closeId }).catch(() => {})
263
+ }
264
+
265
+ // Fail-safe: Langsung batalkan semua agent tasks yang berstatus running di SQLite
266
+ try {
267
+ const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
268
+ for (const t of runningTasks) {
269
+ await transitionAgentTask(t.id, 'cancelled', 'user_abort').catch(() => {})
270
+ }
271
+ } catch (_) {}
272
+
273
+ // Fail-safe: Langsung update chatData UI agar alur kerja dan spinner berhenti seketika
274
+ const stopUpdater = (prev) => {
275
+ const filtered = prev.filter((item) => !item.isThinking)
276
+ return filtered.map((msg) => {
277
+ if (!msg.isPlanSteps || msg.taskStatus !== 'running') return msg
278
+ return {
279
+ ...msg,
280
+ taskStatus: 'stopped',
281
+ plan: (msg.plan || []).map((s) => ({
282
+ ...s,
283
+ status: s.status === 'running' ? 'stopped' : s.status
284
+ }))
285
+ }
286
+ })
287
+ }
288
+
289
+ if (numId !== null && activeSessionUpdatersRef.current.has(numId)) {
290
+ activeSessionUpdatersRef.current.get(numId)(stopUpdater)
291
+ } else {
292
+ setChatData(stopUpdater)
293
+ for (const updater of activeSessionUpdatersRef.current.values()) {
294
+ updater(stopUpdater)
295
+ }
296
+ }
297
+
298
+ if (numId === 1 || numId === null) {
299
+ setIsLoading(false)
300
+ setIsAgentBusy(false)
301
+ }
302
+ }
303
+
304
+ // ==========================================================================
305
+ // DISPATCHER EKSEKUSI INDIVIDUAL TOOL (Native & Functional)
306
+ // ==========================================================================
307
+ const executeSingleTool = async (tool, rawArgs, context) => {
308
+ const {
309
+ tgContext,
310
+ isAutonomous,
311
+ pluginProcessId,
312
+ activeSessionNum = 1,
313
+ activeTopic = null,
314
+ userInput = '',
315
+ durableTask = null,
316
+ durableActiveStep = null,
317
+ agenticProcessId = null,
318
+ targetSetChatData = setChatData,
319
+ signal
320
+ } = context
321
+ const currentSignal = signal || abortControllerRef?.current?.signal
322
+ let resultString = 'Tidak ada hasil.'
323
+
324
+ const stringQuery =
325
+ typeof rawArgs === 'string'
326
+ ? rawArgs
327
+ : typeof rawArgs === 'object' && rawArgs !== null
328
+ ? rawArgs.query ||
329
+ rawArgs.prompt ||
330
+ rawArgs.text ||
331
+ rawArgs.path ||
332
+ JSON.stringify(rawArgs)
333
+ : ''
334
+
335
+ try {
336
+ // 1. YouTube Search
337
+ if (tool === 'yt-search') {
338
+ const q = typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
339
+ const ytResults = await window.api.searchYoutube(q)
340
+ resultString = JSON.stringify(ytResults)
341
+ }
342
+ // 2. YouTube Summary
343
+ else if (tool === 'yt-summary') {
344
+ const url = typeof rawArgs === 'object' && rawArgs?.url ? rawArgs.url : stringQuery
345
+ targetSetChatData((prev) => [
346
+ ...prev,
347
+ {
348
+ role: 'ai',
349
+ content: 'Menonton video youtube...',
350
+ isSummarizing: true,
351
+ youtubeLink: url
352
+ }
353
+ ])
354
+ const yData = await getYoutubeData(url)
355
+ resultString = await getYoutubeSummary(url, yData, currentSignal)
356
+ targetSetChatData((prev) => prev.filter((item) => !item.isSummarizing))
357
+ }
358
+ // 3. Music Control
359
+ else if (tool.startsWith('music')) {
360
+ const musicQuery =
361
+ typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
362
+ resultString = await handleMusic(tool, musicQuery, targetSetChatData)
363
+ }
364
+ // 4. Memory Vector Search
365
+ else if (tool === 'memory-search') {
366
+ const q = typeof rawArgs === 'object' && rawArgs?.query ? rawArgs.query : stringQuery
367
+ resultString = await executeMemorySearch(q)
368
+ }
369
+ // 5. Memory Management Tool
370
+ else if (tool === 'manage-memory') {
371
+ const memArgs = typeof rawArgs === 'object' && rawArgs !== null ? rawArgs : {}
372
+ const action = memArgs.action || 'insert'
373
+ const type = memArgs.type || 'profile'
374
+ const summary = memArgs.summary || ''
375
+ const detail = memArgs.detail || summary
376
+
377
+ let memContent = `[${getCurrentTimeInfo()}] ${detail || summary}`
378
+ const memoryData = {
379
+ type,
380
+ summary,
381
+ memory: memContent
382
+ }
383
+
384
+ // Orama Auto-Dedup check untuk profile / preference
385
+ if (action === 'insert' && (type === 'profile' || type === 'preference')) {
386
+ try {
387
+ const newVec = await generateVector(memContent)
388
+ if (newVec) {
389
+ const similarMemories = await searchMemoriesInOrama(memContent, newVec, 1, type)
390
+ if (similarMemories.length > 0 && similarMemories[0].score > 0.82) {
391
+ memoryData.id = similarMemories[0].id
392
+ await updateMemory(memoryData)
393
+ resultString = `Memori yang mirip ditemukan (ID: ${memoryData.id}). Berhasil diperbarui.`
394
+ return {
395
+ resultString,
396
+ rejected: false,
397
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
398
+ }
399
+ }
400
+ }
401
+ } catch (err) {
402
+ console.error('Error in Orama auto-dedup check:', err)
403
+ }
404
+ }
405
+
406
+ if (action === 'insert') {
407
+ await insertMemory(memoryData)
408
+ resultString = `Fakta baru berhasil disimpan ke memori jangka panjang: "${summary}"`
409
+ } else if (action === 'update') {
410
+ await updateMemory(memoryData)
411
+ resultString = `Memori berhasil diperbarui: "${summary}"`
412
+ } else if (action === 'delete') {
413
+ if (memArgs.id) {
414
+ await deleteMemory(memArgs.id)
415
+ resultString = `Memori ID ${memArgs.id} berhasil dihapus.`
416
+ } else {
417
+ resultString = `Gagal menghapus memori: ID memori tidak disertakan.`
418
+ }
419
+ }
420
+ }
421
+ // 6. Working Memory Update Tool
422
+ else if (tool === 'update-working-memory') {
423
+ const notes = typeof rawArgs === 'object' && rawArgs?.notes ? rawArgs.notes : stringQuery
424
+ if (context?.workspaceRoot && notes) {
425
+ await saveWorkspaceWorkingMemory(context.workspaceRoot, { notes })
426
+ resultString = `Catatan progres koding berhasil disimpan ke .mark/working-memory.json.`
427
+ } else {
428
+ resultString = `Working memory dicatat untuk sesi ini: ${notes}`
429
+ }
430
+ }
431
+ // 7. Speak (TTS)
432
+ else if (tool === 'speak') {
433
+ const textToSpeak =
434
+ typeof rawArgs === 'object' && rawArgs?.text ? rawArgs.text : stringQuery
435
+ if (textToSpeak && textToSpeak.trim() !== '') {
436
+ targetSetChatData((prev) => {
437
+ const filtered = prev.filter((item) => !item.isThinking)
438
+ return [
439
+ ...filtered,
440
+ { role: 'ai', content: `(Sedang berbicara) ${textToSpeak}`, isThinking: true }
441
+ ]
442
+ })
443
+ await playVoice(textToSpeak)
444
+ resultString = `Berhasil berbicara secara lisan: "${textToSpeak}"`
445
+ } else {
446
+ resultString = 'Gagal: teks yang mau diucapkan kosong.'
447
+ }
448
+ }
449
+ // 8. Screenshot ke Telegram
450
+ else if (tool === 'screenshot-to-tg') {
451
+ if (window.api && window.api.tgTakeScreenshot) {
452
+ const targetChatId = tgContext?.chatId || null
453
+ try {
454
+ const ssRes = await window.api.tgTakeScreenshot(targetChatId)
455
+ if (ssRes && ssRes.success === false) {
456
+ resultString = `Gagal mengirim screenshot ke Telegram: ${ssRes.error || 'Terjadi kesalahan'}`
457
+ } else {
458
+ resultString =
459
+ 'Screenshot layar PC berhasil diambil dan dikirimkan ke Telegram Admin.'
460
+ }
461
+ } catch (e) {
462
+ resultString = `Gagal mengirim screenshot ke Telegram: ${e.message}`
463
+ }
464
+ } else {
465
+ resultString = 'Gagal: Fitur Telegram Bot belum tersedia.'
466
+ }
467
+ }
468
+ // 9. Vision: Analyze Screen
469
+ else if (tool === 'analyze-screen') {
470
+ try {
471
+ const screens = await window.api.takeScreenshot()
472
+ const screenArray = Array.isArray(screens) ? screens : screens ? [screens] : []
473
+ if (screenArray.length > 0) {
474
+ targetSetChatData((prev) => [
475
+ ...prev.filter((item) => !item.isThinking),
476
+ { role: 'ai', content: 'Memproses Vision AI...', isThinking: true }
477
+ ])
478
+
479
+ const promptText =
480
+ (typeof rawArgs === 'object' && rawArgs?.prompt) ||
481
+ (typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
482
+ (typeof rawArgs === 'string' ? rawArgs : '') ||
483
+ 'Jelaskan apa yang kamu lihat di layar ini secara ringkas.'
484
+ ;('Jelaskan apa yang kamu lihat di layar ini secara ringkas, fokus pada jendela aplikasi, teks, dan status UI.')
485
+
486
+ const contentArray = [
487
+ {
488
+ type: 'text',
489
+ text: promptText
490
+ },
491
+ ...screenArray.map((scr) => ({
492
+ type: 'image_url',
493
+ image_url: { url: scr }
494
+ }))
495
+ ]
496
+
497
+ let textContent = ''
498
+ try {
499
+ const visionResponse = await fetchAI(
500
+ [{ role: 'user', content: contentArray }],
501
+ false,
502
+ {
503
+ signal: currentSignal,
504
+ isSmallTask: true
505
+ }
506
+ )
507
+ textContent =
508
+ typeof visionResponse === 'object' && visionResponse.content
509
+ ? visionResponse.content
510
+ : String(visionResponse)
511
+ } catch (vErr) {
512
+ textContent = `(Analisis teks awal dilewati: ${vErr.message})`
513
+ }
514
+
515
+ console.log(
516
+ `[Vision AI - analyze-screen] Hasil analisis (${screenArray.length} monitor):`,
517
+ textContent
518
+ )
519
+ resultString = `Hasil Analisis Layar (${screenArray.length} monitor):\n${textContent}`
520
+ return {
521
+ success: true,
522
+ resultString,
523
+ rejected: false,
524
+ imageUrls: screenArray,
525
+ previewUrl: screenArray[0],
526
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
527
+ }
528
+ } else {
529
+ resultString = 'Gagal mengambil screenshot layar untuk analisis.'
530
+ }
531
+ } catch (e) {
532
+ resultString = `Gagal memproses analisis layar: ${e.message}`
533
+ }
534
+ }
535
+ // 10. Vision: Camera Look
536
+ else if (tool === 'camera-look') {
537
+ try {
538
+ if (config[0]?.cameraEnabled === false) {
539
+ resultString =
540
+ 'Fitur kamera dimatikan di pengaturan. Beri tahu user untuk mengaktifkannya.'
541
+ } else if (!requestCameraCapture) {
542
+ resultString = 'Internal Error: Callback requestCameraCapture tidak tersedia.'
543
+ } else {
544
+ targetSetChatData((prev) => [
545
+ ...prev.filter((item) => !item.isThinking),
546
+ { role: 'ai', content: 'Mengakses kamera...', isThinking: true }
547
+ ])
548
+
549
+ const cameraFrame = await requestCameraCapture({
550
+ isAutonomous: isAutonomous,
551
+ deviceId: config[0]?.cameraDeviceId !== 'default' ? config[0]?.cameraDeviceId : null
552
+ })
553
+
554
+ if (cameraFrame) {
555
+ targetSetChatData((prev) => [
556
+ ...prev.filter((item) => !item.isThinking),
557
+ { role: 'ai', content: 'Menganalisis hasil kamera...', isThinking: true }
558
+ ])
559
+
560
+ const promptText =
561
+ (typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
562
+ (typeof rawArgs === 'string' ? rawArgs : '') ||
563
+ 'Jelaskan dengan detail apa yang terlihat dari kamera ini.'
564
+
565
+ const contentArray = [
566
+ {
567
+ type: 'text',
568
+ text: promptText
569
+ },
570
+ { type: 'image_url', image_url: { url: cameraFrame } }
571
+ ]
572
+
573
+ let textContent = ''
574
+ try {
575
+ const visionResponse = await fetchAI(
576
+ [{ role: 'user', content: contentArray }],
577
+ false,
578
+ { signal: currentSignal, isSmallTask: true }
579
+ )
580
+ textContent =
581
+ typeof visionResponse === 'object' && visionResponse.content
582
+ ? visionResponse.content
583
+ : String(visionResponse)
584
+ } catch (vErr) {
585
+ textContent = `(Analisis teks awal dilewati: ${vErr.message})`
586
+ }
587
+
588
+ console.log(`[Vision AI - camera-look] Hasil analisis:`, textContent)
589
+ resultString = `Hasil Analisis Kamera:\n${textContent}`
590
+ return {
591
+ success: true,
592
+ resultString,
593
+ rejected: false,
594
+ imageUrls: [cameraFrame],
595
+ previewUrl: cameraFrame,
596
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
597
+ }
598
+ } else {
599
+ resultString = 'Gagal mengambil gambar dari kamera.'
600
+ }
601
+ }
602
+ } catch (e) {
603
+ resultString = `Gagal memproses kamera: ${e.message}`
604
+ }
605
+ }
606
+ // 11. Built-in Native Tools & Sub-Agent Orchestration
607
+ else if (checkTools(tool)) {
608
+ const approvalCheck = await window.api.checkToolApproval(tool, rawArgs)
609
+
610
+ if (approvalCheck.needsApproval && requestApproval) {
611
+ const userApproved = await requestApproval(approvalCheck.message, tool, rawArgs, {
612
+ sessionId: activeSessionNum,
613
+ targetSetChatData
614
+ })
615
+ if (!userApproved) {
616
+ resultString = `[DITOLAK] User menolak eksekusi "${tool}". Cari cara lain atau tanyakan user.`
617
+ return {
618
+ resultString,
619
+ rejected: true,
620
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
621
+ }
622
+ }
623
+ }
624
+
625
+ const executionResult = await executeAgentTool({
626
+ tool,
627
+ rawArgs,
628
+ config,
629
+ context,
630
+ activeSessionNum,
631
+ activeTopic,
632
+ userInput,
633
+ durableTask,
634
+ durableActiveStep,
635
+ agenticProcessId,
636
+ targetPushProcess,
637
+ targetSetChatData,
638
+ currentSignal,
639
+ abortControllerRef,
640
+ getCurrentTimeInfo,
641
+ activeTaskObjectiveRef
642
+ })
643
+
644
+ const res = executionResult.res
645
+
646
+ if (res && res.success) {
647
+ if (res.data !== undefined) {
648
+ resultString = typeof res.data === 'string' ? res.data : JSON.stringify(res.data)
649
+ } else if (res.output !== undefined) {
650
+ resultString = typeof res.output === 'string' ? res.output : JSON.stringify(res.output)
651
+ } else if (res.result !== undefined) {
652
+ resultString = typeof res.result === 'string' ? res.result : JSON.stringify(res.result)
653
+ } else if (res.content !== undefined) {
654
+ resultString =
655
+ typeof res.content === 'string' ? res.content : JSON.stringify(res.content)
656
+ } else if (res.contents !== undefined) {
657
+ resultString =
658
+ typeof res.contents === 'string' ? res.contents : JSON.stringify(res.contents)
659
+ } else {
660
+ resultString = res.message || 'Success'
661
+ }
662
+
663
+ // Pemotongan isi dokumen jika terlalu panjang
664
+ if (tool === 'read-document') {
665
+ let fullText =
666
+ typeof res.data === 'object' && res.data !== null
667
+ ? res.data.content || ''
668
+ : String(res.data || resultString || '')
669
+ if (fullText && fullText.length > 2500) {
670
+ resultString = `${fullText.slice(0, 2500)}\n\n[DOKUMEN DIPOTONG (Total: ${fullText.length} karakter). Gunakan read-document dengan keyword untuk pencarian spesifik]`
671
+ }
672
+ }
673
+
674
+ // Analisis visual untuk read-image
675
+ if (tool === 'read-image' && res.dataUrl) {
676
+ const promptText =
677
+ (typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
678
+ (typeof rawArgs === 'string' ? rawArgs : '') ||
679
+ 'Jelaskan apa yang kamu lihat pada gambar ini secara rinci.'
680
+ try {
681
+ targetSetChatData((prev) => [
682
+ ...prev.filter((item) => !item.isThinking),
683
+ {
684
+ role: 'ai',
685
+ content: `Menganalisis gambar ${res.filename || ''}...`,
686
+ isThinking: true
687
+ }
688
+ ])
689
+ const visionResponse = await fetchAI(
690
+ [
691
+ {
692
+ role: 'user',
693
+ content: [
694
+ { type: 'text', text: promptText },
695
+ { type: 'image_url', image_url: { url: res.dataUrl } }
696
+ ]
697
+ }
698
+ ],
699
+ false,
700
+ { signal: currentSignal, isSmallTask: true }
701
+ )
702
+ const textContent =
703
+ typeof visionResponse === 'object' && visionResponse.content
704
+ ? visionResponse.content
705
+ : String(visionResponse)
706
+ resultString = `[Vision AI - read-image] Analisis berkas '${res.filename || 'gambar'}':\n${textContent}`
707
+ } catch (vErr) {
708
+ resultString = `Berkas gambar '${res.filename || 'gambar'}' berhasil dibaca. (Analisis teks awal dilewati: ${vErr.message}). Gambar visual diteruskan ke observasi.`
709
+ }
710
+ }
711
+
712
+ // Analisis visual untuk browser-screenshot jika query disertakan
713
+ if (tool === 'browser-screenshot' && res.dataUrl && (rawArgs?.query || res.query)) {
714
+ const promptText =
715
+ (typeof rawArgs === 'object' && (rawArgs?.query || rawArgs?.prompt)) ||
716
+ res.query ||
717
+ 'Jelaskan tampilan visual halaman web ini.'
718
+ try {
719
+ targetSetChatData((prev) => [
720
+ ...prev.filter((item) => !item.isThinking),
721
+ { role: 'ai', content: 'Menganalisis tampilan web...', isThinking: true }
722
+ ])
723
+ const visionResponse = await fetchAI(
724
+ [
725
+ {
726
+ role: 'user',
727
+ content: [
728
+ { type: 'text', text: promptText },
729
+ { type: 'image_url', image_url: { url: res.dataUrl } }
730
+ ]
731
+ }
732
+ ],
733
+ false,
734
+ { signal: currentSignal, isSmallTask: true }
735
+ )
736
+ const textContent =
737
+ typeof visionResponse === 'object' && visionResponse.content
738
+ ? visionResponse.content
739
+ : String(visionResponse)
740
+ resultString = `[Vision AI - browser-screenshot] ${res.message || 'Screenshot berhasil diambil.'}\nHasil analisis:\n${textContent}`
741
+ } catch (vErr) {
742
+ resultString = `${res.message || 'Screenshot berhasil diambil.'} (Analisis teks awal dilewati: ${vErr.message}). Gambar visual diteruskan ke observasi.`
743
+ }
744
+ }
745
+ } else {
746
+ resultString = `[ERROR] ${tool} gagal: ${(res && (res.message || res.error)) || 'Unknown error'}`
747
+ }
748
+
749
+ const toolImageUrls = res?.dataUrl ? [res.dataUrl] : null
750
+ const toolPreviewUrl = res?.dataUrl || null
751
+
752
+ return {
753
+ res,
754
+ success: Boolean(res?.success),
755
+ resultString,
756
+ rejected: false,
757
+ imageUrls: toolImageUrls,
758
+ previewUrl: toolPreviewUrl,
759
+ toolExecution: { action: tool, query: stringQuery, result: resultString },
760
+ loadedGroup: res?.loaded_group || null,
761
+ durableTask: executionResult?.durableTask || durableTask || null,
762
+ durableActiveStep:
763
+ executionResult?.durableActiveStep !== undefined
764
+ ? executionResult.durableActiveStep
765
+ : durableActiveStep || null
766
+ }
767
+ }
768
+ // 12. Dynamic Plugin Execution
769
+ else {
770
+ targetPushProcess({
771
+ id: pluginProcessId,
772
+ type: 'plugin-execution',
773
+ status: 'active',
774
+ data: { action: tool, query: stringQuery }
775
+ })
776
+
777
+ const pluginPromise = window.api.executePlugin(tool, rawArgs)
778
+ const abortPromise = new Promise((_, reject) => {
779
+ const onAbort = () => reject(new Error('AbortError'))
780
+ if (currentSignal?.aborted) return onAbort()
781
+ currentSignal?.addEventListener('abort', onAbort)
782
+ })
783
+ const res = await Promise.race([pluginPromise, abortPromise])
784
+
785
+ resultString = res.success
786
+ ? typeof res.data === 'string'
787
+ ? res.data
788
+ : JSON.stringify(res.data)
789
+ : `[ERROR] Plugin ${tool} gagal: ${res.error}`
790
+
791
+ targetPushProcess({
792
+ id: pluginProcessId,
793
+ type: 'plugin-execution',
794
+ status: 'done',
795
+ data: { action: tool, query: stringQuery, result: resultString }
796
+ })
797
+
798
+ return {
799
+ res,
800
+ success: Boolean(res?.success),
801
+ resultString,
802
+ rejected: false,
803
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
804
+ }
805
+ }
806
+ } catch (toolError) {
807
+ if (toolError.name === 'AbortError' || toolError.message?.includes('AbortError')) {
808
+ throw toolError
809
+ }
810
+ resultString = `[ERROR] Tool ${tool} crash: ${toolError.message}`
811
+ }
812
+
813
+ return {
814
+ success: !resultString.startsWith('[ERROR]'),
815
+ resultString,
816
+ rejected: false,
817
+ toolExecution: { action: tool, query: stringQuery, result: resultString }
818
+ }
819
+ }
820
+
821
+ // ==========================================================================
822
+ // CORE HANDLER: handlePlanningCommand (Native ReAct Loop & Streaming Engine)
823
+ // ==========================================================================
824
+ const handlePlanningCommand = async (
825
+ userInput,
826
+ tgContextOrOptions = null,
827
+ isAutonomous = false,
828
+ autonomousInitialMessage = null,
829
+ options = {},
830
+ isSystem = false
831
+ ) => {
832
+ let tgContext = tgContextOrOptions
833
+ let opts = options || {}
834
+
835
+ // Flexible options detection
836
+ if (
837
+ tgContextOrOptions &&
838
+ typeof tgContextOrOptions === 'object' &&
839
+ !tgContextOrOptions.chatId &&
840
+ !tgContextOrOptions.from
841
+ ) {
842
+ opts = tgContextOrOptions
843
+ opts = { ...opts, ...tgContextOrOptions }
844
+ tgContext = null
845
+ } else if (
846
+ autonomousInitialMessage &&
847
+ typeof autonomousInitialMessage === 'object' &&
848
+ (autonomousInitialMessage.sessionId ||
849
+ autonomousInitialMessage.customChatData ||
850
+ autonomousInitialMessage.customSetChatData ||
851
+ autonomousInitialMessage.onSaveSession)
852
+ ) {
853
+ opts = { ...opts, ...autonomousInitialMessage }
854
+ autonomousInitialMessage = null
855
+ }
856
+
857
+ // ------------------------------------------------------------------------
858
+ // FASE 1: VALIDASI INPUT & PER-SESSION LOCKING
859
+ // ------------------------------------------------------------------------
860
+ const activeSessionNum = opts.sessionId
861
+ ? !isNaN(Number(opts.sessionId))
862
+ ? Number(opts.sessionId)
863
+ : String(opts.sessionId)
864
+ : 1
865
+ activeRunningSessionIdRef.current = activeSessionNum
866
+
867
+ if (activeSessionsRef.current.has(activeSessionNum)) {
868
+ handleIntervention(userInput, activeSessionNum, { displayPrompt: opts.displayPrompt })
869
+ return
870
+ }
871
+
872
+ const sessionAbortController = new AbortController()
873
+ const sessionRecord = {
874
+ abortController: sessionAbortController,
875
+ startTime: Date.now(),
876
+ prompt: userInput,
877
+ interventions: []
878
+ }
879
+ activeSessionsRef.current.set(activeSessionNum, sessionRecord)
880
+
881
+ if (abortControllerRef) {
882
+ abortControllerRef.current = sessionAbortController
883
+ }
884
+
885
+ if (addRunningSessionId) addRunningSessionId(activeSessionNum)
886
+ setIsAgentBusy(true)
887
+
888
+ let finalIsSpeak = opts.forceSpeak !== undefined ? opts.forceSpeak : isSpeak
889
+ if (userInput && typeof userInput === 'string') {
890
+ if (userInput.startsWith('<mic>') || userInput.startsWith('(Mikrofon)')) {
891
+ finalIsSpeak = true
892
+ } else if (!isAutonomous && !isSystem) {
893
+ finalIsSpeak = false
894
+ }
895
+ }
896
+
897
+ // Reset Speech Queue sebelum memulai turn baru
898
+ if (finalIsSpeak) {
899
+ speechQueue.reset()
900
+ }
901
+
902
+ if (!userInput) {
903
+ activeSessionsRef.current.delete(activeSessionNum)
904
+ if (removeRunningSessionId) removeRunningSessionId(activeSessionNum)
905
+ return
906
+ }
907
+
908
+ if (!tgContext && !isAutonomous) {
909
+ if (activeSessionNum === 1) {
910
+ setIsLoading(true)
911
+ }
912
+ if (!isSystem && !opts.customSetChatData) {
913
+ lastUserPromptRef.current = userInput
914
+ setMessage('')
915
+ }
916
+ }
917
+
918
+ const timestampStr = getCurrentTimeInfo()
919
+
920
+ // ------------------------------------------------------------------------
921
+ // FASE 2: FORMATTING PROMPT & VISION PAYLOAD
922
+ // ------------------------------------------------------------------------
923
+ let finalContent = userInput
924
+ if (userInput.startsWith('/')) {
925
+ const skillName = userInput.slice(1).split(' ')[0].trim()
926
+ try {
927
+ const skillData = await window.api.readSkill(skillName)
928
+ const skillContent = typeof skillData === 'string' ? skillData : skillData?.content
929
+ if (skillContent) {
930
+ finalContent = `[SYSTEM INSTRUCTION - SKILL ACTIVATED]: Kamu sekarang harus bertindak dan mengikuti seluruh instruksi dalam dokumen skill berikut ini secara ketat:\n\n=== SKILL: ${skillName} ===\n${skillContent}\n====================\n\nInstruksi dari user: ${userInput.replace('/' + skillName, '').trim() || 'Jalankan skill ini sekarang!'}`
931
+ } else {
932
+ finalContent = `Skill "${skillName}" tidak ditemukan di direktori Mark Skills.`
933
+ }
934
+ } catch (err) {
935
+ console.error('Error loading skill:', err)
936
+ }
937
+ } else if (isSystem) {
938
+ finalContent = `[SYSTEM INSTRUCTION]: ${userInput}`
939
+ }
940
+
941
+ if (isAutonomous) {
942
+ finalContent = `[SISTEM INTERNAL - INISIATIF OTONOM]: Otak bawah sadarmu berinisiatif untuk melakukan tindakan berikut: "${userInput}". LAKUKAN TUGAS INI! Bicaralah seolah-olah kamu yang memiliki inisiatif itu sendiri tanpa disuruh. PENTING: DILARANG KERAS menggunakan tool 'os-*' untuk interaksi PC secara otonom! Respons "answer"-mu HARUS SANGAT SINGKAT (1-2 kalimat pendek).`
943
+ }
944
+
945
+ let imageVisionPayloads = []
946
+
947
+ // 1. Ekstraksi langsung dari opts.attachedFiles jika tersedia
948
+ if (Array.isArray(opts.attachedFiles) && opts.attachedFiles.length > 0) {
949
+ for (const f of opts.attachedFiles) {
950
+ const isImg =
951
+ (f.type && f.type.startsWith('image/')) ||
952
+ (f.path && isImagePath(f.path)) ||
953
+ (f.name && isImagePath(f.name))
954
+ if (isImg) {
955
+ const imgUrl = f.previewUrl || (f.path ? await convertFilePathToBase64(f.path) : null)
956
+ if (imgUrl) {
957
+ imageVisionPayloads.push({ type: 'image_url', image_url: { url: imgUrl } })
958
+ }
959
+ }
960
+ }
961
+ }
962
+
963
+ // 2. Fallback parsing dari teks [FILE TERLAMPIR]:
964
+ if (imageVisionPayloads.length === 0 && userInput.includes('[FILE TERLAMPIR]:')) {
965
+ const matches = userInput.match(/"([^"]+)"/g)
966
+ if (matches && matches.length > 0) {
967
+ const paths = matches.map((m) => m.replace(/^"|"$/g, ''))
968
+ for (const p of paths) {
969
+ if (isImagePath(p)) {
970
+ const b64 = await convertFilePathToBase64(p)
971
+ if (b64) {
972
+ imageVisionPayloads.push({ type: 'image_url', image_url: { url: b64 } })
973
+ }
974
+ }
975
+ }
976
+ }
977
+ }
978
+
979
+ let payloadContent = finalContent
980
+ if (imageVisionPayloads.length > 0) {
981
+ payloadContent = [{ type: 'text', text: finalContent }, ...imageVisionPayloads]
982
+ }
983
+
984
+ let uiDisplayContent = opts.displayPrompt !== undefined ? opts.displayPrompt : userInput
985
+ if (
986
+ typeof uiDisplayContent === 'string' &&
987
+ uiDisplayContent.includes('=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===')
988
+ ) {
989
+ const cleanBeforeSkill = uiDisplayContent
990
+ .split('=== SYSTEM INSTRUCTION: SKILL DIAKTIFKAN ===')[0]
991
+ .trim()
992
+ uiDisplayContent = cleanBeforeSkill || 'Jalankan Skill'
993
+ }
994
+
995
+ // Jika ada gambar terlampir dan user tidak menulis teks manual, kosongkan teks display
996
+ if (imageVisionPayloads.length > 0 && typeof opts.displayPrompt === 'string') {
997
+ uiDisplayContent = opts.displayPrompt.trim()
998
+ }
999
+
1000
+ let finalUserMessageContent = uiDisplayContent
1001
+ if (imageVisionPayloads.length > 0) {
1002
+ const textPart = typeof uiDisplayContent === 'string' ? uiDisplayContent.trim() : ''
1003
+ finalUserMessageContent = [
1004
+ ...(textPart ? [{ type: 'text', text: textPart }] : []),
1005
+ ...imageVisionPayloads
1006
+ ]
1007
+ }
1008
+
1009
+ const userMessage = opts.customUserMessage
1010
+ ? {
1011
+ ...opts.customUserMessage,
1012
+ timestamp: timestampStr,
1013
+ created_at: Date.now()
1014
+ }
1015
+ : {
1016
+ role: 'user',
1017
+ content: finalUserMessageContent,
1018
+ timestamp: timestampStr,
1019
+ created_at: Date.now(),
1020
+ source: tgContext ? 'telegram' : 'pc',
1021
+ sender:
1022
+ tgContext?.from?.first_name ||
1023
+ tgContext?.from?.username ||
1024
+ (tgContext ? 'Telegram Admin' : undefined)
1025
+ }
1026
+
1027
+ // Penyiapan data sesi terisolasi (Database-First Persistent Pipeline)
1028
+ let inMemorySessionData = []
1029
+ if (activeSessionNum !== 1) {
1030
+ if (Array.isArray(opts.customChatData) && opts.customChatData.length > 0) {
1031
+ inMemorySessionData = [...opts.customChatData]
1032
+ } else {
1033
+ try {
1034
+ const existing = await getChatData(activeSessionNum)
1035
+ if (existing && Array.isArray(existing)) {
1036
+ inMemorySessionData = [...existing]
1037
+ }
1038
+ } catch (e) {}
1039
+ }
1040
+ }
1041
+
1042
+ // Ambil workspaceRoot dari database jika belum disertakan di opts
1043
+ if (!opts.workspaceRoot) {
1044
+ try {
1045
+ const sessionRecord = await db.sessions.get(activeSessionNum)
1046
+ if (sessionRecord?.workspaceRoot) {
1047
+ opts.workspaceRoot = sessionRecord.workspaceRoot
1048
+ }
1049
+ } catch (e) {}
1050
+ }
1051
+
1052
+ const targetSetChatData = (updater) => {
1053
+ if (activeSessionNum !== 1) {
1054
+ const next = typeof updater === 'function' ? updater(inMemorySessionData) : updater
1055
+ inMemorySessionData = next
1056
+
1057
+ // 1. Direct persistent DB write
1058
+ saveSession(activeSessionNum, next).catch((err) => {
1059
+ console.warn(`[useMarkPlan] Gagal auto-save session ${activeSessionNum}:`, err)
1060
+ })
1061
+
1062
+ // 2. Broadcast reactive event to UI
1063
+ window.dispatchEvent(
1064
+ new CustomEvent('session-updated', {
1065
+ detail: { sessionId: activeSessionNum, data: next }
1066
+ })
1067
+ )
1068
+ } else {
1069
+ setChatData((prev) => {
1070
+ const next = typeof updater === 'function' ? updater(prev) : updater
1071
+ chatDataRef.current = next
1072
+ return next
1073
+ })
1074
+ }
1075
+ }
1076
+
1077
+ activeSessionUpdatersRef.current.set(activeSessionNum, targetSetChatData)
1078
+
1079
+ // ------------------------------------------------------------------------
1080
+ // FASE 3: PENYIAPAN HISTORY CHAT & RETRIEVAL KONTEKS
1081
+ // ------------------------------------------------------------------------
1082
+ const sourceChatData =
1083
+ activeSessionNum === 1 ? chatDataRef.current || chatData : inMemorySessionData
1084
+ const validHistory = sourceChatData.filter(
1085
+ (m) =>
1086
+ m &&
1087
+ !m.isThinking &&
1088
+ !m.isSearching &&
1089
+ !m.isSummarizing &&
1090
+ m.role !== 'command' &&
1091
+ m.role !== 'system'
1092
+ )
1093
+
1094
+ if (!isAutonomous && !isSystem) {
1095
+ targetSetChatData((prev) => [
1096
+ ...prev,
1097
+ userMessage,
1098
+ {
1099
+ role: 'ai',
1100
+ content: 'Menganalisis instruksi...',
1101
+ isThinking: true,
1102
+ mood: 'neutral'
1103
+ }
1104
+ ])
1105
+ }
1106
+
1107
+ const agenticProcessId = `agentic-${Date.now()}`
1108
+ let durableTaskForRecovery = null
1109
+ let execSteps = [{ task: 'Menganalisis Konteks...' }]
1110
+ let accumulatedThoughts = []
1111
+ let executedToolsList = []
1112
+ let currentInFlightTool = null
1113
+ let currentTurnReasoning = ''
1114
+ let finalContentAccumulator = ''
1115
+
1116
+ try {
1117
+ let durableTask = null
1118
+ let durableActiveStep = null
1119
+
1120
+ const allMemory = await getAllMemory()
1121
+ let searchQuery = userInput
1122
+ if (validHistory.length > 0) {
1123
+ const lastMsg = validHistory[validHistory.length - 1]
1124
+ if (lastMsg && (lastMsg.role === 'assistant' || lastMsg.role === 'ai') && lastMsg.content) {
1125
+ let lastAiText =
1126
+ typeof lastMsg.content === 'string' ? lastMsg.content : JSON.stringify(lastMsg.content)
1127
+ if (lastAiText.length > 600) {
1128
+ lastAiText = lastAiText.substring(0, 300) + ' ... ' + lastAiText.slice(-300)
1129
+ }
1130
+ searchQuery = `Konteks obrolan sebelumnya: "${lastAiText}". Pertanyaan user saat ini: "${userInput}"`
1131
+ }
1132
+ }
1133
+
1134
+ const contextPromise = getUnifiedContext(searchQuery, allMemory)
1135
+ const abortPromise = new Promise((_, reject) => {
1136
+ const onAbort = () => reject(new Error('AbortError'))
1137
+ if (sessionAbortController.signal.aborted) return onAbort()
1138
+ sessionAbortController.signal.addEventListener('abort', onAbort)
1139
+ })
1140
+ const unifiedContext = await Promise.race([contextPromise, abortPromise])
1141
+
1142
+ let contextMsgStr = ''
1143
+ if (tgContext)
1144
+ contextMsgStr += `Permintaan ini berasal dari Telegram (Chat ID: ${tgContext.chatId}).\n`
1145
+ if (isSystem)
1146
+ contextMsgStr += `[SYSTEM INSTRUCTION]: Pesan ini adalah instruksi internal sistem.\n`
1147
+ if (isAutonomous) {
1148
+ contextMsgStr += `[AWARENESS MODE]: Ini adalah pemikiran autonom-mu sendiri. Buka topik secara proaktif.\n`
1149
+ }
1150
+ if (currentMusicTrack && currentMusicTrack.title) {
1151
+ contextMsgStr += `[STATUS SISTEM]: Sedang memutar "${currentMusicTrack.title}" oleh ${currentMusicTrack.artist}.\n`
1152
+ }
1153
+
1154
+ // Inject 5 aktivitas OS terakhir dari window tracker
1155
+ try {
1156
+ const activityBuffer = await window.api.getActivityBuffer()
1157
+ if (activityBuffer && activityBuffer.length > 0) {
1158
+ const recent = activityBuffer.slice(-5)
1159
+ const activitySummary = recent
1160
+ .map((a) => `[${a.time || a.timestamp}] ${a.app}${a.title ? ` — ${a.title}` : ''}`)
1161
+ .join('\n')
1162
+ contextMsgStr += `[AKTIVITAS PC USER (terakhir)]\n${activitySummary}\n`
1163
+ }
1164
+ } catch (_) {}
1165
+
1166
+ // Ambil daftar sub-agent yang tersedia
1167
+ let existingSubagents = ''
1168
+ try {
1169
+ const { subagentStore } = await import('../../api/subagent/subagentStore.js')
1170
+ const allSubs = await subagentStore.listSubagents()
1171
+ if (allSubs && allSubs.length > 0) {
1172
+ existingSubagents = allSubs
1173
+ .slice(0, 10)
1174
+ .map(
1175
+ (s) =>
1176
+ `- [ID: ${s.id}] "${s.name}" (${s.role}) | Status: ${s.status} | Turns: ${s.turnCount || 0} | Goal: "${s.goal}"`
1177
+ )
1178
+ .join('\n')
1179
+ }
1180
+ } catch (e) {}
1181
+
1182
+ // Susun System Prompt Mark V5
1183
+ let systemTelemetry = null
1184
+ try {
1185
+ if (window.api && typeof window.api.getSystemTelemetry === 'function') {
1186
+ systemTelemetry = await window.api.getSystemTelemetry()
1187
+ }
1188
+ } catch (_) {}
1189
+
1190
+ const systemPrompt = await buildPlanningSystemPrompt(
1191
+ userInput,
1192
+ {
1193
+ ...opts,
1194
+ tgContext,
1195
+ currentMusicTrack,
1196
+ systemTelemetry,
1197
+ activeTaskObjective: activeTaskObjectiveRef.current,
1198
+ existingSubagents
1199
+ },
1200
+ unifiedContext,
1201
+ contextMsgStr
1202
+ )
1203
+
1204
+ // ------------------------------------------------------------------------
1205
+ // FASE 3.5: PRE-FLIGHT CONTEXT COMPACTION (Ambang Batas 256.000 Tokens)
1206
+ // ------------------------------------------------------------------------
1207
+ let activeSessionCompact = null
1208
+ try {
1209
+ activeSessionCompact = await getSessionCompact(String(activeSessionNum))
1210
+ } catch (_) {}
1211
+
1212
+ const activeSummaryBlock =
1213
+ activeSessionCompact?.summaryBlock || activeSessionCompact?.summary_block || ''
1214
+ const activeLastCompactedId =
1215
+ activeSessionCompact?.lastCompactedMessageId ||
1216
+ activeSessionCompact?.last_compacted_message_id ||
1217
+ null
1218
+
1219
+ const currentUserMsg = { ...userMessage, content: payloadContent }
1220
+ let effectiveSourceMessages = [...sourceChatData, currentUserMsg]
1221
+
1222
+ currentUserMsg.tokens = calculateMessageTokens(currentUserMsg)
1223
+ const currentEstimatedTokens = calculateSessionTokens(
1224
+ effectiveSourceMessages,
1225
+ activeSummaryBlock,
1226
+ activeLastCompactedId,
1227
+ systemPrompt,
1228
+ activeSessionCompact?.lastCompactedAt || activeSessionCompact?.last_compacted_at || null
1229
+ )
1230
+
1231
+ // Bypass proses kompaksi berat jika instruksi internal (greeting sistem / awareness autonomous / disableTools)
1232
+ const isInternalTurn = Boolean(
1233
+ isSystem || isAutonomous || opts.skipCompaction || opts.disableTools
1234
+ )
1235
+
1236
+ // Layer 1: Gateway Session Hygiene (Hermes 85% safety net)
1237
+ const gatewayHygieneTriggerTokens = MAX_CONTEXT_TOKENS * GATEWAY_HYGIENE_THRESHOLD
1238
+ if (!isInternalTurn && currentEstimatedTokens >= gatewayHygieneTriggerTokens) {
1239
+ const compactBannerId = `compact-banner-${Date.now()}`
1240
+ targetSetChatData((prev) => [
1241
+ ...prev,
1242
+ {
1243
+ id: compactBannerId,
1244
+ role: 'system',
1245
+ isCompacting: true,
1246
+ compactProgress: 'Memangkas log tool di memori...'
1247
+ }
1248
+ ])
1249
+
1250
+ try {
1251
+ const compactionResult = await executeSessionCompaction({
1252
+ sessionId: String(activeSessionNum),
1253
+ messages: effectiveSourceMessages,
1254
+ activeConfig: config[0] || {},
1255
+ systemPrompt,
1256
+ onProgress: (prog) => {
1257
+ targetSetChatData((prev) =>
1258
+ prev.map((item) =>
1259
+ item.id === compactBannerId ? { ...item, compactProgress: prog.text } : item
1260
+ )
1261
+ )
1262
+ }
1263
+ })
1264
+
1265
+ if (compactionResult?.isCompacted) {
1266
+ if (compactionResult.compactedMessages) {
1267
+ effectiveSourceMessages = compactionResult.compactedMessages
1268
+ // Sinkronkan riwayat pesan terpangkas ke state UI & persistent storage
1269
+ targetSetChatData(compactionResult.compactedMessages)
1270
+ }
1271
+ if (compactionResult.newSummaryBlock && compactionResult.lastCompactedMessageId) {
1272
+ activeSessionCompact = {
1273
+ summaryBlock: compactionResult.newSummaryBlock,
1274
+ lastCompactedMessageId: compactionResult.lastCompactedMessageId,
1275
+ lastCompactedAt: Date.now()
1276
+ }
1277
+ window.dispatchEvent(
1278
+ new CustomEvent('session-compact-updated', {
1279
+ detail: {
1280
+ sessionId: String(activeSessionNum),
1281
+ lastCompactedMessageId: compactionResult.lastCompactedMessageId,
1282
+ summaryBlock: compactionResult.newSummaryBlock
1283
+ }
1284
+ })
1285
+ )
1286
+ }
1287
+
1288
+ // Segera update indikator context-tracker agar gauge langsung berwarna hijau
1289
+ const activeTokensAfterCompact = Number(
1290
+ compactionResult.currentTokens || compactionResult.currentChars || 0
1291
+ )
1292
+ window.dispatchEvent(
1293
+ new CustomEvent('context-tracker-updated', {
1294
+ detail: {
1295
+ sessionId: String(activeSessionNum),
1296
+ currentTokens: activeTokensAfterCompact,
1297
+ maxTokens: MAX_CONTEXT_TOKENS,
1298
+ percentage: Math.min(100, (activeTokensAfterCompact / MAX_CONTEXT_TOKENS) * 100),
1299
+ currentChars: activeTokensAfterCompact,
1300
+ maxChars: MAX_CONTEXT_TOKENS,
1301
+ lastCompactedAt: activeSessionCompact?.lastCompactedAt || Date.now()
1302
+ }
1303
+ })
1304
+ )
1305
+ }
1306
+ } catch (compactErr) {
1307
+ console.error('[useMarkPlan] Gagal context compaction:', compactErr)
1308
+ } finally {
1309
+ targetSetChatData((prev) => (prev || []).filter((item) => item.id !== compactBannerId))
1310
+ }
1311
+ }
1312
+
1313
+ // ------------------------------------------------------------------------
1314
+ // FASE 4: AGENTIC REACT LOOP (Native Function Calling + SSE Token Stream)
1315
+ // ------------------------------------------------------------------------
1316
+ let loopMessages = []
1317
+
1318
+ if (isSystem) {
1319
+ // GREETING BOOT SEQUENCE: Sapaan awal startup hanya butuh systemPrompt + 1-2 pesan terakhir
1320
+ // Mencegah ledakan 1M+ token dari akumulasi ratusan riwayat masa lalu di database.
1321
+ const recentHistory = sourceChatData
1322
+ .filter(
1323
+ (m) =>
1324
+ m &&
1325
+ !m.isThinking &&
1326
+ !m.isSearching &&
1327
+ !m.isSummarizing &&
1328
+ m.role !== 'command' &&
1329
+ m.role !== 'system'
1330
+ )
1331
+ .slice(-2)
1332
+
1333
+ loopMessages = [
1334
+ { role: 'system', content: systemPrompt },
1335
+ ...recentHistory.map((m) => {
1336
+ const role = m.role === 'ai' || m.role === 'planSteps' ? 'assistant' : m.role
1337
+ let content = m.content || ''
1338
+ if (role === 'assistant' && m.mood && !/<mark\b/i.test(content)) {
1339
+ content = `<mark mood="${m.mood}" done="true" /> ${content}`
1340
+ }
1341
+ return { role, content, mood: m.mood || undefined }
1342
+ }),
1343
+ { role: 'user', content: payloadContent }
1344
+ ]
1345
+ } else if (isAutonomous) {
1346
+ // AWARENESS PROAKTIF: Cukup 3-4 pesan riwayat obrolan terkini
1347
+ const recentHistory = sourceChatData
1348
+ .filter(
1349
+ (m) =>
1350
+ m &&
1351
+ !m.isThinking &&
1352
+ !m.isSearching &&
1353
+ !m.isSummarizing &&
1354
+ m.role !== 'command' &&
1355
+ m.role !== 'system'
1356
+ )
1357
+ .slice(-4)
1358
+
1359
+ loopMessages = [
1360
+ { role: 'system', content: systemPrompt },
1361
+ ...recentHistory.map((m) => {
1362
+ const role = m.role === 'ai' || m.role === 'planSteps' ? 'assistant' : m.role
1363
+ let content = m.content || ''
1364
+ if (role === 'assistant' && m.mood && !/<mark\b/i.test(content)) {
1365
+ content = `<mark mood="${m.mood}" done="true" /> ${content}`
1366
+ }
1367
+ return { role, content, mood: m.mood || undefined }
1368
+ }),
1369
+ { role: 'user', content: payloadContent }
1370
+ ]
1371
+ } else {
1372
+ // TURN CHAT/CODING NORMAL:
1373
+ // Seluruh riwayat pesan & log tool dikirim 100% UTUH tanpa batasan turn (maxTurns)
1374
+ // selama masih berada dalam kapasitas 256.000 tokens (MAX_CONTEXT_TOKENS).
1375
+ loopMessages = assembleCompactedPayload({
1376
+ messages: effectiveSourceMessages,
1377
+ sessionCompact: activeSessionCompact,
1378
+ systemPrompt
1379
+ })
1380
+ }
1381
+
1382
+ let isDone = false
1383
+ let stepCount = 0
1384
+ executedToolsList = []
1385
+ sessionRecord.executedToolsList = executedToolsList
1386
+ let lastToolExecution = null
1387
+ accumulatedThoughts = []
1388
+ let currentActiveMood = 'neutral'
1389
+ finalContentAccumulator = ''
1390
+ let savedTurnAiMsg = null
1391
+ let lastServerUsage = null
1392
+ execSteps = [{ task: 'Menganalisis Konteks...' }]
1393
+ const dynamicallyLoadedToolGroups = new Set()
1394
+
1395
+ // Deteksi Tool Groups yang di-mention (@group_name) di input pengguna
1396
+ if (typeof userInput === 'string' && userInput.includes('@')) {
1397
+ try {
1398
+ const groupToolsData = await webApi.getGroupTools()
1399
+ const validGroupNames = new Set(
1400
+ groupToolsData?.names || Object.keys(groupToolsData?.schema || {})
1401
+ )
1402
+ const mentionMatches = userInput.match(/@([a-zA-Z0-9_-]+)/g) || []
1403
+ const taggedGroups = []
1404
+ for (const rawMatch of mentionMatches) {
1405
+ const groupName = rawMatch.slice(1).toLowerCase()
1406
+ if (validGroupNames.has(groupName)) {
1407
+ dynamicallyLoadedToolGroups.add(groupName)
1408
+ taggedGroups.push(groupName)
1409
+ }
1410
+ }
1411
+ if (taggedGroups.length > 0) {
1412
+ loopMessages.push({
1413
+ role: 'system',
1414
+ content: `[TOOL GROUPS AKTIF VIA MENTION USER]: Pengguna secara eksplisit menandai kelompok tool berikut: ${taggedGroups.map((g) => `@${g}`).join(', ')}. Seluruh kapabilitas tool dalam kelompok ini telah dibuka dan aktif. Utamakan penggunaan tool ini untuk menyelesaikan instruksi pengguna.`
1415
+ })
1416
+ }
1417
+ } catch (err) {
1418
+ console.error('[useMarkPlan] Gagal memproses mention tool group:', err)
1419
+ }
1420
+ }
1421
+
1422
+ let consecutiveErrors = 0
1423
+ const maxConsecutiveErrorRetries = 50
1424
+
1425
+ while (!isDone && !sessionAbortController.signal.aborted) {
1426
+ // Cek Abort Signal
1427
+ if (sessionAbortController.signal.aborted) {
1428
+ try {
1429
+ const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
1430
+ for (const t of runningTasks) {
1431
+ await transitionAgentTask(t.id, 'cancelled', 'user_abort').catch(() => {})
1432
+ }
1433
+ } catch (_) {}
1434
+ targetSetChatData((prev) =>
1435
+ prev.map((msg) => {
1436
+ if (!msg.isPlanSteps || msg.taskStatus !== 'running') return msg
1437
+ return {
1438
+ ...msg,
1439
+ taskStatus: 'stopped',
1440
+ plan: (msg.plan || []).map((s) => ({
1441
+ ...s,
1442
+ status: s.status === 'running' ? 'stopped' : s.status
1443
+ }))
1444
+ }
1445
+ })
1446
+ )
1447
+ throw new Error('AbortError')
1448
+ }
1449
+
1450
+ // Cek Intervensi User di tengah jalan
1451
+ if (sessionRecord.interventions?.length > 0) {
1452
+ const interventions = sessionRecord.interventions.splice(0).join('\n')
1453
+ loopMessages.push({ role: 'user', content: `[USER INTERVENTION]: ${interventions}` })
1454
+
1455
+ const alreadyInList = executedToolsList.some(
1456
+ (t) => t.type === 'intervention' && t.text === interventions
1457
+ )
1458
+ if (!alreadyInList) {
1459
+ executedToolsList.push({
1460
+ type: 'intervention',
1461
+ text: interventions,
1462
+ timestamp: Date.now()
1463
+ })
1464
+ }
1465
+
1466
+ execSteps.push({ task: `Intervensi User: ${interventions}` })
1467
+ targetPushProcess({
1468
+ id: agenticProcessId,
1469
+ type: 'planning',
1470
+ status: 'active',
1471
+ data: {
1472
+ steps: [...execSteps],
1473
+ currentStep: execSteps.length - 1,
1474
+ reasoning: 'Menerima arahan baru dari user di tengah proses.'
1475
+ }
1476
+ })
1477
+
1478
+ targetSetChatData((prev) =>
1479
+ prev.map((msg) =>
1480
+ msg.isThinking
1481
+ ? {
1482
+ ...msg,
1483
+ executedTools: [...executedToolsList]
1484
+ }
1485
+ : msg
1486
+ )
1487
+ )
1488
+ }
1489
+
1490
+ stepCount++
1491
+
1492
+ const isDurableTaskCompleted = Boolean(
1493
+ durableTask &&
1494
+ (durableTask.status === 'completed' ||
1495
+ (durableTask.steps &&
1496
+ durableTask.steps.length > 0 &&
1497
+ durableTask.steps.every((s) => s.status === 'completed')))
1498
+ )
1499
+ if (isDurableTaskCompleted) {
1500
+ durableActiveStep = null
1501
+ }
1502
+
1503
+ // Ambil Tools OpenAPI Schema yang relevan dengan query/tugas saat ini + group yang sudah dimuat
1504
+ const activeTools =
1505
+ opts.disableTools || isDurableTaskCompleted
1506
+ ? null
1507
+ : await getActiveToolsSchema(
1508
+ userInput + ' ' + (activeTaskObjectiveRef.current || ''),
1509
+ dynamicallyLoadedToolGroups
1510
+ )
1511
+
1512
+ // Loading thinking indicator di awal turn (akumulasi semua pemikiran dari langkah sebelumnya)
1513
+ targetSetChatData((prev) => {
1514
+ const filtered = prev.filter((item) => !item.isThinking)
1515
+ const loadingText =
1516
+ isAutonomous && autonomousInitialMessage ? autonomousInitialMessage : ''
1517
+ const allPriorThoughts = accumulatedThoughts
1518
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
1519
+ .filter(Boolean)
1520
+ const initialReasoning =
1521
+ allPriorThoughts.length > 0
1522
+ ? Array.from(new Set(allPriorThoughts)).join('\n\n---\n\n')
1523
+ : undefined
1524
+
1525
+ return [
1526
+ ...filtered,
1527
+ {
1528
+ role: 'ai',
1529
+ content: loadingText,
1530
+ isThinking: true,
1531
+ reasoning: initialReasoning,
1532
+ executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
1533
+ mood: currentActiveMood
1534
+ }
1535
+ ]
1536
+ })
1537
+
1538
+ currentTurnReasoning = ''
1539
+ let currentTurnContent = ''
1540
+ let sentenceBuffer = ''
1541
+
1542
+ // In-Flight Pruning: Jika akumulasi pesan tool di tengah loop mencapai batas kapasitas,
1543
+ // pangkas observasi tool terlama agar payload ReAct tetap berada di bawah 256K tokens.
1544
+ // Layer 2: In-Loop Agent ContextCompressor (Hermes 50% Threshold + O(n) Pruning)
1545
+ // Di setiap iterasi ReAct, evaluasi ukuran payload loopMessages.
1546
+ // Jika melebihi 50% kapasitas, lakukan pemangkasan Fase 1 O(n) dan jika perlu Fase 2-4 in-place.
1547
+ try {
1548
+ const inLoopResult = await checkAndCompressInLoop({
1549
+ loopMessages,
1550
+ sessionId: String(activeSessionNum || 1),
1551
+ maxTokens: MAX_CONTEXT_TOKENS,
1552
+ thresholdRatio: IN_LOOP_COMPACT_THRESHOLD,
1553
+ protectLastN: 6,
1554
+ protectFirstN: 2,
1555
+ activeConfig: config[0] || {},
1556
+ systemPrompt
1557
+ })
1558
+
1559
+ if (inLoopResult?.compressed && inLoopResult.loopMessages) {
1560
+ loopMessages = inLoopResult.loopMessages
1561
+ if (inLoopResult.totalTokens || inLoopResult.totalChars) {
1562
+ const inLoopTokens = inLoopResult.totalTokens || inLoopResult.totalChars
1563
+ window.dispatchEvent(
1564
+ new CustomEvent('context-tracker-updated', {
1565
+ detail: {
1566
+ sessionId: String(activeSessionNum || 1),
1567
+ currentTokens: inLoopTokens,
1568
+ maxTokens: MAX_CONTEXT_TOKENS,
1569
+ percentage: Math.min(100, (inLoopTokens / MAX_CONTEXT_TOKENS) * 100),
1570
+ currentChars: inLoopTokens,
1571
+ maxChars: MAX_CONTEXT_TOKENS,
1572
+ lastCompactedAt: Date.now()
1573
+ }
1574
+ })
1575
+ )
1576
+ }
1577
+ }
1578
+ } catch (inLoopErr) {
1579
+ console.warn('[useMarkPlan] In-loop compaction warning:', inLoopErr)
1580
+ }
1581
+
1582
+ // Safety Net Pruning
1583
+ loopMessages = pruneInFlightMessages(loopMessages, MAX_CONTEXT_TOKENS)
1584
+
1585
+ // Request streaming ke Backend AI Bridge
1586
+ const streamResult = await fetchAI(loopMessages, true, {
1587
+ sessionId: String(activeSessionNum || 1),
1588
+ tools: isDurableTaskCompleted ? null : activeTools,
1589
+ signal: sessionAbortController.signal,
1590
+ onReasoning: (chunk) => {
1591
+ currentTurnReasoning += chunk
1592
+ const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
1593
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
1594
+ .filter(Boolean)
1595
+ const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
1596
+
1597
+ targetSetChatData((prev) => {
1598
+ const filtered = prev.filter((item) => !item.isThinking)
1599
+ return [
1600
+ ...filtered,
1601
+ {
1602
+ role: 'ai',
1603
+ content: currentTurnContent,
1604
+ isThinking: true,
1605
+ reasoning: liveReasoning || undefined,
1606
+ executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
1607
+ mood: currentActiveMood
1608
+ }
1609
+ ]
1610
+ })
1611
+ },
1612
+ onMood: (moodTag) => {
1613
+ currentActiveMood = moodTag
1614
+ targetSetChatData((prev) =>
1615
+ prev.map((msg) => (msg.isThinking ? { ...msg, mood: moodTag } : msg))
1616
+ )
1617
+ },
1618
+ onMeta: (meta) => {
1619
+ if (meta?.mood) {
1620
+ currentActiveMood = meta.mood
1621
+ targetSetChatData((prev) =>
1622
+ prev.map((msg) => (msg.isThinking ? { ...msg, mood: meta.mood } : msg))
1623
+ )
1624
+ }
1625
+ },
1626
+ onToken: (token) => {
1627
+ currentTurnContent += token
1628
+ finalContentAccumulator = currentTurnContent
1629
+
1630
+ // Sentence-Level Streaming TTS: Deteksi kalimat lengkap secara real-time
1631
+ if (finalIsSpeak) {
1632
+ sentenceBuffer += token
1633
+ // Deteksi batas akhir kalimat (. ! ? atau newline ganda)
1634
+ let sentenceEndMatch
1635
+ while ((sentenceEndMatch = sentenceBuffer.match(/^(.*?[\.!\?\n]+)([\s\S]*)$/))) {
1636
+ const completeSentence = sentenceEndMatch[1].trim()
1637
+ sentenceBuffer = sentenceEndMatch[2] || ''
1638
+ if (completeSentence) {
1639
+ speechQueue.enqueue(completeSentence)
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ targetSetChatData((prev) => {
1645
+ const filtered = prev.filter((item) => !item.isThinking)
1646
+ const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
1647
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
1648
+ .filter(Boolean)
1649
+ const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
1650
+
1651
+ return [
1652
+ ...filtered,
1653
+ {
1654
+ role: 'ai',
1655
+ content: currentTurnContent,
1656
+ isThinking: true,
1657
+ reasoning: liveReasoning || undefined,
1658
+ executedTools: executedToolsList.length > 0 ? [...executedToolsList] : undefined,
1659
+ mood: currentActiveMood
1660
+ }
1661
+ ]
1662
+ })
1663
+ }
1664
+ })
1665
+
1666
+ if (
1667
+ currentTurnReasoning &&
1668
+ typeof currentTurnReasoning === 'string' &&
1669
+ currentTurnReasoning.trim()
1670
+ ) {
1671
+ const trimmed = currentTurnReasoning.trim()
1672
+ if (!accumulatedThoughts.includes(trimmed)) {
1673
+ accumulatedThoughts.push(trimmed)
1674
+ }
1675
+ }
1676
+
1677
+ if (streamResult?.mood && streamResult.mood !== 'neutral') {
1678
+ currentActiveMood = streamResult.mood
1679
+ }
1680
+
1681
+ if (streamResult?.usage) {
1682
+ lastServerUsage = streamResult.usage
1683
+ const liveTokens = Number(
1684
+ streamResult.usage.total_tokens ||
1685
+ (streamResult.usage.prompt_tokens || 0) +
1686
+ (streamResult.usage.completion_tokens || 0) ||
1687
+ 0
1688
+ )
1689
+ if (liveTokens > 0) {
1690
+ window.dispatchEvent(
1691
+ new CustomEvent('context-tracker-updated', {
1692
+ detail: {
1693
+ sessionId: String(activeSessionNum || 1),
1694
+ currentTokens: liveTokens,
1695
+ maxTokens: MAX_CONTEXT_TOKENS,
1696
+ percentage: Math.min(100, (liveTokens / MAX_CONTEXT_TOKENS) * 100),
1697
+ currentChars: liveTokens,
1698
+ maxChars: MAX_CONTEXT_TOKENS,
1699
+ lastCompactedAt: activeSessionCompact?.lastCompactedAt || Date.now()
1700
+ }
1701
+ })
1702
+ )
1703
+ }
1704
+ }
1705
+
1706
+ if (streamResult?.finishReason === 'error') {
1707
+ throw new Error('Terjadi kesalahan pada respon stream AI.')
1708
+ }
1709
+
1710
+ // Fallback Interceptor: Jika model mengembalikan teks JSON (tool_calls, mood, atau structured answer)
1711
+ let effectiveToolCalls = streamResult.toolCalls
1712
+ if ((!effectiveToolCalls || effectiveToolCalls.length === 0) && currentTurnContent) {
1713
+ const rawMatch = currentTurnContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
1714
+ let cand = (rawMatch ? rawMatch[1] : currentTurnContent).trim()
1715
+ const firstBrace = cand.indexOf('{')
1716
+ const lastBrace = cand.lastIndexOf('}')
1717
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
1718
+ cand = cand.substring(firstBrace, lastBrace + 1).trim()
1719
+ }
1720
+ if (
1721
+ cand.includes('"tool_calls"') ||
1722
+ cand.includes('"action"') ||
1723
+ cand.includes('"tool"') ||
1724
+ cand.includes('"mood"') ||
1725
+ cand.includes('"answer"')
1726
+ ) {
1727
+ try {
1728
+ const { jsonrepair } = await import('jsonrepair')
1729
+ let pObj = null
1730
+ try {
1731
+ pObj = JSON.parse(cand)
1732
+ } catch (_) {
1733
+ pObj = JSON.parse(jsonrepair(cand))
1734
+ }
1735
+ if (pObj) {
1736
+ if (pObj.mood) {
1737
+ currentActiveMood = String(pObj.mood).toLowerCase().trim()
1738
+ }
1739
+ if (pObj.answer !== undefined || pObj.content !== undefined) {
1740
+ currentTurnContent = pObj.answer !== undefined ? pObj.answer : pObj.content
1741
+ finalContentAccumulator = currentTurnContent
1742
+ }
1743
+ if (Array.isArray(pObj.tool_calls) && pObj.tool_calls.length > 0) {
1744
+ effectiveToolCalls = pObj.tool_calls.map((tc, idx) => ({
1745
+ id: tc.id || `call_fallback_${Date.now()}_${idx}`,
1746
+ type: 'function',
1747
+ function: {
1748
+ name: tc.name || tc.function?.name,
1749
+ arguments:
1750
+ typeof tc.arguments === 'object'
1751
+ ? JSON.stringify(tc.arguments)
1752
+ : String(tc.arguments || '{}')
1753
+ }
1754
+ }))
1755
+ currentTurnContent = ''
1756
+ finalContentAccumulator = ''
1757
+ } else if (Array.isArray(pObj.action) && pObj.action.length > 0) {
1758
+ // Format BATCH ACTIONS array V4: { "action": [ { "tool": "...", "query": "..." }, ... ] }
1759
+ effectiveToolCalls = pObj.action
1760
+ .filter((act) => act && (act.tool || act.name))
1761
+ .map((act, idx) => ({
1762
+ id: `call_fallback_${Date.now()}_${idx}`,
1763
+ type: 'function',
1764
+ function: {
1765
+ name: act.tool || act.name,
1766
+ arguments:
1767
+ typeof act.arguments === 'object'
1768
+ ? JSON.stringify(act.arguments)
1769
+ : typeof act.query === 'object'
1770
+ ? JSON.stringify(act.query)
1771
+ : JSON.stringify(
1772
+ act.query !== undefined
1773
+ ? { query: act.query }
1774
+ : act.arguments !== undefined
1775
+ ? { query: act.arguments }
1776
+ : {}
1777
+ )
1778
+ }
1779
+ }))
1780
+ currentTurnContent = ''
1781
+ finalContentAccumulator = ''
1782
+ } else if (Array.isArray(pObj) && pObj.length > 0) {
1783
+ // Format array tool calls langsung: [ { "name": "...", "arguments": ... }, ... ]
1784
+ effectiveToolCalls = pObj
1785
+ .filter((item) => item && (item.name || item.tool || item.function?.name))
1786
+ .map((item, idx) => ({
1787
+ id: item.id || `call_fallback_${Date.now()}_${idx}`,
1788
+ type: 'function',
1789
+ function: {
1790
+ name: item.name || item.tool || item.function?.name,
1791
+ arguments:
1792
+ typeof item.arguments === 'object'
1793
+ ? JSON.stringify(item.arguments)
1794
+ : typeof item.query === 'object'
1795
+ ? JSON.stringify(item.query)
1796
+ : String(
1797
+ item.arguments ||
1798
+ (item.query !== undefined
1799
+ ? JSON.stringify({ query: item.query })
1800
+ : '{}')
1801
+ )
1802
+ }
1803
+ }))
1804
+ currentTurnContent = ''
1805
+ finalContentAccumulator = ''
1806
+ } else if (pObj.action && (pObj.action.tool || pObj.action.name)) {
1807
+ effectiveToolCalls = [
1808
+ {
1809
+ id: `call_fallback_${Date.now()}_0`,
1810
+ type: 'function',
1811
+ function: {
1812
+ name: pObj.action.tool || pObj.action.name,
1813
+ arguments:
1814
+ typeof pObj.action.arguments === 'object'
1815
+ ? JSON.stringify(pObj.action.arguments)
1816
+ : typeof pObj.action.query === 'object'
1817
+ ? JSON.stringify(pObj.action.query)
1818
+ : JSON.stringify(
1819
+ pObj.action.query !== undefined
1820
+ ? { query: pObj.action.query }
1821
+ : {}
1822
+ )
1823
+ }
1824
+ }
1825
+ ]
1826
+ currentTurnContent = ''
1827
+ finalContentAccumulator = ''
1828
+ }
1829
+ }
1830
+ } catch (_) {}
1831
+ }
1832
+ }
1833
+
1834
+ // Jika alur kerja Durable Task telah tuntas 100%, abaikan seluruh tool calls dan paksa penyelesaian
1835
+ if (isDurableTaskCompleted) {
1836
+ effectiveToolCalls = []
1837
+ }
1838
+
1839
+ // ======================================================================
1840
+ // CABANG 1: MODEL MEMANGGIL NATIVE TOOL CALLS
1841
+ // ======================================================================
1842
+ if (effectiveToolCalls && effectiveToolCalls.length > 0) {
1843
+ sentenceBuffer = ''
1844
+ speechQueue.reset()
1845
+ const assistantMsg = {
1846
+ role: 'assistant',
1847
+ content: streamResult.content || null,
1848
+ tool_calls: effectiveToolCalls
1849
+ }
1850
+ loopMessages.push(assistantMsg)
1851
+ const turnVisualUrls = []
1852
+
1853
+ for (const tc of effectiveToolCalls) {
1854
+ const toolName = tc.function?.name
1855
+ let parsedArgs = {}
1856
+ try {
1857
+ parsedArgs = JSON.parse(tc.function?.arguments || '{}')
1858
+ } catch (_) {
1859
+ parsedArgs = { raw: tc.function?.arguments || '' }
1860
+ }
1861
+
1862
+ if (!toolName) continue
1863
+ if (sessionAbortController.signal.aborted) throw new Error('AbortError')
1864
+
1865
+ execSteps.push({ task: `Eksekusi ${toolName}`, query: JSON.stringify(parsedArgs) })
1866
+ targetPushProcess({
1867
+ id: agenticProcessId,
1868
+ type: 'planning',
1869
+ status: 'active',
1870
+ data: {
1871
+ steps: [...execSteps],
1872
+ currentStep: execSteps.length - 1,
1873
+ reasoning: currentTurnReasoning || `Mengeksekusi ${toolName}`
1874
+ }
1875
+ })
1876
+
1877
+ const toolReason =
1878
+ parsedArgs.reason ||
1879
+ parsedArgs.description ||
1880
+ parsedArgs.task ||
1881
+ parsedArgs.title ||
1882
+ null
1883
+
1884
+ currentInFlightTool = {
1885
+ tool: toolName,
1886
+ query: JSON.stringify(parsedArgs),
1887
+ reason: toolReason,
1888
+ status: 'running'
1889
+ }
1890
+ const currentLiveTools = [...executedToolsList, currentInFlightTool]
1891
+
1892
+ const currentCombined = [...accumulatedThoughts, currentTurnReasoning]
1893
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
1894
+ .filter(Boolean)
1895
+ const liveReasoning = Array.from(new Set(currentCombined)).join('\n\n---\n\n')
1896
+
1897
+ targetSetChatData((prev) => {
1898
+ const filtered = prev.filter((item) => !item.isThinking)
1899
+ return [
1900
+ ...filtered,
1901
+ {
1902
+ role: 'ai',
1903
+ content: streamResult.content || `Mengeksekusi [${toolName}]...`,
1904
+ isThinking: true,
1905
+ reasoning: liveReasoning || undefined,
1906
+ executedTools: currentLiveTools,
1907
+ mood: currentActiveMood,
1908
+ usage: lastServerUsage || undefined,
1909
+ tokens: lastServerUsage?.completion_tokens || undefined
1910
+ }
1911
+ ]
1912
+ })
1913
+
1914
+ // Eksekusi tool
1915
+ const pluginProcessId = `plugin-${Date.now()}`
1916
+ const currentSessionTitle =
1917
+ activeSessionNum === 1
1918
+ ? activeTopic?.title || activeTopic?.name || 'Main Thread'
1919
+ : `Sesi #${activeSessionNum}`
1920
+
1921
+ // Jika alur kerja Durable Task sudah selesai, tolak pemanggilan task baru atau mark_done_task
1922
+ if (
1923
+ durableTask &&
1924
+ !durableActiveStep &&
1925
+ (toolName === 'create_agent_task' || toolName === 'mark_done_task')
1926
+ ) {
1927
+ const rejectResult = `[TUGAS TELAH SELESAI]: Seluruh tahapan alur kerja '${durableTask.title}' telah tuntas 100%. DILARANG memanggil tool '${toolName}' lagi! SEGERA berikan jawaban akhir ringkasan menyeluruh kepada pengguna.`
1928
+ const toolObservation = {
1929
+ type: 'tool_result',
1930
+ tool_call_id: tc.id,
1931
+ tool: toolName,
1932
+ success: false,
1933
+ data: null,
1934
+ error: rejectResult
1935
+ }
1936
+ loopMessages.push({
1937
+ role: 'tool',
1938
+ tool_call_id: tc.id,
1939
+ name: toolName,
1940
+ content: JSON.stringify(toolObservation)
1941
+ })
1942
+ continue
1943
+ }
1944
+
1945
+ const execResult = await executeSingleTool(toolName, parsedArgs, {
1946
+ tgContext,
1947
+ isAutonomous,
1948
+ loopMessages,
1949
+ pluginProcessId,
1950
+ targetSetChatData,
1951
+ activeSessionNum,
1952
+ activeTopic,
1953
+ userInput,
1954
+ durableTask,
1955
+ durableActiveStep,
1956
+ agenticProcessId,
1957
+ workspaceRoot: opts.workspaceRoot,
1958
+ sessionId: String(activeSessionNum || 1),
1959
+ sessionTitle: currentSessionTitle,
1960
+ signal: sessionAbortController.signal
1961
+ })
1962
+
1963
+ if (execResult.durableTask) {
1964
+ durableTask = execResult.durableTask
1965
+ durableTaskForRecovery = execResult.durableTask
1966
+ }
1967
+ if (toolName === 'create_agent_task' || toolName === 'mark_done_task') {
1968
+ durableActiveStep = execResult.durableActiveStep || null
1969
+ }
1970
+
1971
+ lastToolExecution = execResult.toolExecution
1972
+ if (execResult.loadedGroup) {
1973
+ dynamicallyLoadedToolGroups.add(execResult.loadedGroup)
1974
+ }
1975
+ const resStr = String(execResult.resultString || '')
1976
+ const executionSucceeded =
1977
+ execResult.success === true ||
1978
+ execResult.res?.success === true ||
1979
+ (!resStr.startsWith('[ERROR]') &&
1980
+ !resStr.includes(' crash:') &&
1981
+ !resStr.toLowerCase().includes(' gagal:'))
1982
+ if (executionSucceeded) {
1983
+ consecutiveErrors = 0
1984
+ } else {
1985
+ consecutiveErrors++
1986
+ if (consecutiveErrors >= maxConsecutiveErrorRetries) {
1987
+ const error = new Error(
1988
+ `Batas retry error berturut-turut tercapai (${maxConsecutiveErrorRetries}).`
1989
+ )
1990
+ error.code = 'LOOP_GUARD'
1991
+ throw error
1992
+ }
1993
+ }
1994
+
1995
+ executedToolsList.push({
1996
+ tool: toolName,
1997
+ query: JSON.stringify(parsedArgs),
1998
+ reason: toolReason,
1999
+ status: executionSucceeded ? 'done' : 'failed',
2000
+ preview: execResult.previewUrl || execResult.imageUrls?.[0] || null,
2001
+ fullResult:
2002
+ typeof execResult.resultString === 'string'
2003
+ ? execResult.resultString.slice(0, 4000)
2004
+ : execResult.resultString,
2005
+ resultSummary:
2006
+ typeof execResult.resultString === 'string' && execResult.resultString.length > 250
2007
+ ? execResult.resultString.slice(0, 250) + '...'
2008
+ : execResult.resultString
2009
+ })
2010
+ currentInFlightTool = null
2011
+
2012
+ if (Array.isArray(execResult.imageUrls) && execResult.imageUrls.length > 0) {
2013
+ for (const u of execResult.imageUrls) {
2014
+ if (u && !turnVisualUrls.includes(u)) {
2015
+ turnVisualUrls.push(u)
2016
+ }
2017
+ }
2018
+ }
2019
+
2020
+ let obsStr = execResult.resultString
2021
+ if (
2022
+ typeof execResult.resultString === 'string' &&
2023
+ execResult.resultString.length > 3000
2024
+ ) {
2025
+ obsStr = `${execResult.resultString.slice(0, 3000)}\n\n[SISA OUTPUT DIPOTONG (Total: ${execResult.resultString.length} karakter). Gunakan start_line/end_line atau grep-search untuk mencari bagian spesifik.]`
2026
+ }
2027
+
2028
+ const toolObservation = {
2029
+ type: 'tool_result',
2030
+ tool_call_id: tc.id,
2031
+ tool: toolName,
2032
+ success: executionSucceeded,
2033
+ data: executionSucceeded ? obsStr : null,
2034
+ error: executionSucceeded ? null : obsStr
2035
+ }
2036
+
2037
+ // Push role: 'tool' observation ke ephemeral context
2038
+ loopMessages.push({
2039
+ role: 'tool',
2040
+ tool_call_id: tc.id,
2041
+ name: toolName,
2042
+ content: JSON.stringify(toolObservation)
2043
+ })
2044
+
2045
+ // Jika mark_done_task berhasil dan ada tahap berikutnya, otomatis majukan loop
2046
+ if (
2047
+ toolName === 'mark_done_task' &&
2048
+ executionSucceeded &&
2049
+ execResult?.durableActiveStep &&
2050
+ execResult?.durableTask
2051
+ ) {
2052
+ const nextStep = execResult.durableActiveStep
2053
+ const nextIdx = (nextStep.stepIndex ?? nextStep.index ?? 0) + 1
2054
+ const totalSteps = execResult.durableTask?.steps?.length || 0
2055
+ const isNextLastStep = nextIdx === totalSteps
2056
+ consecutiveErrors = 0
2057
+ loopMessages.push({
2058
+ role: 'user',
2059
+ content: `[TAHAP SELESAI & PINDAH KE TAHAP ${nextIdx}${isNextLastStep ? ' - TAHAP TERAKHIR' : ''}]: Artefak tahap sebelumnya telah disimpan ke disk.\n\n>>> SEKARANG KERJAKAN TAHAP ${nextIdx}: "${nextStep.title}"\n- Sasaran: ${nextStep.objective}\n- Target Deliverable: ${nextStep.deliverable}\n${nextStep.acceptanceCriteria?.length ? `- Kriteria Sukses: ${nextStep.acceptanceCriteria.join(', ')}` : ''}\nKerjakan tugas tahap ini di workspace pengguna (buat file/tulis kode). DILARANG memanggil 'read_task' untuk membaca artefak sebelumnya (kamu sudah tahu apa yang kamu buat). WAJIB UJI & VERIFIKASI HASIL terlebih dahulu. DILARANG memanggil 'mark_done_task' sebelum pengujian berhasil! Setelah teruji, barulah PANGGIL TOOL 'mark_done_task' TEPAT 1 KALI dengan parameter stepIndex: ${nextIdx}, artifactContent, dan verificationProof (bukti hasil uji)!${isNextLastStep ? ' Karena ini adalah TAHAP TERAKHIR, saat deliverable tahap ini selesai dan diuji, panggil tool "mark_done_task" dengan parameter "summary" yang memuat rangkuman menyeluruh seluruh alur kerja proyek dari awal hingga akhir!' : ''}`
2060
+ })
2061
+ await startAgentTaskStep(execResult.durableTask.id, nextStep.id)
2062
+ targetPushProcess({
2063
+ id: agenticProcessId,
2064
+ type: 'planning',
2065
+ status: 'active',
2066
+ data: {
2067
+ steps: (execResult.durableTask.steps || []).map((s) => ({ task: s.title })),
2068
+ currentStep: nextStep.stepIndex ?? nextStep.index ?? 0,
2069
+ reasoning: `Melanjutkan ke Tahap ${nextIdx}: "${nextStep.title}"...`
2070
+ }
2071
+ })
2072
+ } else if (
2073
+ toolName === 'mark_done_task' &&
2074
+ executionSucceeded &&
2075
+ !execResult?.durableActiveStep &&
2076
+ execResult?.durableTask
2077
+ ) {
2078
+ // Seluruh tahapan alur kerja telah tuntas 100%!
2079
+ consecutiveErrors = 0
2080
+ durableActiveStep = null
2081
+
2082
+ const completedTask = execResult.durableTask
2083
+ const steps = completedTask.steps || []
2084
+ const taskTitle = completedTask.title || 'Task Workflow'
2085
+
2086
+ let rawSummary =
2087
+ (typeof parsedArgs.summary === 'string' && parsedArgs.summary.trim()) ||
2088
+ (typeof parsedArgs.artifactContent === 'string' &&
2089
+ parsedArgs.artifactContent.trim()) ||
2090
+ ''
2091
+
2092
+ let finalReport = ''
2093
+ if (rawSummary && rawSummary.length > 150) {
2094
+ finalReport = rawSummary
2095
+ } else {
2096
+ const stepsSummary = steps
2097
+ .map((s, i) => {
2098
+ const num = (s.stepIndex ?? s.index ?? i) + 1
2099
+ const title = s.title || `Tahap ${num}`
2100
+ const out = s.outputSummary || s.deliverable || 'Selesai dan terverifikasi'
2101
+ return `${num}. **${title}**: ${out}`
2102
+ })
2103
+ .join('\n')
2104
+
2105
+ finalReport = `Seluruh tahapan alur kerja **${taskTitle}** telah berhasil diselesaikan dan terverifikasi 100%.\n\n### Rangkuman Hasil Pekerjaan:\n${stepsSummary}${rawSummary ? `\n\n**Catatan Tambahan:**\n${rawSummary}` : ''}`
2106
+ }
2107
+
2108
+ // Pastikan nol emoji
2109
+ finalReport = finalReport
2110
+ .replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '')
2111
+ .trim()
2112
+
2113
+ isDone = true
2114
+ finalContentAccumulator = finalReport
2115
+
2116
+ execSteps.push({ task: 'Selesai' })
2117
+ targetPushProcess({
2118
+ id: agenticProcessId,
2119
+ type: 'planning',
2120
+ status: 'done',
2121
+ data: {
2122
+ steps: [...execSteps],
2123
+ currentStep: execSteps.length,
2124
+ reasoning: 'Seluruh tahapan tugas telah diselesaikan dan terverifikasi.'
2125
+ }
2126
+ })
2127
+
2128
+ targetSetChatData((prev) => {
2129
+ const filtered = prev.filter((item) => !item.isThinking)
2130
+ const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
2131
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
2132
+ .filter(Boolean)
2133
+ const mergedReasoning =
2134
+ finalAllThoughts.length > 0
2135
+ ? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
2136
+ : null
2137
+
2138
+ const { meta: reportMeta, cleanContent: cleanFinalOutput } =
2139
+ parseMarkTag(finalReport)
2140
+ if (reportMeta?.mood && reportMeta.mood !== 'neutral') {
2141
+ currentActiveMood = reportMeta.mood
2142
+ }
2143
+
2144
+ const aiMsg = {
2145
+ role: 'ai',
2146
+ content: cleanFinalOutput,
2147
+ executedTools: executedToolsList.length > 0 ? executedToolsList : null,
2148
+ isTaskDone: true,
2149
+ reasoning: mergedReasoning,
2150
+ mood: currentActiveMood || 'neutral',
2151
+ pluginExecution: lastToolExecution,
2152
+ isProactive: isAutonomous,
2153
+ timestamp: getCurrentTimeInfo(),
2154
+ created_at: Date.now(),
2155
+ source: tgContext ? 'telegram' : 'pc',
2156
+ usage: lastServerUsage || null,
2157
+ tokens:
2158
+ lastServerUsage?.completion_tokens ||
2159
+ calculateMessageTokens({
2160
+ content: cleanFinalOutput,
2161
+ reasoning: mergedReasoning
2162
+ })
2163
+ }
2164
+
2165
+ savedTurnAiMsg = aiMsg
2166
+
2167
+ return filtered
2168
+ .map((msg) => {
2169
+ if (!msg.isPlanSteps || msg.taskId !== completedTask.id) return msg
2170
+ return {
2171
+ ...msg,
2172
+ taskStatus: 'completed',
2173
+ currentStep: (msg.plan || []).length,
2174
+ plan: (msg.plan || []).map((s) => ({
2175
+ ...s,
2176
+ status: s.status === 'failed' ? 'failed' : 'completed'
2177
+ }))
2178
+ }
2179
+ })
2180
+ .concat(aiMsg)
2181
+ })
2182
+
2183
+ if (finalIsSpeak && finalReport) {
2184
+ playVoice(finalReport).catch(() => {})
2185
+ }
2186
+
2187
+ if (window.api?.showNotification && !document.hasFocus() && finalReport) {
2188
+ window.api.showNotification('Mark', `Alur kerja "${taskTitle}" tuntas!`)
2189
+ }
2190
+
2191
+ break
2192
+ }
2193
+ }
2194
+
2195
+ // Jika ada tool yang menghasilkan gambar visual, sertakan observasi multimodal
2196
+ if (turnVisualUrls.length > 0) {
2197
+ loopMessages.push({
2198
+ role: 'user',
2199
+ content: [
2200
+ {
2201
+ type: 'text',
2202
+ text: '[Visual Observation]: Berikut adalah gambar visual aktual beresolusi penuh dari eksekusi tool di atas untuk kamu analisis secara langsung:'
2203
+ },
2204
+ ...turnVisualUrls.map((url) => ({
2205
+ type: 'image_url',
2206
+ image_url: { url }
2207
+ }))
2208
+ ]
2209
+ })
2210
+ }
2211
+
2212
+ // Lanjut ke giliran berikutnya untuk membiarkan model menganalisis observasi tool
2213
+ if (sessionAbortController.signal.aborted) {
2214
+ throw new Error('AbortError')
2215
+ }
2216
+ if (isDone) {
2217
+ break
2218
+ }
2219
+ continue
2220
+ }
2221
+
2222
+ // ======================================================================
2223
+ // CABANG 2: SELESAI / DIRECT TEXT RESPONSE (Stop / Selesai)
2224
+ // ======================================================================
2225
+ const rawTurnAnswer = streamResult.content || currentTurnContent || ''
2226
+ const { meta: turnMeta, cleanContent: turnAnswer } = parseMarkTag(rawTurnAnswer)
2227
+ if (turnMeta?.mood && turnMeta.mood !== 'neutral') {
2228
+ currentActiveMood = turnMeta.mood
2229
+ }
2230
+
2231
+ // Jika alur kerja Durable Task sedang aktif tapi belum seluruh tahap selesai
2232
+ if (durableTask && !isDurableTaskCompleted) {
2233
+ const activeStep =
2234
+ durableActiveStep ||
2235
+ durableTask.steps?.find((s) => s.status === 'running') ||
2236
+ durableTask.steps?.find((s) => s.status !== 'completed') ||
2237
+ durableTask.steps?.[0]
2238
+ const curIdx = activeStep ? (activeStep.stepIndex ?? activeStep.index ?? 0) + 1 : 1
2239
+ const curTitle = activeStep?.title || 'Tahap Aktif'
2240
+ const curObj = activeStep?.objective || durableTask.objective || ''
2241
+ const curDeliv = activeStep?.deliverable || ''
2242
+
2243
+ loopMessages.push({
2244
+ role: 'assistant',
2245
+ content: rawTurnAnswer
2246
+ })
2247
+ loopMessages.push({
2248
+ role: 'user',
2249
+ content: `[PENGINGAT TAHAP AKTIF]: Kamu saat ini sedang berada di Tahap ${curIdx}: "${curTitle}".\n- Sasaran: ${curObj}\n- Target Deliverable: ${curDeliv}\n\nTahap ini BELUM selesai karena kamu belum memverifikasi hasil dan belum memanggil tool 'mark_done_task'. Kerjakan sasaran ini dengan tools yang relevan di workspace pengguna, lakukan pengujian/verifikasi hasil, lalu PANGGIL TOOL 'mark_done_task' dengan parameter stepIndex: ${curIdx}, artifactContent, dan verificationProof agar alur kerja dapat beralih ke tahap berikutnya!`
2250
+ })
2251
+ continue
2252
+ }
2253
+
2254
+ // Jika seluruh tahapan Durable Task telah tuntas diselesaikan via mark_done_task
2255
+ if (durableTask && isDurableTaskCompleted) {
2256
+ targetSetChatData((prev) =>
2257
+ prev.map((msg) => {
2258
+ if (!msg.isPlanSteps || msg.taskId !== durableTask.id) return msg
2259
+ return {
2260
+ ...msg,
2261
+ taskStatus: 'completed',
2262
+ currentStep: (msg.plan || []).length,
2263
+ plan: (msg.plan || []).map((s) => ({
2264
+ ...s,
2265
+ status: s.status === 'failed' ? 'failed' : 'completed'
2266
+ }))
2267
+ }
2268
+ })
2269
+ )
2270
+ }
2271
+
2272
+ // Cek apakah ada intervensi user yang masuk saat streaming giliran ini
2273
+ if (sessionRecord.interventions?.length > 0) {
2274
+ const interventions = sessionRecord.interventions.splice(0).join('\n')
2275
+ if (rawTurnAnswer && rawTurnAnswer.trim()) {
2276
+ loopMessages.push({ role: 'assistant', content: rawTurnAnswer })
2277
+ }
2278
+ loopMessages.push({ role: 'user', content: `[USER INTERVENTION]: ${interventions}` })
2279
+
2280
+ const alreadyInList = executedToolsList.some(
2281
+ (t) => t.type === 'intervention' && t.text === interventions
2282
+ )
2283
+ if (!alreadyInList) {
2284
+ executedToolsList.push({
2285
+ type: 'intervention',
2286
+ text: interventions,
2287
+ timestamp: Date.now()
2288
+ })
2289
+ }
2290
+
2291
+ execSteps.push({ task: `Intervensi User: ${interventions}` })
2292
+ targetPushProcess({
2293
+ id: agenticProcessId,
2294
+ type: 'planning',
2295
+ status: 'active',
2296
+ data: {
2297
+ steps: [...execSteps],
2298
+ currentStep: execSteps.length - 1,
2299
+ reasoning: 'Menerima arahan baru dari user saat penyelesaian giliran.'
2300
+ }
2301
+ })
2302
+
2303
+ targetSetChatData((prev) =>
2304
+ prev.map((msg) =>
2305
+ msg.isThinking
2306
+ ? {
2307
+ ...msg,
2308
+ executedTools: [...executedToolsList]
2309
+ }
2310
+ : msg
2311
+ )
2312
+ )
2313
+
2314
+ continue
2315
+ }
2316
+
2317
+ // Strict Explicit Done: AI harus menuliskan done="true" untuk mengakhiri loop.
2318
+ // Jika done === false, teruskan loop ke turn berikutnya agar AI bisa mengeksekusi langkah lanjutan.
2319
+ if (!turnMeta.done) {
2320
+ if (rawTurnAnswer && rawTurnAnswer.trim()) {
2321
+ loopMessages.push({ role: 'assistant', content: rawTurnAnswer })
2322
+ }
2323
+ loopMessages.push({
2324
+ role: 'user',
2325
+ content: '[Catatan Sistem]: Lanjutkan analisis dan eksekusi tugas berikutnya.'
2326
+ })
2327
+
2328
+ execSteps.push({ task: 'Melanjutkan langkah...' })
2329
+ targetPushProcess({
2330
+ id: agenticProcessId,
2331
+ type: 'planning',
2332
+ status: 'active',
2333
+ data: {
2334
+ steps: [...execSteps],
2335
+ currentStep: execSteps.length - 1,
2336
+ reasoning: currentTurnReasoning || 'Melanjutkan langkah berikutnya...'
2337
+ }
2338
+ })
2339
+
2340
+ continue
2341
+ }
2342
+
2343
+ isDone = true
2344
+ finalContentAccumulator = turnAnswer || 'Selesai.'
2345
+
2346
+ execSteps.push({ task: 'Selesai' })
2347
+ targetPushProcess({
2348
+ id: agenticProcessId,
2349
+ type: 'planning',
2350
+ status: 'done',
2351
+ data: {
2352
+ steps: [...execSteps],
2353
+ currentStep: execSteps.length,
2354
+ reasoning: currentTurnReasoning || 'Selesai'
2355
+ }
2356
+ })
2357
+
2358
+ // TTS Lisan: Kirimkan sisa buffer kalimat yang belum ter-enqueue
2359
+ if (finalIsSpeak) {
2360
+ if (sentenceBuffer && sentenceBuffer.trim()) {
2361
+ speechQueue.enqueue(sentenceBuffer.trim())
2362
+ sentenceBuffer = ''
2363
+ } else if (
2364
+ !speechQueue.isPlaying &&
2365
+ speechQueue.queue.length === 0 &&
2366
+ finalContentAccumulator
2367
+ ) {
2368
+ // Fallback jika tidak ada tanda baca di output model sama sekali
2369
+ playVoice(finalContentAccumulator).catch(() => {})
2370
+ }
2371
+ }
2372
+
2373
+ // OS Notification
2374
+ if (window.api.showNotification && !document.hasFocus() && finalContentAccumulator) {
2375
+ window.api.showNotification('Mark', finalContentAccumulator)
2376
+ }
2377
+
2378
+ // Tampilkan balasan final di chat UI
2379
+ targetSetChatData((prev) => {
2380
+ const filtered = prev.filter((item) => {
2381
+ if (item.isThinking) return false
2382
+ if (isAutonomous && item.isProactive && item.content === autonomousInitialMessage)
2383
+ return false
2384
+ return true
2385
+ })
2386
+
2387
+ const { meta: endMeta, cleanContent: parsedEndOutput } = parseMarkTag(
2388
+ finalContentAccumulator || ''
2389
+ )
2390
+ if (endMeta?.mood && endMeta.mood !== 'neutral') {
2391
+ currentActiveMood = endMeta.mood
2392
+ }
2393
+ let finalOutput = parsedEndOutput
2394
+ if (isAutonomous && autonomousInitialMessage) {
2395
+ finalOutput = `**${autonomousInitialMessage}**\n\n${finalOutput}`
2396
+ }
2397
+
2398
+ const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
2399
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
2400
+ .filter(Boolean)
2401
+ const mergedReasoning =
2402
+ finalAllThoughts.length > 0
2403
+ ? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
2404
+ : null
2405
+
2406
+ const aiMsg = {
2407
+ role: 'ai',
2408
+ content: finalOutput,
2409
+ executedTools: executedToolsList.length > 0 ? executedToolsList : null,
2410
+ isTaskDone: true,
2411
+ reasoning: mergedReasoning,
2412
+ mood: currentActiveMood || 'neutral',
2413
+ pluginExecution: lastToolExecution,
2414
+ isProactive: isAutonomous,
2415
+ timestamp: getCurrentTimeInfo(),
2416
+ created_at: Date.now(),
2417
+ source: tgContext ? 'telegram' : 'pc',
2418
+ usage: lastServerUsage || null,
2419
+ tokens:
2420
+ lastServerUsage?.completion_tokens ||
2421
+ calculateMessageTokens({ content: finalOutput, reasoning: mergedReasoning })
2422
+ }
2423
+
2424
+ savedTurnAiMsg = aiMsg
2425
+ return [...filtered, aiMsg]
2426
+ })
2427
+
2428
+ // Meta-Learning: Sintesis skill otomatis jika turn berhasil mengeksekusi tool bermakna
2429
+ if (
2430
+ !isAutonomous &&
2431
+ !isSystem &&
2432
+ executedToolsList &&
2433
+ executedToolsList.length > 0 &&
2434
+ finalContentAccumulator
2435
+ ) {
2436
+ const finalAllThoughts = [...accumulatedThoughts, currentTurnReasoning]
2437
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
2438
+ .filter(Boolean)
2439
+ const mergedReasoning =
2440
+ finalAllThoughts.length > 0
2441
+ ? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
2442
+ : ''
2443
+
2444
+ synthesizeSkillAndSave({
2445
+ userPrompt: lastUserPromptRef.current || userInput,
2446
+ executedTools: executedToolsList,
2447
+ finalAnswer: finalContentAccumulator,
2448
+ thought: mergedReasoning
2449
+ }).catch((err) => {
2450
+ console.warn('[useMarkPlan] Background Meta-Learning error:', err)
2451
+ })
2452
+ }
2453
+
2454
+ break
2455
+ }
2456
+
2457
+ if (sessionAbortController.signal.aborted) {
2458
+ throw new Error('AbortError')
2459
+ }
2460
+
2461
+ // Pastikan sisa thinking indicator selalu dibersihkan jika loop selesai
2462
+ targetSetChatData((prev) => {
2463
+ const hasThinking = prev.some((item) => item.isThinking)
2464
+ if (!hasThinking) return prev
2465
+ const filtered = prev.filter((item) => !item.isThinking)
2466
+ if (finalContentAccumulator) return filtered
2467
+
2468
+ const finalAllThoughts = [...accumulatedThoughts]
2469
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
2470
+ .filter(Boolean)
2471
+ const mergedReasoning =
2472
+ finalAllThoughts.length > 0
2473
+ ? Array.from(new Set(finalAllThoughts)).join('\n\n---\n\n')
2474
+ : null
2475
+
2476
+ return [
2477
+ ...filtered,
2478
+ {
2479
+ role: 'ai',
2480
+ content: 'Tugas telah selesai diproses.',
2481
+ executedTools: executedToolsList.length > 0 ? executedToolsList : null,
2482
+ isTaskDone: true,
2483
+ reasoning: mergedReasoning,
2484
+ mood: currentActiveMood || 'neutral',
2485
+ timestamp: getCurrentTimeInfo(),
2486
+ created_at: Date.now(),
2487
+ usage: lastServerUsage || null,
2488
+ tokens:
2489
+ lastServerUsage?.completion_tokens ||
2490
+ calculateMessageTokens({
2491
+ content: 'Tugas telah selesai diproses.',
2492
+ reasoning: mergedReasoning
2493
+ })
2494
+ }
2495
+ ]
2496
+ })
2497
+
2498
+ // ------------------------------------------------------------------------
2499
+ // FASE 5: CLEANUP & CLOSING
2500
+ // ------------------------------------------------------------------------
2501
+ targetPushProcess({
2502
+ id: agenticProcessId,
2503
+ type: 'planning',
2504
+ status: 'done',
2505
+ data: {
2506
+ steps: [...execSteps],
2507
+ currentStep: execSteps.length,
2508
+ reasoning: accumulatedThoughts[accumulatedThoughts.length - 1] || 'Selesai'
2509
+ }
2510
+ })
2511
+ setTimeout(() => {
2512
+ dismissProcess(agenticProcessId)
2513
+ }, 1500)
2514
+
2515
+ if (!tgContext && !isAutonomous) {
2516
+ if (activeSessionNum === 1) {
2517
+ setIsLoading(false)
2518
+ }
2519
+ lastUserPromptRef.current = ''
2520
+ }
2521
+
2522
+ // Post-Turn Context Sync: Hitung total token terkini dan trigger event ke UI
2523
+ try {
2524
+ let latestSessionData =
2525
+ activeSessionNum === 1 ? chatDataRef.current || chatData : inMemorySessionData
2526
+ if (savedTurnAiMsg) {
2527
+ const hasAi = (latestSessionData || []).some(
2528
+ (m) => m.created_at === savedTurnAiMsg.created_at
2529
+ )
2530
+ if (!hasAi) {
2531
+ latestSessionData = [...(latestSessionData || []), savedTurnAiMsg]
2532
+ }
2533
+ }
2534
+ const latestTokens = calculateSessionTokens(
2535
+ latestSessionData,
2536
+ activeSessionCompact?.summaryBlock || activeSessionCompact?.summary_block || '',
2537
+ activeSessionCompact?.lastCompactedMessageId ||
2538
+ activeSessionCompact?.last_compacted_message_id ||
2539
+ null,
2540
+ systemPrompt,
2541
+ activeSessionCompact?.lastCompactedAt || activeSessionCompact?.last_compacted_at || null
2542
+ )
2543
+ window.dispatchEvent(
2544
+ new CustomEvent('context-tracker-updated', {
2545
+ detail: {
2546
+ sessionId: String(activeSessionNum),
2547
+ currentTokens: latestTokens,
2548
+ maxTokens: MAX_CONTEXT_TOKENS,
2549
+ percentage: Math.min(100, (latestTokens / MAX_CONTEXT_TOKENS) * 100),
2550
+ currentChars: latestTokens,
2551
+ maxChars: MAX_CONTEXT_TOKENS,
2552
+ lastCompactedAt: activeSessionCompact?.lastCompactedAt || null
2553
+ }
2554
+ })
2555
+ )
2556
+ } catch {
2557
+ /* ignore */
2558
+ }
2559
+
2560
+ try {
2561
+ if (window.api && window.api.executeNativeTool) {
2562
+ window.api.executeNativeTool('os-control-close').catch(() => {})
2563
+ }
2564
+ } catch {
2565
+ /* ignore */
2566
+ }
2567
+ } catch (error) {
2568
+ const isAbort =
2569
+ error.name === 'AbortError' ||
2570
+ error.message?.includes('AbortError') ||
2571
+ Boolean(sessionAbortController?.signal?.aborted)
2572
+
2573
+ if (!isAbort) {
2574
+ console.error('[useMarkPlan] Critical ReAct Loop Error:', error)
2575
+ } else {
2576
+ console.log('[useMarkPlan] ReAct loop dihentikan oleh pengguna.')
2577
+ }
2578
+
2579
+ if (!isAbort) {
2580
+ targetPushProcess({
2581
+ id: agenticProcessId,
2582
+ type: 'planning',
2583
+ status: 'failed',
2584
+ data: {
2585
+ steps: [...execSteps],
2586
+ currentStep: execSteps.length,
2587
+ reasoning: `Error: ${error.message}`
2588
+ }
2589
+ })
2590
+ setTimeout(() => {
2591
+ dismissProcess(agenticProcessId)
2592
+ }, 3000)
2593
+
2594
+ if (durableTaskForRecovery && durableTaskForRecovery.status === 'running') {
2595
+ transitionAgentTask(
2596
+ durableTaskForRecovery.id,
2597
+ 'failed',
2598
+ `Uncaught exception: ${error.message}`
2599
+ ).catch(() => {})
2600
+ }
2601
+ } else {
2602
+ dismissProcess(agenticProcessId)
2603
+ try {
2604
+ speechQueue.reset()
2605
+ } catch (_) {}
2606
+ try {
2607
+ const runningTasks = await db.agentTasks.where('status').equals('running').toArray()
2608
+ for (const t of runningTasks) {
2609
+ await transitionAgentTask(
2610
+ t.id,
2611
+ 'cancelled',
2612
+ 'Eksekusi dibatalkan atas permintaan pengguna.'
2613
+ ).catch(() => {})
2614
+ }
2615
+ } catch (_) {}
2616
+ if (durableTaskForRecovery) {
2617
+ transitionAgentTask(
2618
+ durableTaskForRecovery.id,
2619
+ 'cancelled',
2620
+ 'Eksekusi dibatalkan atas permintaan pengguna.'
2621
+ ).catch(() => {})
2622
+ }
2623
+ }
2624
+
2625
+ targetSetChatData((prev) => {
2626
+ const thinkingItem = prev.find((item) => item.isThinking)
2627
+ let updated = prev.filter((item) => !item.isThinking)
2628
+
2629
+ if (isAbort) {
2630
+ updated = updated.map((msg) => {
2631
+ if (!msg.isPlanSteps) return msg
2632
+ if (durableTaskForRecovery && msg.taskId && msg.taskId !== durableTaskForRecovery.id)
2633
+ return msg
2634
+ return {
2635
+ ...msg,
2636
+ taskStatus: 'stopped',
2637
+ plan: (msg.plan || []).map((s) => ({
2638
+ ...s,
2639
+ status: s.status === 'running' ? 'stopped' : s.status
2640
+ }))
2641
+ }
2642
+ })
2643
+ }
2644
+
2645
+ // Ambil riwayat tool yang sempat dieksekusi sebelum di-abort
2646
+ let rawExecutedTools =
2647
+ thinkingItem?.executedTools && thinkingItem.executedTools.length > 0
2648
+ ? [...thinkingItem.executedTools]
2649
+ : [...(executedToolsList || [])]
2650
+
2651
+ // Pastikan in-flight tool yang sedang dieksekusi saat abort tidak hilang
2652
+ if (
2653
+ currentInFlightTool &&
2654
+ !rawExecutedTools.some(
2655
+ (t) =>
2656
+ t.tool === currentInFlightTool.tool &&
2657
+ (t.status === 'running' || t.status === 'stopped')
2658
+ )
2659
+ ) {
2660
+ rawExecutedTools.push(currentInFlightTool)
2661
+ }
2662
+
2663
+ // Tandai tool yang sedang 'running' saat abort menjadi 'stopped'
2664
+ const preservedExecutedTools = rawExecutedTools.map((t) => ({
2665
+ ...t,
2666
+ status: t.status === 'running' ? 'stopped' : t.status
2667
+ }))
2668
+
2669
+ // Ambil riwayat pemikiran (reasoning) yang sempat digenerate
2670
+ const allAccumulated = [...accumulatedThoughts, currentTurnReasoning]
2671
+ .map((t) => (typeof t === 'string' ? t.trim() : ''))
2672
+ .filter(Boolean)
2673
+ const fallbackReasoning =
2674
+ allAccumulated.length > 0
2675
+ ? Array.from(new Set(allAccumulated)).join('\n\n---\n\n')
2676
+ : undefined
2677
+ const preservedReasoning = thinkingItem?.reasoning || fallbackReasoning
2678
+
2679
+ // Ambil konten parsial yang sempat digenerate
2680
+ const rawContent =
2681
+ finalContentAccumulator ||
2682
+ (thinkingItem?.content && !thinkingItem.content.startsWith('Mengeksekusi [')
2683
+ ? thinkingItem.content
2684
+ : '')
2685
+ const partialContent = typeof rawContent === 'string' ? rawContent.trim() : ''
2686
+
2687
+ let abortContent = ''
2688
+ if (isAbort) {
2689
+ abortContent = partialContent
2690
+ ? `${partialContent}\n\n[Eksekusi dihentikan oleh pengguna]`
2691
+ : 'Eksekusi dibatalkan atas permintaan pengguna.'
2692
+ } else {
2693
+ abortContent = partialContent
2694
+ ? `${partialContent}\n\n[Terjadi kendala saat memproses: ${error.message}]`
2695
+ : `Terjadi kendala saat memproses: ${error.message}`
2696
+ }
2697
+
2698
+ const finalUpdated = [
2699
+ ...updated,
2700
+ {
2701
+ role: 'ai',
2702
+ content: abortContent,
2703
+ reasoning: preservedReasoning,
2704
+ executedTools: preservedExecutedTools.length > 0 ? preservedExecutedTools : undefined,
2705
+ mood: isAbort ? 'neutral' : 'sadness',
2706
+ timestamp: getCurrentTimeInfo(),
2707
+ created_at: Date.now()
2708
+ }
2709
+ ]
2710
+
2711
+ if (activeSessionNum === 1) {
2712
+ saveSession('1', finalUpdated).catch((err) => {
2713
+ console.warn('[useMarkPlan] Gagal auto-save abort main thread session 1:', err)
2714
+ })
2715
+ }
2716
+
2717
+ return finalUpdated
2718
+ })
2719
+ } finally {
2720
+ activeSessionsRef.current.delete(activeSessionNum)
2721
+ activeSessionUpdatersRef.current.delete(activeSessionNum)
2722
+ if (removeRunningSessionId) removeRunningSessionId(activeSessionNum)
2723
+ if (activeSessionsRef.current.size === 0) {
2724
+ setIsAgentBusy(false)
2725
+ if (setRunningSessionId) setRunningSessionId(null)
2726
+ }
2727
+ if (activeSessionNum === 1) {
2728
+ setIsLoading(false)
2729
+ }
2730
+ }
2731
+ }
2732
+
2733
+ return {
2734
+ handlePlanningCommand,
2735
+ handleIntervention,
2736
+ handleStop,
2737
+ activeRunningSessionIdRef
2738
+ }
2739
+ }