@agentprojectcontext/apx 1.80.2 → 1.89.1

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 (838) hide show
  1. package/package.json +9 -8
  2. package/skills/apc-context/SKILL.md +14 -7
  3. package/skills/apx/SKILL.md +62 -2
  4. package/skills/apx-mcp/SKILL.md +42 -15
  5. package/src/core/agent/a2a/peers.js +161 -0
  6. package/src/core/agent/a2a/reply.js +379 -27
  7. package/src/core/agent/abort.js +25 -0
  8. package/src/core/agent/agent-model.js +58 -0
  9. package/src/core/agent/agent-tools.js +101 -0
  10. package/src/core/agent/author-line.js +110 -0
  11. package/src/core/agent/build-agent-system.js +52 -18
  12. package/src/core/agent/channels/voice-context.js +17 -2
  13. package/src/core/agent/closing-floor.js +117 -0
  14. package/src/core/agent/constants.js +119 -11
  15. package/src/core/agent/engine-call.js +56 -0
  16. package/src/core/agent/group/run-group-turn.js +218 -0
  17. package/src/core/agent/group/turn-resolver.js +165 -0
  18. package/src/core/agent/judge.js +138 -11
  19. package/src/core/agent/loop/side-effects.js +106 -3
  20. package/src/core/agent/memory.js +31 -18
  21. package/src/core/agent/model-capabilities.js +47 -0
  22. package/src/core/agent/model-router.js +11 -40
  23. package/src/core/agent/prompt-builder.js +47 -0
  24. package/src/core/agent/prompts/channels/a2a.md +12 -0
  25. package/src/core/agent/prompts/channels/code.md +3 -1
  26. package/src/core/agent/prompts/channels/telegram.md +2 -0
  27. package/src/core/agent/prompts/channels/web_code.md +1 -1
  28. package/src/core/agent/prompts/channels/whatsapp.md +37 -0
  29. package/src/core/agent/prompts/core/agent-base.md +2 -0
  30. package/src/core/agent/prompts/discipline/action.md +1 -0
  31. package/src/core/agent/prompts/discipline/two-segment.md +2 -1
  32. package/src/core/agent/prompts/modes/mobility.md +36 -0
  33. package/src/core/agent/retry.js +42 -0
  34. package/src/core/agent/run-agent.js +322 -25
  35. package/src/core/agent/run-turn.js +253 -0
  36. package/src/core/agent/self-memory.js +18 -52
  37. package/src/core/agent/skills/agent-skills.js +118 -0
  38. package/src/core/agent/skills/catalog.js +1 -1
  39. package/src/core/agent/skills/declared.js +13 -0
  40. package/src/core/agent/skills/index-store.js +8 -1
  41. package/src/core/agent/skills/inspector.js +20 -3
  42. package/src/core/agent/skills/loader.js +18 -3
  43. package/src/core/agent/skills/media.js +121 -0
  44. package/src/core/agent/skills/origin.js +61 -0
  45. package/src/core/agent/skills/sections.js +136 -0
  46. package/src/core/agent/super-agent.js +54 -14
  47. package/src/core/agent/tools/handlers/_calendar.js +50 -0
  48. package/src/core/agent/tools/handlers/add-mcp.js +103 -0
  49. package/src/core/agent/tools/handlers/asana-create-task.js +3 -1
  50. package/src/core/agent/tools/handlers/attach-media.js +56 -0
  51. package/src/core/agent/tools/handlers/calendar-create-event.js +38 -0
  52. package/src/core/agent/tools/handlers/calendar-find-slot.js +39 -0
  53. package/src/core/agent/tools/handlers/calendar-list-events.js +39 -0
  54. package/src/core/agent/tools/handlers/calendar-update-event.js +35 -0
  55. package/src/core/agent/tools/handlers/call-agent.js +14 -6
  56. package/src/core/agent/tools/handlers/call-runtime.js +27 -49
  57. package/src/core/agent/tools/handlers/comment-task.js +78 -0
  58. package/src/core/agent/tools/handlers/complete-task.js +60 -0
  59. package/src/core/agent/tools/handlers/configure-agent.js +70 -0
  60. package/src/core/agent/tools/handlers/create-agent.js +68 -0
  61. package/src/core/agent/tools/handlers/create-task.js +41 -3
  62. package/src/core/agent/tools/handlers/import-agent.js +5 -5
  63. package/src/core/agent/tools/handlers/list-mcp-tools.js +83 -0
  64. package/src/core/agent/tools/handlers/list-routines.js +46 -0
  65. package/src/core/agent/tools/handlers/list-tasks.js +8 -1
  66. package/src/core/agent/tools/handlers/mark-commitment.js +57 -0
  67. package/src/core/agent/tools/handlers/read-skill.js +64 -0
  68. package/src/core/agent/tools/handlers/remember-routine.js +2 -8
  69. package/src/core/agent/tools/handlers/remember.js +82 -6
  70. package/src/core/agent/tools/handlers/remove-agent.js +42 -0
  71. package/src/core/agent/tools/handlers/run-routine.js +94 -0
  72. package/src/core/agent/tools/handlers/run-shell.js +44 -2
  73. package/src/core/agent/tools/handlers/send-telegram.js +41 -19
  74. package/src/core/agent/tools/handlers/set-agent-prompt.js +44 -0
  75. package/src/core/agent/tools/handlers/view-media.js +69 -0
  76. package/src/core/agent/tools/handlers/write-agent-memory.js +52 -0
  77. package/src/core/agent/tools/helpers.js +36 -4
  78. package/src/core/agent/tools/names.js +64 -0
  79. package/src/core/agent/tools/registry-bridge.js +24 -1
  80. package/src/core/agent/tools/registry.js +79 -0
  81. package/src/core/agent/tools/tool-call-parser.js +136 -13
  82. package/src/core/agent/vision-bridge.js +69 -0
  83. package/src/core/apc/agent-identity.js +74 -0
  84. package/src/core/apc/agent-rename-refs.js +187 -0
  85. package/src/core/apc/agent-write.js +312 -0
  86. package/src/core/apc/agents-vault.js +1 -1
  87. package/src/core/apc/blob-keys.js +42 -0
  88. package/src/core/apc/frontmatter.js +166 -10
  89. package/src/core/apc/parser.js +33 -83
  90. package/src/core/apc/paths.js +4 -6
  91. package/src/core/apc/project-memory.js +44 -0
  92. package/src/core/apc/projects-helpers.js +33 -0
  93. package/src/core/apc/scaffold.js +36 -16
  94. package/src/core/channels/telegram/ask-callbacks.js +103 -0
  95. package/src/core/channels/telegram/dispatch.js +154 -30
  96. package/src/core/channels/telegram/helpers.js +59 -7
  97. package/src/core/channels/telegram/inbound/audio.js +26 -30
  98. package/src/core/channels/telegram/inbound/file.js +22 -32
  99. package/src/core/channels/telegram/inbound/photo.js +24 -28
  100. package/src/core/channels/telegram/progress-gate.js +76 -0
  101. package/src/core/channels/telegram/reply.js +151 -43
  102. package/src/core/channels/telegram/voice-note.js +150 -0
  103. package/src/core/config/index.js +53 -54
  104. package/src/core/config/paths.js +117 -31
  105. package/src/core/config/redact.js +89 -4
  106. package/src/core/config/secret-values.js +2 -4
  107. package/src/core/constants/actors.js +5 -0
  108. package/src/core/constants/channels.js +3 -1
  109. package/src/core/constants/task-categories.js +73 -0
  110. package/src/core/constants/task-fields.js +28 -0
  111. package/src/core/engines/_globs.js +33 -0
  112. package/src/core/engines/anthropic.js +19 -1
  113. package/src/core/engines/catalog.js +20 -0
  114. package/src/core/engines/gemini.js +84 -16
  115. package/src/core/engines/index.js +19 -3
  116. package/src/core/engines/mock.js +145 -1
  117. package/src/core/engines/openai-compatible.js +150 -15
  118. package/src/core/engines/presets.js +36 -9
  119. package/src/core/engines/zen.js +131 -0
  120. package/src/core/events/bus.js +140 -0
  121. package/src/core/events/mascot-notify.js +156 -0
  122. package/src/core/http-tools/browser.js +366 -36
  123. package/src/core/http-tools/catalog.js +206 -22
  124. package/src/core/http-tools/grep.js +62 -7
  125. package/src/core/http-tools/registry.js +3 -1
  126. package/src/core/i18n/en.js +34 -3
  127. package/src/core/i18n/es.js +36 -4
  128. package/src/core/i18n/index.js +5 -5
  129. package/src/core/i18n/pt.js +32 -3
  130. package/src/core/identity/self.js +14 -1
  131. package/src/core/images/engines/a1111.js +219 -0
  132. package/src/core/images/engines/index.js +187 -0
  133. package/src/core/images/engines/mock.js +74 -0
  134. package/src/core/images/engines/openai.js +130 -0
  135. package/src/core/images/engines/sdcpp.js +204 -0
  136. package/src/core/images/engines/shared.js +218 -0
  137. package/src/core/images/generate.js +260 -0
  138. package/src/core/integrations/catalog.js +8 -3
  139. package/src/core/integrations/plugins/_google-oauth.js +179 -0
  140. package/src/core/integrations/plugins/asana.js +7 -3
  141. package/src/core/integrations/plugins/calendar.js +404 -0
  142. package/src/core/integrations/store.js +2 -0
  143. package/src/core/memory/broker.js +16 -1
  144. package/src/core/memory/compactor.js +19 -3
  145. package/src/core/memory/consolidate.js +2 -2
  146. package/src/core/memory/dated-log.js +66 -0
  147. package/src/core/memory/embed-engines/custom.js +59 -0
  148. package/src/core/memory/embed-engines/gemini.js +9 -3
  149. package/src/core/memory/embed-engines/index.js +87 -15
  150. package/src/core/memory/embeddings.js +52 -24
  151. package/src/core/memory/index.js +13 -3
  152. package/src/core/memory/indexer.js +83 -27
  153. package/src/core/memory/prune.js +182 -0
  154. package/src/core/memory/store.js +22 -0
  155. package/src/core/memory/summarizer.js +72 -8
  156. package/src/core/mobility/answer.js +80 -0
  157. package/src/core/mobility/geofence.js +667 -0
  158. package/src/core/mobility/osm-route.js +282 -0
  159. package/src/core/mobility/preferences.js +91 -0
  160. package/src/core/mobility/state.js +245 -0
  161. package/src/core/mobility/trip-event.js +384 -0
  162. package/src/core/net/tailscale.js +168 -0
  163. package/src/core/profiles/bundled/secretary/PROFILE.md +32 -42
  164. package/src/core/profiles/bundled/secretary/config.schema.es.json +68 -0
  165. package/src/core/profiles/bundled/secretary/config.schema.json +30 -2
  166. package/src/core/profiles/bundled/secretary/profile.json +3 -1
  167. package/src/core/profiles/bundled/secretary/routines/a2a-sweep.json +20 -0
  168. package/src/core/profiles/bundled/secretary/routines/day-close.json +4 -1
  169. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -1
  170. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -2
  171. package/src/core/profiles/lifecycle.js +243 -19
  172. package/src/core/profiles/paths.js +12 -0
  173. package/src/core/profiles/store.js +47 -0
  174. package/src/core/routines/active-runs.js +213 -0
  175. package/src/core/routines/delivery.js +612 -0
  176. package/src/core/routines/header.js +88 -0
  177. package/src/core/routines/index.js +26 -0
  178. package/src/core/routines/run-log.js +78 -0
  179. package/src/core/routines/runner.js +629 -66
  180. package/src/core/routines/signals.js +75 -0
  181. package/src/core/runtime-skills/apc-context/SKILL.md +14 -7
  182. package/src/core/runtime-skills/apx/SKILL.md +93 -1
  183. package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
  184. package/src/core/runtime-skills/apx-agent/SKILL.md +134 -19
  185. package/src/core/runtime-skills/apx-commitment/SKILL.md +134 -0
  186. package/src/core/runtime-skills/apx-image/SKILL.md +60 -0
  187. package/src/core/runtime-skills/apx-integrations/SKILL.md +82 -0
  188. package/src/core/runtime-skills/apx-mcp/SKILL.md +45 -14
  189. package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +4 -4
  190. package/src/core/runtime-skills/apx-profile/SKILL.md +27 -2
  191. package/src/core/runtime-skills/apx-project/SKILL.md +23 -2
  192. package/src/core/runtime-skills/apx-routine/SKILL.md +40 -17
  193. package/src/core/runtime-skills/apx-runtime/SKILL.md +124 -14
  194. package/src/core/runtime-skills/apx-sessions/SKILL.md +7 -6
  195. package/src/core/runtime-skills/apx-skill-builder/SKILL.md +15 -13
  196. package/src/core/runtime-skills/apx-task/SKILL.md +79 -26
  197. package/src/core/runtime-skills/apx-telegram/SKILL.md +12 -12
  198. package/src/core/runtime-skills/apx-transcribe/SKILL.md +56 -0
  199. package/src/core/runtime-skills/apx-voice/SKILL.md +27 -8
  200. package/src/core/runtime-skills/claude-code/SKILL.md +8 -0
  201. package/src/core/runtime-skills/openrouter/SKILL.md +8 -2
  202. package/src/core/runtimes/_spawn.js +9 -1
  203. package/src/core/runtimes/claude-code.js +11 -2
  204. package/src/core/runtimes/codex.js +44 -10
  205. package/src/core/runtimes/index.js +17 -2
  206. package/src/core/runtimes/opencode.js +88 -9
  207. package/src/core/runtimes/outcome.js +119 -0
  208. package/src/core/sessions/index.js +193 -1
  209. package/src/core/stores/agent-inbox.js +109 -23
  210. package/src/core/stores/code-sessions.js +21 -0
  211. package/src/core/stores/commitments.js +27 -3
  212. package/src/core/stores/conversations.js +220 -20
  213. package/src/core/stores/deliveries.js +177 -0
  214. package/src/core/stores/media-archive.js +228 -0
  215. package/src/core/stores/messages.js +1096 -112
  216. package/src/core/stores/organization.js +34 -0
  217. package/src/core/stores/project-memory.js +71 -0
  218. package/src/core/stores/routine-memory.js +16 -8
  219. package/src/core/stores/routines.js +27 -49
  220. package/src/core/stores/sessions.js +4 -8
  221. package/src/core/stores/tasks.js +212 -18
  222. package/src/core/stores/turn-record.js +209 -0
  223. package/src/core/tasks/columns.js +136 -0
  224. package/src/core/tasks/comment-turn.js +258 -0
  225. package/src/core/util/time.js +13 -0
  226. package/src/core/voice/engines/index.js +54 -1
  227. package/src/core/voice/engines/openai.js +50 -4
  228. package/src/core/voice/opus.js +67 -0
  229. package/src/core/voice/pronounceable.js +43 -0
  230. package/src/core/voice/transcription.js +4 -0
  231. package/src/core/voice/tts.js +114 -24
  232. package/src/host/daemon/active-turns.js +188 -0
  233. package/src/host/daemon/api/admin-config.js +9 -0
  234. package/src/host/daemon/api/agents.js +99 -90
  235. package/src/host/daemon/api/artifact-preview.js +7 -6
  236. package/src/host/daemon/api/artifacts.js +3 -2
  237. package/src/host/daemon/api/code.js +55 -9
  238. package/src/host/daemon/api/commitments.js +11 -0
  239. package/src/host/daemon/api/confirm.js +3 -2
  240. package/src/host/daemon/api/conversations.js +543 -35
  241. package/src/host/daemon/api/deck.js +9 -8
  242. package/src/host/daemon/api/deliveries.js +33 -0
  243. package/src/host/daemon/api/desktop.js +5 -4
  244. package/src/host/daemon/api/embeddings.js +15 -6
  245. package/src/host/daemon/api/engines.js +59 -8
  246. package/src/host/daemon/api/exec.js +437 -121
  247. package/src/host/daemon/api/groups.js +215 -0
  248. package/src/host/daemon/api/images.js +107 -0
  249. package/src/host/daemon/api/inbox.js +123 -2
  250. package/src/host/daemon/api/integrations.js +64 -2
  251. package/src/host/daemon/api/mcps.js +7 -6
  252. package/src/host/daemon/api/media.js +283 -0
  253. package/src/host/daemon/api/mobility.js +177 -0
  254. package/src/host/daemon/api/net.js +182 -0
  255. package/src/host/daemon/api/pairing.js +38 -5
  256. package/src/host/daemon/api/profiles.js +30 -2
  257. package/src/host/daemon/api/routines.js +36 -4
  258. package/src/host/daemon/api/runtimes.js +74 -39
  259. package/src/host/daemon/api/sessions-search.js +7 -2
  260. package/src/host/daemon/api/sessions.js +44 -2
  261. package/src/host/daemon/api/shared.js +127 -20
  262. package/src/host/daemon/api/skills.js +11 -4
  263. package/src/host/daemon/api/super-agent.js +384 -53
  264. package/src/host/daemon/api/tasks.js +180 -10
  265. package/src/host/daemon/api/telegram.js +11 -10
  266. package/src/host/daemon/api/thread-faces.js +159 -0
  267. package/src/host/daemon/api/top-level.js +3 -3
  268. package/src/host/daemon/api/transcribe.js +11 -10
  269. package/src/host/daemon/api/tts.js +97 -4
  270. package/src/host/daemon/api/turn-abort.js +23 -0
  271. package/src/host/daemon/api/turns.js +49 -0
  272. package/src/host/daemon/api/voice.js +5 -4
  273. package/src/host/daemon/api/web.js +98 -0
  274. package/src/host/daemon/api.js +39 -0
  275. package/src/host/daemon/delivery-sweep.js +58 -0
  276. package/src/host/daemon/desktop-ws.js +5 -23
  277. package/src/host/daemon/events-ws.js +273 -0
  278. package/src/host/daemon/index.js +59 -15
  279. package/src/host/daemon/plugins/desktop/index.js +17 -4
  280. package/src/host/daemon/plugins/telegram/index.js +78 -40
  281. package/src/host/daemon/pty-bridge.py +99 -0
  282. package/src/host/daemon/routines-scheduler.js +1 -0
  283. package/src/host/daemon/smoke.js +1 -2
  284. package/src/host/daemon/terminal-ws.js +132 -0
  285. package/src/host/daemon/tts-keepwarm.js +87 -0
  286. package/src/host/daemon/wakeup.js +11 -6
  287. package/src/host/daemon/whisper-server.js +44 -0
  288. package/src/host/daemon/whisper-server.py +70 -13
  289. package/src/host/daemon/ws-auth.js +28 -0
  290. package/src/interfaces/android/app/build.gradle +42 -0
  291. package/src/interfaces/android/app/proguard-rules.pro +4 -0
  292. package/src/interfaces/android/app/src/main/AndroidManifest.xml +139 -0
  293. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AlertScreen.java +139 -0
  294. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AndroidAutoDetector.java +106 -0
  295. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxCarAppService.java +57 -0
  296. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxPreferences.java +165 -0
  297. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarAlertStore.java +84 -0
  298. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageActionReceiver.java +47 -0
  299. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageNotification.java +152 -0
  300. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonAddress.java +35 -0
  301. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonClient.java +339 -0
  302. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DeviceLocation.java +38 -0
  303. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ErrandScreen.java +127 -0
  304. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MainActivity.java +872 -0
  305. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationDetector.java +63 -0
  306. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationListenerService.java +453 -0
  307. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsShareIntentParser.java +93 -0
  308. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotBlobCatalog.java +66 -0
  309. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotOverlayService.java +448 -0
  310. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotView.java +249 -0
  311. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MessageFrameParser.java +240 -0
  312. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/NotifyChannels.java +69 -0
  313. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityActionReceiver.java +78 -0
  314. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityNotification.java +161 -0
  315. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/StartupReceiver.java +57 -0
  316. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelEventGate.java +62 -0
  317. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelStatusBanner.java +116 -0
  318. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripLocationService.java +272 -0
  319. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripPlacesSheet.java +203 -0
  320. package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripScreen.java +182 -0
  321. package/src/interfaces/android/app/src/main/res/drawable/app_background.xml +12 -0
  322. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_app.xml +10 -0
  323. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_launcher_foreground.xml +12 -0
  324. package/src/interfaces/android/app/src/main/res/drawable/ic_apx_notification.xml +22 -0
  325. package/src/interfaces/android/app/src/main/res/drawable/ic_trip_car.xml +37 -0
  326. package/src/interfaces/android/app/src/main/res/drawable-nodpi/apx_logo.png +0 -0
  327. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_amatista.png +0 -0
  328. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_aqua.png +0 -0
  329. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_brasa.png +0 -0
  330. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_campana.png +0 -0
  331. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_caracol.png +0 -0
  332. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_carbon.png +0 -0
  333. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cobalto.png +0 -0
  334. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cometa.png +0 -0
  335. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_coral.png +0 -0
  336. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cubi.png +0 -0
  337. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_eclipse.png +0 -0
  338. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_faro.png +0 -0
  339. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_gajo.png +0 -0
  340. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_iris.png +0 -0
  341. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_kiwi.png +0 -0
  342. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_laguna.png +0 -0
  343. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_lila.png +0 -0
  344. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_menta.png +0 -0
  345. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_nimbo.png +0 -0
  346. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_noche.png +0 -0
  347. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_onyx.png +0 -0
  348. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_orbita.png +0 -0
  349. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_ovni.png +0 -0
  350. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_papa.png +0 -0
  351. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_parche.png +0 -0
  352. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_peon.png +0 -0
  353. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_perla.png +0 -0
  354. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_pino.png +0 -0
  355. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_quad.png +0 -0
  356. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_rubi.png +0 -0
  357. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_saturno.png +0 -0
  358. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trebol.png +0 -0
  359. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trino.png +0 -0
  360. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_volcan.png +0 -0
  361. package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_zafiro.png +0 -0
  362. package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  363. package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  364. package/src/interfaces/android/app/src/main/res/raw/apx_notification.mp3 +0 -0
  365. package/src/interfaces/android/app/src/main/res/values/colors.xml +4 -0
  366. package/src/interfaces/android/app/src/main/res/values/strings.xml +25 -0
  367. package/src/interfaces/android/app/src/main/res/values/styles.xml +10 -0
  368. package/src/interfaces/android/app/src/main/res/values-v31/styles.xml +14 -0
  369. package/src/interfaces/android/app/src/main/res/xml/automotive_app_desc.xml +9 -0
  370. package/src/interfaces/android/app/src/main/res/xml/network_security_config.xml +10 -0
  371. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/AndroidAutoDetectorTest.java +85 -0
  372. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/CarAlertStoreTest.java +104 -0
  373. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/DaemonAddressTest.java +23 -0
  374. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsNavigationDetectorTest.java +69 -0
  375. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsShareIntentParserTest.java +31 -0
  376. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MessageFrameParserTest.java +180 -0
  377. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TravelEventGateTest.java +64 -0
  378. package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TripPlacesSheetTest.java +66 -0
  379. package/src/interfaces/android/build.gradle +3 -0
  380. package/src/interfaces/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  381. package/src/interfaces/android/gradle/wrapper/gradle-wrapper.properties +9 -0
  382. package/src/interfaces/android/gradle.properties +3 -0
  383. package/src/interfaces/android/gradlew +248 -0
  384. package/src/interfaces/android/gradlew.bat +82 -0
  385. package/src/interfaces/android/settings.gradle +18 -0
  386. package/src/interfaces/cli/commands/a2a.js +145 -4
  387. package/src/interfaces/cli/commands/agent.js +247 -22
  388. package/src/interfaces/cli/commands/code.js +1 -1
  389. package/src/interfaces/cli/commands/commitment.js +5 -0
  390. package/src/interfaces/cli/commands/config.js +85 -6
  391. package/src/interfaces/cli/commands/exec.js +90 -1
  392. package/src/interfaces/cli/commands/image.js +314 -0
  393. package/src/interfaces/cli/commands/mcp.js +87 -23
  394. package/src/interfaces/cli/commands/memory.js +35 -22
  395. package/src/interfaces/cli/commands/panel.js +94 -0
  396. package/src/interfaces/cli/commands/profile.js +14 -0
  397. package/src/interfaces/cli/commands/project.js +29 -0
  398. package/src/interfaces/cli/commands/routine.js +45 -10
  399. package/src/interfaces/cli/commands/runtime.js +30 -7
  400. package/src/interfaces/cli/commands/session.js +90 -38
  401. package/src/interfaces/cli/commands/sessions.js +35 -2
  402. package/src/interfaces/cli/commands/skills.js +15 -9
  403. package/src/interfaces/cli/commands/status.js +2 -3
  404. package/src/interfaces/cli/commands/task.js +54 -5
  405. package/src/interfaces/cli/commands/telegram.js +12 -4
  406. package/src/interfaces/cli/commands/transcribe.js +177 -0
  407. package/src/interfaces/cli/help/index.js +311 -72
  408. package/src/interfaces/cli/http.js +61 -13
  409. package/src/interfaces/cli/index.js +1 -1
  410. package/src/interfaces/cli/routes/agent.js +5 -4
  411. package/src/interfaces/cli/routes/commitment.js +3 -2
  412. package/src/interfaces/cli/routes/config.js +7 -2
  413. package/src/interfaces/cli/routes/image.js +11 -0
  414. package/src/interfaces/cli/routes/index.js +2 -0
  415. package/src/interfaces/cli/routes/log.js +1 -1
  416. package/src/interfaces/cli/routes/memory.js +3 -2
  417. package/src/interfaces/cli/routes/panel.js +15 -2
  418. package/src/interfaces/cli/routes/profile.js +3 -2
  419. package/src/interfaces/cli/routes/transcribe.js +10 -0
  420. package/src/interfaces/cli/stdin.js +29 -0
  421. package/src/interfaces/desktop/assets/blobs/amatista.png +0 -0
  422. package/src/interfaces/desktop/assets/blobs/aqua.png +0 -0
  423. package/src/interfaces/desktop/assets/blobs/brasa.png +0 -0
  424. package/src/interfaces/desktop/assets/blobs/campana.png +0 -0
  425. package/src/interfaces/desktop/assets/blobs/caracol.png +0 -0
  426. package/src/interfaces/desktop/assets/blobs/carbon.png +0 -0
  427. package/src/interfaces/desktop/assets/blobs/cobalto.png +0 -0
  428. package/src/interfaces/desktop/assets/blobs/cometa.png +0 -0
  429. package/src/interfaces/desktop/assets/blobs/coral.png +0 -0
  430. package/src/interfaces/desktop/assets/blobs/cubi.png +0 -0
  431. package/src/interfaces/desktop/assets/blobs/eclipse.png +0 -0
  432. package/src/interfaces/desktop/assets/blobs/faro.png +0 -0
  433. package/src/interfaces/desktop/assets/blobs/gajo.png +0 -0
  434. package/src/interfaces/desktop/assets/blobs/iris.png +0 -0
  435. package/src/interfaces/desktop/assets/blobs/kiwi.png +0 -0
  436. package/src/interfaces/desktop/assets/blobs/laguna.png +0 -0
  437. package/src/interfaces/desktop/assets/blobs/lila.png +0 -0
  438. package/src/interfaces/desktop/assets/blobs/menta.png +0 -0
  439. package/src/interfaces/desktop/assets/blobs/nimbo.png +0 -0
  440. package/src/interfaces/desktop/assets/blobs/noche.png +0 -0
  441. package/src/interfaces/desktop/assets/blobs/onyx.png +0 -0
  442. package/src/interfaces/desktop/assets/blobs/orbita.png +0 -0
  443. package/src/interfaces/desktop/assets/blobs/ovni.png +0 -0
  444. package/src/interfaces/desktop/assets/blobs/papa.png +0 -0
  445. package/src/interfaces/desktop/assets/blobs/parche.png +0 -0
  446. package/src/interfaces/desktop/assets/blobs/peon.png +0 -0
  447. package/src/interfaces/desktop/assets/blobs/perla.png +0 -0
  448. package/src/interfaces/desktop/assets/blobs/pino.png +0 -0
  449. package/src/interfaces/desktop/assets/blobs/quad.png +0 -0
  450. package/src/interfaces/desktop/assets/blobs/rubi.png +0 -0
  451. package/src/interfaces/desktop/assets/blobs/saturno.png +0 -0
  452. package/src/interfaces/desktop/assets/blobs/trebol.png +0 -0
  453. package/src/interfaces/desktop/assets/blobs/trino.png +0 -0
  454. package/src/interfaces/desktop/assets/blobs/volcan.png +0 -0
  455. package/src/interfaces/desktop/assets/blobs/zafiro.png +0 -0
  456. package/src/interfaces/desktop/assets/notification.mp3 +0 -0
  457. package/src/interfaces/desktop/main.js +473 -29
  458. package/src/interfaces/desktop/mascot-blobs.js +41 -0
  459. package/src/interfaces/desktop/mascot-preload.js +32 -0
  460. package/src/interfaces/desktop/mascot.css +186 -0
  461. package/src/interfaces/desktop/mascot.html +40 -0
  462. package/src/interfaces/desktop/mascot.js +163 -0
  463. package/src/interfaces/desktop/preload.js +10 -0
  464. package/src/interfaces/desktop/renderer.js +308 -48
  465. package/src/interfaces/desktop/style.css +34 -0
  466. package/src/interfaces/mcp-server/index.js +87 -0
  467. package/src/interfaces/tui/_shims/util-locale.ts +0 -2
  468. package/src/interfaces/tui/app.tsx +0 -6
  469. package/src/interfaces/tui/attach.ts +0 -1
  470. package/src/interfaces/tui/context/sdk-apx.tsx +0 -4
  471. package/src/interfaces/tui/plugin/api.tsx +0 -3
  472. package/src/interfaces/tui/plugin/runtime.ts +0 -3
  473. package/src/interfaces/tui/thread.ts +0 -16
  474. package/src/interfaces/web/README.md +22 -14
  475. package/src/interfaces/web/dist/assets/claude-XtpLmGtF.webp +0 -0
  476. package/src/interfaces/web/dist/assets/codex-DExPUgvh.webp +0 -0
  477. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css +32 -0
  478. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.br +0 -0
  479. package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.gz +0 -0
  480. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js +1074 -0
  481. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.br +0 -0
  482. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.gz +0 -0
  483. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map +1 -0
  484. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.br +0 -0
  485. package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.gz +0 -0
  486. package/src/interfaces/web/dist/favicon/dark/site.webmanifest +12 -6
  487. package/src/interfaces/web/dist/favicon/white/site.webmanifest +12 -6
  488. package/src/interfaces/web/dist/index.html +22 -6
  489. package/src/interfaces/web/dist/index.html.br +0 -0
  490. package/src/interfaces/web/dist/index.html.gz +0 -0
  491. package/src/interfaces/web/dist/manifest.webmanifest +25 -0
  492. package/src/interfaces/web/dist/modules/blobs/amatista.png +0 -0
  493. package/src/interfaces/web/dist/modules/blobs/aqua.png +0 -0
  494. package/src/interfaces/web/dist/modules/blobs/brasa.png +0 -0
  495. package/src/interfaces/web/dist/modules/blobs/campana.png +0 -0
  496. package/src/interfaces/web/dist/modules/blobs/caracol.png +0 -0
  497. package/src/interfaces/web/dist/modules/blobs/carbon.png +0 -0
  498. package/src/interfaces/web/dist/modules/blobs/cobalto.png +0 -0
  499. package/src/interfaces/web/dist/modules/blobs/cometa.png +0 -0
  500. package/src/interfaces/web/dist/modules/blobs/coral.png +0 -0
  501. package/src/interfaces/web/dist/modules/blobs/cubi.png +0 -0
  502. package/src/interfaces/web/dist/modules/blobs/eclipse.png +0 -0
  503. package/src/interfaces/web/dist/modules/blobs/faro.png +0 -0
  504. package/src/interfaces/web/dist/modules/blobs/gajo.png +0 -0
  505. package/src/interfaces/web/dist/modules/blobs/iris.png +0 -0
  506. package/src/interfaces/web/dist/modules/blobs/kiwi.png +0 -0
  507. package/src/interfaces/web/dist/modules/blobs/laguna.png +0 -0
  508. package/src/interfaces/web/dist/modules/blobs/lila.png +0 -0
  509. package/src/interfaces/web/dist/modules/blobs/menta.png +0 -0
  510. package/src/interfaces/web/dist/modules/blobs/nimbo.png +0 -0
  511. package/src/interfaces/web/dist/modules/blobs/noche.png +0 -0
  512. package/src/interfaces/web/dist/modules/blobs/onyx.png +0 -0
  513. package/src/interfaces/web/dist/modules/blobs/orbita.png +0 -0
  514. package/src/interfaces/web/dist/modules/blobs/ovni.png +0 -0
  515. package/src/interfaces/web/dist/modules/blobs/papa.png +0 -0
  516. package/src/interfaces/web/dist/modules/blobs/parche.png +0 -0
  517. package/src/interfaces/web/dist/modules/blobs/peon.png +0 -0
  518. package/src/interfaces/web/dist/modules/blobs/perla.png +0 -0
  519. package/src/interfaces/web/dist/modules/blobs/pino.png +0 -0
  520. package/src/interfaces/web/dist/modules/blobs/quad.png +0 -0
  521. package/src/interfaces/web/dist/modules/blobs/rubi.png +0 -0
  522. package/src/interfaces/web/dist/modules/blobs/saturno.png +0 -0
  523. package/src/interfaces/web/dist/modules/blobs/trebol.png +0 -0
  524. package/src/interfaces/web/dist/modules/blobs/trino.png +0 -0
  525. package/src/interfaces/web/dist/modules/blobs/volcan.png +0 -0
  526. package/src/interfaces/web/dist/modules/blobs/zafiro.png +0 -0
  527. package/src/interfaces/web/dist/sw.js +106 -0
  528. package/src/interfaces/web/dist/sw.js.br +0 -0
  529. package/src/interfaces/web/dist/sw.js.gz +0 -0
  530. package/src/interfaces/web/eslint.config.js +83 -0
  531. package/src/interfaces/web/index.html +20 -4
  532. package/src/interfaces/web/package-lock.json +1651 -1280
  533. package/src/interfaces/web/package.json +9 -10
  534. package/src/interfaces/web/pnpm-lock.yaml +867 -869
  535. package/src/interfaces/web/public/favicon/dark/site.webmanifest +12 -6
  536. package/src/interfaces/web/public/favicon/white/site.webmanifest +12 -6
  537. package/src/interfaces/web/public/manifest.webmanifest +25 -0
  538. package/src/interfaces/web/public/modules/blobs/amatista.png +0 -0
  539. package/src/interfaces/web/public/modules/blobs/aqua.png +0 -0
  540. package/src/interfaces/web/public/modules/blobs/brasa.png +0 -0
  541. package/src/interfaces/web/public/modules/blobs/campana.png +0 -0
  542. package/src/interfaces/web/public/modules/blobs/caracol.png +0 -0
  543. package/src/interfaces/web/public/modules/blobs/carbon.png +0 -0
  544. package/src/interfaces/web/public/modules/blobs/cobalto.png +0 -0
  545. package/src/interfaces/web/public/modules/blobs/cometa.png +0 -0
  546. package/src/interfaces/web/public/modules/blobs/coral.png +0 -0
  547. package/src/interfaces/web/public/modules/blobs/cubi.png +0 -0
  548. package/src/interfaces/web/public/modules/blobs/eclipse.png +0 -0
  549. package/src/interfaces/web/public/modules/blobs/faro.png +0 -0
  550. package/src/interfaces/web/public/modules/blobs/gajo.png +0 -0
  551. package/src/interfaces/web/public/modules/blobs/iris.png +0 -0
  552. package/src/interfaces/web/public/modules/blobs/kiwi.png +0 -0
  553. package/src/interfaces/web/public/modules/blobs/laguna.png +0 -0
  554. package/src/interfaces/web/public/modules/blobs/lila.png +0 -0
  555. package/src/interfaces/web/public/modules/blobs/menta.png +0 -0
  556. package/src/interfaces/web/public/modules/blobs/nimbo.png +0 -0
  557. package/src/interfaces/web/public/modules/blobs/noche.png +0 -0
  558. package/src/interfaces/web/public/modules/blobs/onyx.png +0 -0
  559. package/src/interfaces/web/public/modules/blobs/orbita.png +0 -0
  560. package/src/interfaces/web/public/modules/blobs/ovni.png +0 -0
  561. package/src/interfaces/web/public/modules/blobs/papa.png +0 -0
  562. package/src/interfaces/web/public/modules/blobs/parche.png +0 -0
  563. package/src/interfaces/web/public/modules/blobs/peon.png +0 -0
  564. package/src/interfaces/web/public/modules/blobs/perla.png +0 -0
  565. package/src/interfaces/web/public/modules/blobs/pino.png +0 -0
  566. package/src/interfaces/web/public/modules/blobs/quad.png +0 -0
  567. package/src/interfaces/web/public/modules/blobs/rubi.png +0 -0
  568. package/src/interfaces/web/public/modules/blobs/saturno.png +0 -0
  569. package/src/interfaces/web/public/modules/blobs/trebol.png +0 -0
  570. package/src/interfaces/web/public/modules/blobs/trino.png +0 -0
  571. package/src/interfaces/web/public/modules/blobs/volcan.png +0 -0
  572. package/src/interfaces/web/public/modules/blobs/zafiro.png +0 -0
  573. package/src/interfaces/web/public/sw.js +106 -0
  574. package/src/interfaces/web/src/App.tsx +106 -10
  575. package/src/interfaces/web/src/assets/cli/claude.webp +0 -0
  576. package/src/interfaces/web/src/assets/cli/codex.webp +0 -0
  577. package/src/interfaces/web/src/assets/cli/cursor.svg +1 -0
  578. package/src/interfaces/web/src/assets/cli/opencode.png +0 -0
  579. package/src/interfaces/web/src/components/Combobox.tsx +201 -0
  580. package/src/interfaces/web/src/components/MobileHint.tsx +95 -0
  581. package/src/interfaces/web/src/components/MobileLinkDialog.tsx +156 -0
  582. package/src/interfaces/web/src/components/ModelCombobox.tsx +21 -113
  583. package/src/interfaces/web/src/components/{routines/ReadOnlyBlock.tsx → ReadOnlyBlock.tsx} +13 -4
  584. package/src/interfaces/web/src/components/Roby.tsx +5 -3
  585. package/src/interfaces/web/src/components/RobyBubble.tsx +23 -5
  586. package/src/interfaces/web/src/components/RowMenu.tsx +46 -0
  587. package/src/interfaces/web/src/components/TimezoneSelect.tsx +4 -1
  588. package/src/interfaces/web/src/components/Toast.tsx +50 -14
  589. package/src/interfaces/web/src/components/UiSelect.tsx +12 -5
  590. package/src/interfaces/web/src/components/agents/AgentAvatar.tsx +171 -0
  591. package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +42 -11
  592. package/src/interfaces/web/src/components/agents/AgentModelBadge.tsx +157 -0
  593. package/src/interfaces/web/src/components/agents/AgentModelSelect.tsx +57 -0
  594. package/src/interfaces/web/src/components/agents/AgentSkillsPicker.tsx +115 -0
  595. package/src/interfaces/web/src/components/agents/BlobAvatar.tsx +50 -0
  596. package/src/interfaces/web/src/components/agents/blobPresets.ts +50 -0
  597. package/src/interfaces/web/src/components/agents/modelCatalog.ts +66 -0
  598. package/src/interfaces/web/src/components/chat/ActionGroup.tsx +162 -0
  599. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +4 -3
  600. package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +1 -1
  601. package/src/interfaces/web/src/components/chat/Attachment.tsx +220 -0
  602. package/src/interfaces/web/src/components/chat/ChatList.tsx +226 -51
  603. package/src/interfaces/web/src/components/chat/ChatRowActivity.tsx +45 -0
  604. package/src/interfaces/web/src/components/chat/Composer.tsx +672 -23
  605. package/src/interfaces/web/src/components/chat/ComposerCommands.tsx +175 -0
  606. package/src/interfaces/web/src/components/chat/ComposerSuggest.tsx +215 -0
  607. package/src/interfaces/web/src/components/chat/ContextBar.tsx +133 -65
  608. package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +107 -47
  609. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +460 -119
  610. package/src/interfaces/web/src/components/chat/MessageList.tsx +165 -18
  611. package/src/interfaces/web/src/components/chat/ReasoningBlock.tsx +58 -0
  612. package/src/interfaces/web/src/components/chat/SessionPicker.tsx +182 -0
  613. package/src/interfaces/web/src/components/chat/SkillTrace.tsx +183 -0
  614. package/src/interfaces/web/src/components/chat/ToolCall.tsx +11 -11
  615. package/src/interfaces/web/src/components/code/CodeContextTab.tsx +87 -53
  616. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +11 -52
  617. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +5 -4
  618. package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +31 -7
  619. package/src/interfaces/web/src/components/code/CodeSessionList.tsx +32 -11
  620. package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +33 -3
  621. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +5 -4
  622. package/src/interfaces/web/src/components/code/NewCodeSessionDialog.tsx +164 -0
  623. package/src/interfaces/web/src/components/commitments/CommitmentDetail.tsx +162 -0
  624. package/src/interfaces/web/src/components/commitments/CommitmentFormDialog.tsx +158 -0
  625. package/src/interfaces/web/src/components/commitments/CommitmentList.tsx +194 -0
  626. package/src/interfaces/web/src/components/commitments/commitmentState.tsx +46 -0
  627. package/src/interfaces/web/src/components/common/BulkActionBar.tsx +60 -0
  628. package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +4 -1
  629. package/src/interfaces/web/src/components/common/SelectCheckbox.tsx +40 -0
  630. package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
  631. package/src/interfaces/web/src/components/cron/CronPicker.tsx +1 -1
  632. package/src/interfaces/web/src/components/files/FileBrowser.tsx +1 -1
  633. package/src/interfaces/web/src/components/files/FileTree.tsx +6 -4
  634. package/src/interfaces/web/src/components/files/FileViewer.tsx +9 -5
  635. package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +45 -9
  636. package/src/interfaces/web/src/components/images/ImageDefaultsCard.tsx +93 -0
  637. package/src/interfaces/web/src/components/images/ImageProviderList.tsx +142 -0
  638. package/src/interfaces/web/src/components/images/ImageProviderModal.tsx +263 -0
  639. package/src/interfaces/web/src/components/images/ImageTestCard.tsx +142 -0
  640. package/src/interfaces/web/src/components/inbox/ChannelFilter.tsx +178 -0
  641. package/src/interfaces/web/src/components/inbox/InboxList.tsx +77 -81
  642. package/src/interfaces/web/src/components/inbox/InboxRowItem.tsx +140 -0
  643. package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +2 -2
  644. package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +208 -30
  645. package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +1 -1
  646. package/src/interfaces/web/src/components/layout/ProjectAvatar.tsx +39 -24
  647. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +253 -58
  648. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +94 -47
  649. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +202 -90
  650. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +28 -6
  651. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +16 -5
  652. package/src/interfaces/web/src/components/routines/RoutineList.tsx +49 -9
  653. package/src/interfaces/web/src/components/routines/VarTextarea.tsx +1 -1
  654. package/src/interfaces/web/src/components/routines/runChat.ts +121 -0
  655. package/src/interfaces/web/src/components/sessions/SessionDetail.tsx +148 -0
  656. package/src/interfaces/web/src/components/sessions/SessionTerminal.tsx +98 -0
  657. package/src/interfaces/web/src/components/settings/AccessPanel.tsx +205 -0
  658. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +246 -101
  659. package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +36 -2
  660. package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +19 -6
  661. package/src/interfaces/web/src/components/settings/EmbedProviderList.tsx +274 -0
  662. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +26 -6
  663. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +118 -119
  664. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +4 -3
  665. package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +15 -4
  666. package/src/interfaces/web/src/components/settings/PanelPrefs.tsx +374 -0
  667. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +70 -2
  668. package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +2 -1
  669. package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +102 -43
  670. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +76 -21
  671. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +10 -3
  672. package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +8 -0
  673. package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +11 -2
  674. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +19 -8
  675. package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +12 -2
  676. package/src/interfaces/web/src/components/settings/WebPanel.tsx +22 -31
  677. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +15 -3
  678. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +97 -66
  679. package/src/interfaces/web/src/components/settings/providers/ProviderTestDialog.tsx +150 -0
  680. package/src/interfaces/web/src/components/settings/providers/typeStyles.ts +67 -25
  681. package/src/interfaces/web/src/components/settings/providers/types.ts +3 -0
  682. package/src/interfaces/web/src/components/tasks/TaskBoard.tsx +201 -0
  683. package/src/interfaces/web/src/components/tasks/TaskColumnsDialog.tsx +243 -0
  684. package/src/interfaces/web/src/components/tasks/TaskComments.tsx +167 -0
  685. package/src/interfaces/web/src/components/tasks/TaskDetail.tsx +335 -0
  686. package/src/interfaces/web/src/components/tasks/TaskFormDialog.tsx +406 -0
  687. package/src/interfaces/web/src/components/tasks/TaskList.tsx +258 -0
  688. package/src/interfaces/web/src/components/tasks/TaskSubtasks.tsx +126 -0
  689. package/src/interfaces/web/src/components/tasks/columns.ts +51 -0
  690. package/src/interfaces/web/src/components/tasks/taskStatus.tsx +159 -34
  691. package/src/interfaces/web/src/components/tasks/taskViewPrefs.ts +73 -0
  692. package/src/interfaces/web/src/components/tasks/useTaskColumns.ts +31 -0
  693. package/src/interfaces/web/src/components/ui/chat-input.tsx +136 -12
  694. package/src/interfaces/web/src/components/ui/context-menu.tsx +15 -198
  695. package/src/interfaces/web/src/components/ui/dialog.tsx +3 -2
  696. package/src/interfaces/web/src/components/ui/filter-chips.tsx +4 -0
  697. package/src/interfaces/web/src/components/ui/popover.tsx +14 -49
  698. package/src/interfaces/web/src/components/ui/sheet.tsx +2 -1
  699. package/src/interfaces/web/src/components/ui/spinner.tsx +2 -1
  700. package/src/interfaces/web/src/components/ui.tsx +62 -23
  701. package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +2 -1
  702. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +66 -8
  703. package/src/interfaces/web/src/components/voice/VoiceQvoxInstallCard.tsx +63 -0
  704. package/src/interfaces/web/src/components/voice/VoiceQvoxStatusCard.tsx +51 -0
  705. package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +4 -3
  706. package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +38 -4
  707. package/src/interfaces/web/src/constants/index.ts +1 -0
  708. package/src/interfaces/web/src/hooks/useChannelPrefs.ts +39 -0
  709. package/src/interfaces/web/src/hooks/useChat.ts +1092 -85
  710. package/src/interfaces/web/src/hooks/useChatActivity.ts +24 -0
  711. package/src/interfaces/web/src/hooks/useInbox.ts +26 -4
  712. package/src/interfaces/web/src/hooks/useLiveMessages.ts +16 -0
  713. package/src/interfaces/web/src/hooks/useOllamaModels.ts +79 -0
  714. package/src/interfaces/web/src/hooks/useRecorder.ts +198 -0
  715. package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +23 -6
  716. package/src/interfaces/web/src/i18n/en.ts +832 -175
  717. package/src/interfaces/web/src/i18n/es.ts +841 -188
  718. package/src/interfaces/web/src/lib/api/agents.ts +23 -4
  719. package/src/interfaces/web/src/lib/api/code.ts +17 -0
  720. package/src/interfaces/web/src/lib/api/commitments.ts +7 -1
  721. package/src/interfaces/web/src/lib/api/conversations.ts +38 -11
  722. package/src/interfaces/web/src/lib/api/embeddings.ts +3 -0
  723. package/src/interfaces/web/src/lib/api/engines.ts +14 -0
  724. package/src/interfaces/web/src/lib/api/groups.ts +93 -0
  725. package/src/interfaces/web/src/lib/api/images.ts +193 -0
  726. package/src/interfaces/web/src/lib/api/inbox.ts +29 -5
  727. package/src/interfaces/web/src/lib/api/integrations.ts +9 -0
  728. package/src/interfaces/web/src/lib/api/media.ts +126 -0
  729. package/src/interfaces/web/src/lib/api/net.ts +44 -0
  730. package/src/interfaces/web/src/lib/api/profiles.ts +12 -0
  731. package/src/interfaces/web/src/lib/api/projects.ts +9 -1
  732. package/src/interfaces/web/src/lib/api/routines.ts +9 -1
  733. package/src/interfaces/web/src/lib/api/sessions.ts +18 -0
  734. package/src/interfaces/web/src/lib/api/skills.ts +8 -0
  735. package/src/interfaces/web/src/lib/api/super_agent.ts +3 -0
  736. package/src/interfaces/web/src/lib/api/tasks.ts +46 -6
  737. package/src/interfaces/web/src/lib/api/tools.ts +2 -0
  738. package/src/interfaces/web/src/lib/api/transcribe.ts +31 -0
  739. package/src/interfaces/web/src/lib/api/turns.ts +20 -0
  740. package/src/interfaces/web/src/lib/api/voice.ts +72 -2
  741. package/src/interfaces/web/src/lib/api.ts +3 -0
  742. package/src/interfaces/web/src/lib/channels.ts +214 -0
  743. package/src/interfaces/web/src/lib/chat-activity.ts +175 -0
  744. package/src/interfaces/web/src/lib/chat-prefs.ts +48 -0
  745. package/src/interfaces/web/src/lib/code-context.ts +0 -2
  746. package/src/interfaces/web/src/lib/http.ts +51 -17
  747. package/src/interfaces/web/src/lib/inbox-selection.ts +29 -0
  748. package/src/interfaces/web/src/lib/live.ts +252 -0
  749. package/src/interfaces/web/src/lib/net.ts +211 -0
  750. package/src/interfaces/web/src/lib/notify.ts +331 -0
  751. package/src/interfaces/web/src/lib/projectNav.ts +40 -0
  752. package/src/interfaces/web/src/lib/pwa.ts +115 -0
  753. package/src/interfaces/web/src/lib/qvox.ts +24 -0
  754. package/src/interfaces/web/src/lib/slug.ts +3 -0
  755. package/src/interfaces/web/src/lib/tone.ts +150 -0
  756. package/src/interfaces/web/src/main.tsx +15 -4
  757. package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +33 -11
  758. package/src/interfaces/web/src/screens/InboxScreen.tsx +126 -71
  759. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -4
  760. package/src/interfaces/web/src/screens/SettingsScreen.tsx +7 -4
  761. package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +30 -9
  762. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +150 -168
  763. package/src/interfaces/web/src/screens/base/LogsTab.tsx +8 -7
  764. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +99 -37
  765. package/src/interfaces/web/src/screens/base/WorkspacesTab.tsx +1 -1
  766. package/src/interfaces/web/src/screens/mobile/MobileChat.tsx +85 -0
  767. package/src/interfaces/web/src/screens/mobile/MobileChatList.tsx +244 -0
  768. package/src/interfaces/web/src/screens/mobile/MobileCommitments.tsx +350 -0
  769. package/src/interfaces/web/src/screens/mobile/MobileScreen.tsx +138 -0
  770. package/src/interfaces/web/src/screens/mobile/MobileTabBar.tsx +105 -0
  771. package/src/interfaces/web/src/screens/mobile/MobileTasks.tsx +407 -0
  772. package/src/interfaces/web/src/screens/mobile/NewChatSheet.tsx +313 -0
  773. package/src/interfaces/web/src/screens/mobile/mobileList.tsx +194 -0
  774. package/src/interfaces/web/src/screens/mobile/routes.ts +239 -0
  775. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +135 -64
  776. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +0 -1
  777. package/src/interfaces/web/src/screens/modules/ImagesScreen.tsx +172 -0
  778. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +36 -4
  779. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +117 -30
  780. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +537 -92
  781. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +344 -93
  782. package/src/interfaces/web/src/screens/project/ArtifactsTab.tsx +1 -1
  783. package/src/interfaces/web/src/screens/project/ChatTab.tsx +1160 -94
  784. package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +3 -2
  785. package/src/interfaces/web/src/screens/project/McpsTab.tsx +62 -14
  786. package/src/interfaces/web/src/screens/project/Overview.tsx +36 -15
  787. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +93 -8
  788. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +4 -1
  789. package/src/interfaces/web/src/screens/project/StructureTab.tsx +5 -4
  790. package/src/interfaces/web/src/screens/project/TasksTab.tsx +351 -129
  791. package/src/interfaces/web/src/screens/project/VarsTab.tsx +17 -5
  792. package/src/interfaces/web/src/styles.css +106 -31
  793. package/src/interfaces/web/src/types/daemon.ts +246 -2
  794. package/src/interfaces/web/src/vite-env.d.ts +20 -0
  795. package/src/interfaces/tui/plugin/command-shim.ts +0 -109
  796. package/src/interfaces/web/components.json +0 -25
  797. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +0 -849
  798. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +0 -1
  799. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +0 -1
  800. package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +0 -142
  801. package/src/interfaces/web/src/components/ui/accordion.tsx +0 -72
  802. package/src/interfaces/web/src/components/ui/alert-dialog.tsx +0 -187
  803. package/src/interfaces/web/src/components/ui/alert.tsx +0 -76
  804. package/src/interfaces/web/src/components/ui/aspect-ratio.tsx +0 -22
  805. package/src/interfaces/web/src/components/ui/avatar.tsx +0 -107
  806. package/src/interfaces/web/src/components/ui/breadcrumb.tsx +0 -125
  807. package/src/interfaces/web/src/components/ui/button-group.tsx +0 -87
  808. package/src/interfaces/web/src/components/ui/calendar.tsx +0 -221
  809. package/src/interfaces/web/src/components/ui/card.tsx +0 -103
  810. package/src/interfaces/web/src/components/ui/carousel.tsx +0 -242
  811. package/src/interfaces/web/src/components/ui/chart.tsx +0 -371
  812. package/src/interfaces/web/src/components/ui/checkbox.tsx +0 -29
  813. package/src/interfaces/web/src/components/ui/collapsible.tsx +0 -19
  814. package/src/interfaces/web/src/components/ui/combobox.tsx +0 -295
  815. package/src/interfaces/web/src/components/ui/command.tsx +0 -196
  816. package/src/interfaces/web/src/components/ui/direction.tsx +0 -4
  817. package/src/interfaces/web/src/components/ui/drawer.tsx +0 -134
  818. package/src/interfaces/web/src/components/ui/empty.tsx +0 -104
  819. package/src/interfaces/web/src/components/ui/field.tsx +0 -236
  820. package/src/interfaces/web/src/components/ui/hover-card.tsx +0 -51
  821. package/src/interfaces/web/src/components/ui/input-otp.tsx +0 -85
  822. package/src/interfaces/web/src/components/ui/item.tsx +0 -201
  823. package/src/interfaces/web/src/components/ui/menubar.tsx +0 -280
  824. package/src/interfaces/web/src/components/ui/native-select.tsx +0 -61
  825. package/src/interfaces/web/src/components/ui/navigation-menu.tsx +0 -168
  826. package/src/interfaces/web/src/components/ui/pagination.tsx +0 -130
  827. package/src/interfaces/web/src/components/ui/progress.tsx +0 -83
  828. package/src/interfaces/web/src/components/ui/radio-group.tsx +0 -36
  829. package/src/interfaces/web/src/components/ui/resizable.tsx +0 -50
  830. package/src/interfaces/web/src/components/ui/scroll-area.tsx +0 -53
  831. package/src/interfaces/web/src/components/ui/sidebar.tsx +0 -726
  832. package/src/interfaces/web/src/components/ui/slider.tsx +0 -52
  833. package/src/interfaces/web/src/components/ui/sonner.tsx +0 -49
  834. package/src/interfaces/web/src/components/ui/table.tsx +0 -116
  835. package/src/interfaces/web/src/components/ui/toggle-group.tsx +0 -87
  836. package/src/interfaces/web/src/components/ui/toggle.tsx +0 -45
  837. package/src/interfaces/web/src/hooks/use-mobile.ts +0 -19
  838. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +0 -161
@@ -23,6 +23,10 @@ import { CHANNELS } from "../constants/channels.js";
23
23
  import { SUPERAGENT_ACTOR_ID } from "../constants/actors.js";
24
24
 
25
25
  import { nowIso } from "../util/time.js";
26
+ import { summarizeToolTrace } from "../agent/tool-summary.js";
27
+ import { emitMessageEvent } from "../events/bus.js";
28
+ import { cleanTextOfPseudoToolCalls } from "../agent/tools/tool-call-parser.js";
29
+ import { attachmentsMeta } from "./media-archive.js";
26
30
 
27
31
  function dayPathJsonl(projectRoot, ts) {
28
32
  const day = (ts || nowIso()).slice(0, 10);
@@ -121,6 +125,39 @@ export function appendMessageToFs({ projectRoot, channel, direction, type, actor
121
125
  };
122
126
 
123
127
  fs.appendFileSync(file, JSON.stringify(record) + "\n");
128
+ // Announce it. Both project writers land here — `appendMessage` wraps this
129
+ // function — so one emit covers the whole project funnel. The storage path
130
+ // and not a project id: core has no registry to resolve one, and the daemon
131
+ // that subscribes does. See core/events/bus.js.
132
+ emitMessageEvent({
133
+ scope: "project",
134
+ project_root: projectRoot,
135
+ channel,
136
+ thread: ts.slice(0, 10),
137
+ agent_slug: agent_slug || null,
138
+ direction,
139
+ type: msgType,
140
+ author: author || null,
141
+ // `via` lets a subscriber tell a routine DELIVERY (an agent reaching the
142
+ // owner) apart from an ordinary reply the owner is watching — the desktop
143
+ // mascot bubbles the former even though it is `direction: "out"`.
144
+ via: fullMeta.via || null,
145
+ // A bounded (≤100 char) headline for that delivery, so the pet can say what
146
+ // arrived without breaking "signal, not data" — it carries a notice, not the
147
+ // message. Null on every ordinary row.
148
+ notify: fullMeta.notify || null,
149
+ // Closing vs mid-turn: the pet only bubbles the launched final on
150
+ // Telegram / group / A2A, never the owner's send and never a stream chunk.
151
+ final: fullMeta.final === true ? true : null,
152
+ streamed: fullMeta.streamed === true ? true : null,
153
+ // The a2a counterpart. `author` is who spoke; on an a2a row `meta.to` is who
154
+ // was spoken TO, and the pet needs both ends to say "de magui a roby" rather
155
+ // than naming a channel nobody can place. a2a ONLY: it is the one channel
156
+ // where `to` is an agent's name and not an address (a chat id, a number)
157
+ // that has no business on a signal feed.
158
+ to: channel === "a2a" ? (fullMeta.to || null) : null,
159
+ ts,
160
+ });
124
161
  return { ts, file };
125
162
  }
126
163
 
@@ -240,42 +277,6 @@ export function parseDayJsonl(text) {
240
277
  return out;
241
278
  }
242
279
 
243
- // Parse the legacy .md format (kept so rebuild still picks up files written
244
- // by older versions of the daemon).
245
- export function parseDayFile(text) {
246
- const out = [];
247
- const blocks = text.split(/\n(?=## \d{4}-\d{2}-\d{2}T)/);
248
- for (const block of blocks) {
249
- const m = block.match(/^## (\S+)\s+(\S+)\s+(in|out)\s+(.*?)\n([\s\S]*)$/);
250
- if (!m) continue;
251
- const ts = m[1];
252
- const channel = m[2];
253
- const direction = m[3];
254
- const author = m[4].trim();
255
- let body = m[5];
256
- let meta = {};
257
- const metaMatch = body.match(/<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/);
258
- if (metaMatch) {
259
- try { meta = JSON.parse(metaMatch[1]); } catch {}
260
- body = body.replace(metaMatch[0], "");
261
- }
262
- body = body.trim();
263
- const agent_slug = meta.agent;
264
- const type = inferMessageType({ channel, direction, author, agent_slug, meta });
265
- const actor_id = inferActorId({ type, author, agent_slug, meta });
266
- const actor_kind = inferActorKind({ type, actor_id, meta });
267
- out.push({
268
- ts, channel, direction, author, body, meta,
269
- type,
270
- actor_id,
271
- actor_kind,
272
- agent_slug,
273
- session_id: meta.session_id ?? (typeof meta.apc_session_id === "number" ? meta.apc_session_id : null),
274
- external_id: meta.external_id,
275
- });
276
- }
277
- return out;
278
- }
279
280
 
280
281
  // Pull the recent conversation for a given Telegram chat_id from the messages
281
282
  // table. Returns the messages in CHRONOLOGICAL order (oldest first), shaped
@@ -322,7 +323,11 @@ export function getRecentTelegramTurns(
322
323
  return rows.reverse().map((r) => {
323
324
  const role = r.direction === "in" ? "user" : "assistant";
324
325
  let content = r.body;
325
- if (role === "assistant") content = sanitizeAssistantForContext(content);
326
+ if (role === "assistant") {
327
+ content = sanitizeAssistantForContext(content);
328
+ // Redacted: the note goes on the system side, never in the model's voice.
329
+ if (content === null) return { role: "system", content: OMITTED_TURN_NOTE };
330
+ }
326
331
  return { role, content };
327
332
  });
328
333
  }
@@ -340,11 +345,49 @@ export function getRecentTelegramTurns(
340
345
  // (sofia exists, not "assistant", and her model is
341
346
  // claude-haiku-4-5, not the carry-over from above)
342
347
  //
343
- // Solution: replace any assistant turn that *looks* like it contains data
344
- // with a generic "I answered" placeholder. The model loses the cache to
345
- // copy from but keeps enough hint to track the conversation flow.
348
+ // Solution: any assistant turn that *looks* like it contains data is dropped
349
+ // from the model's own side, and a note on the system side keeps the place. The
350
+ // model loses the cache to copy from but keeps enough hint to track the
351
+ // conversation flow.
352
+ // The annotation that stands in for a redacted answer, on the SYSTEM side.
353
+ //
354
+ // It used to ride in the assistant's own voice, and that is what broke: it is a
355
+ // note ABOUT a turn ("you answered here, the data may have moved on"), so every
356
+ // factual answer in the window rendered as this one sentence. On 2026-09-02 six
357
+ // of the ten assistant turns Roby could see were literally this line, and
358
+ // `zen:big-pickle` did the obvious thing — it wrote the sentence back as its own
359
+ // reply, and Manu got `[omitted: …]` in Telegram twice. Tool logs were moved off
360
+ // the assistant side for exactly this failure (see TOOL_LOG_HEADER); a note the
361
+ // model cannot mistake for its own words is not a sentence it can copy.
362
+ const OMITTED_TURN_NOTE =
363
+ "[Turn log — you answered here, and the answer carried data (names, models, " +
364
+ "paths, counts) that may have changed since. Not from the user, not your own " +
365
+ "words, and nothing here is waiting on a reply. To state any of it again, " +
366
+ "call the tool again.]";
367
+
368
+ // The old assistant-voice marker. Kept only to recognise the ones already
369
+ // written into the ledger before this was fixed, so a leaked turn is read back
370
+ // as the annotation it always was instead of being replayed as an answer.
371
+ const LEAKED_OMITTED_TURN = /^\s*\[omitted:[^\]]*\]\s*$/i;
372
+
373
+ // Returns the text to keep in the assistant's voice, or `null` when the turn
374
+ // must not stand there at all — the caller replaces it with OMITTED_TURN_NOTE.
346
375
  function sanitizeAssistantForContext(content) {
347
376
  if (!content) return "";
377
+ // A marker that leaked out as an answer (see run-agent's cleaner) is not a
378
+ // turn: it is this same annotation, wearing the assistant's voice because it
379
+ // was sent to the user. Read it back as a note, never as something said.
380
+ if (LEAKED_OMITTED_TURN.test(content)) return null;
381
+ // A past turn that leaked wire format is the worst thing there is to replay:
382
+ // the model reads its OWN voice writing a call as prose and does it again,
383
+ // which is how one leaked turn becomes fourteen in a day. Scrub the markup
384
+ // out of the history exactly as it is scrubbed out of an answer, so a reply
385
+ // that leaked before this was fixed stops teaching the shape today rather
386
+ // than when it ages out of the window. A turn that was nothing BUT markup
387
+ // is dropped and annotated — "call the tool again" is precisely right for a
388
+ // turn whose whole content was a call that never ran.
389
+ content = cleanTextOfPseudoToolCalls(content) || "";
390
+ if (!content.trim()) return null;
348
391
  // Heuristics — if any of these match, the turn likely contains facts
349
392
  // the model should re-derive from tools rather than parrot from cache.
350
393
  const FACTUAL_PATTERNS = [
@@ -358,12 +401,10 @@ function sanitizeAssistantForContext(content) {
358
401
  ];
359
402
  for (const re of FACTUAL_PATTERNS) {
360
403
  if (re.test(content)) {
361
- // Third person, and visibly an annotation ABOUT the turn rather than the
362
- // turn itself. Written in the first person ("I answered with data here…")
363
- // this read as something the assistant had said, and after a few of them
364
- // in a row the model copied the sentence and sent it to the user as its
365
- // reply. History the model can mistake for its own voice gets imitated.
366
- return "[omitted: this turn contained data that may be stale — call the tool again instead of repeating it]";
404
+ // Dropped from the assistant's side entirely. Third person and bracketed
405
+ // was not enough: whatever the wording, a line the model reads as its own
406
+ // past words is a line it will write again (see OMITTED_TURN_NOTE).
407
+ return null;
367
408
  }
368
409
  }
369
410
  // Otherwise it's conversational small-talk; keep up to 200 chars.
@@ -386,7 +427,6 @@ export function readProjectMessages(projectRoot, { channel, agent_slug, since, l
386
427
  const text = fs.readFileSync(full, "utf8");
387
428
  let msgs = [];
388
429
  if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) msgs = parseDayJsonl(text);
389
- else if (/^\d{4}-\d{2}-\d{2}\.md$/.test(f)) msgs = parseDayFile(text);
390
430
  for (const m of msgs) {
391
431
  if (channel && m.channel !== channel) continue;
392
432
  if (agent_slug && m.agent_slug !== agent_slug) continue;
@@ -409,7 +449,6 @@ export function searchProjectMessages(projectRoot, query, limit = 50) {
409
449
  const text = fs.readFileSync(full, "utf8");
410
450
  let msgs = [];
411
451
  if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) msgs = parseDayJsonl(text);
412
- else if (/^\d{4}-\d{2}-\d{2}\.md$/.test(f)) msgs = parseDayFile(text);
413
452
  for (const m of msgs) {
414
453
  if ((m.body || "").toLowerCase().includes(q)) all.push(m);
415
454
  }
@@ -418,13 +457,674 @@ export function searchProjectMessages(projectRoot, query, limit = 50) {
418
457
  return all.slice(0, Math.min(limit, 500));
419
458
  }
420
459
 
421
- const TOOL_CONTEXT_CAP = 400; // chars of a tool result kept in model context
460
+ // ── A2A "group chats" ────────────────────────────────────────────────────────
461
+ // Agent-to-agent exchanges are project-scoped (they live in the project ledger,
462
+ // not the global channel dir), so listGlobalThreads never sees them. But they
463
+ // are NOT one agent's conversation either — an a2a exchange is a thing between
464
+ // two agents (claude · roby). We surface it as its own thread, keyed by the
465
+ // PAIR of participants, so the web sidebar's "Agent ↔ Agent" group fills with a
466
+ // group chat per pair instead of hiding inside either agent's session.
467
+
468
+ // The two participants of an a2a row. `agent_slug` is the ledger owner (the
469
+ // `from` or `to` the row was written under); `meta.from`/`meta.to` names the
470
+ // counterpart; `author` is who actually spoke. Any two distinct of these are
471
+ // the pair.
472
+ function a2aPair(m) {
473
+ const set = new Set([m.agent_slug, m.author, m.meta?.from, m.meta?.to].filter(Boolean));
474
+ return [...set].sort();
475
+ }
476
+ function a2aPairId(pair) {
477
+ return pair.join("~");
478
+ }
479
+ // `apx send … --deliver` logs each utterance twice — once under `from`, once
480
+ // under `to`. New writes share an external_id. Older rows did not, so their
481
+ // mirror is paired only when the full body, author and participant pair match,
482
+ // the ledger owners are opposite, and timestamps are within five seconds.
483
+ // This avoids collapsing two genuine repeated messages merely because they
484
+ // share a second or the first 120 characters.
485
+ export function dedupA2A(msgs) {
486
+ const sorted = [...(msgs || [])].sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
487
+ const out = [];
488
+ const strong = new Map();
489
+
490
+ const preferSpeakerCopy = (current, candidate) =>
491
+ candidate.agent_slug === candidate.author && current.agent_slug !== current.author;
492
+
493
+ for (const m of sorted) {
494
+ const pairId = a2aPairId(a2aPair(m));
495
+ if (m.external_id) {
496
+ const key = `${pairId}|${m.external_id}`;
497
+ const at = strong.get(key);
498
+ if (at === undefined) {
499
+ strong.set(key, out.length);
500
+ out.push(m);
501
+ } else if (preferSpeakerCopy(out[at], m)) {
502
+ out[at] = m;
503
+ }
504
+ continue;
505
+ }
506
+
507
+ const time = Date.parse(m.ts || "");
508
+ let mirrorAt = -1;
509
+ for (let i = out.length - 1; i >= 0; i -= 1) {
510
+ const current = out[i];
511
+ const currentTime = Date.parse(current.ts || "");
512
+ if (Number.isFinite(time) && Number.isFinite(currentTime) && time - currentTime > 5000) break;
513
+ if (current.external_id) continue;
514
+ if (current.author !== m.author || current.body !== m.body) continue;
515
+ if (a2aPairId(a2aPair(current)) !== pairId) continue;
516
+ if (current.agent_slug === m.agent_slug) continue;
517
+ mirrorAt = i;
518
+ break;
519
+ }
520
+ if (mirrorAt < 0) out.push(m);
521
+ else if (preferSpeakerCopy(out[mirrorAt], m)) out[mirrorAt] = m;
522
+ }
523
+ return out;
524
+ }
525
+
526
+ /** One thread entry per a2a participant-pair in this project. Shaped like a
527
+ * listGlobalThreads() row so the web sidebar renders it in the a2a group,
528
+ * plus `participants` for the double-avatar face. */
529
+ export function listProjectA2AThreads(projectRoot) {
530
+ const msgs = readProjectMessages(projectRoot, { channel: "a2a", limit: 1000 });
531
+ const byPair = new Map();
532
+ for (const m of msgs) {
533
+ const pair = a2aPair(m);
534
+ if (pair.length < 2) continue;
535
+ const id = a2aPairId(pair);
536
+ const g = byPair.get(id) || { pair, msgs: [] };
537
+ g.msgs.push(m);
538
+ byPair.set(id, g);
539
+ }
540
+ const out = [];
541
+ for (const [id, g] of byPair) {
542
+ const uniq = dedupA2A(g.msgs).sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
543
+ if (!uniq.length) continue;
544
+ const last = uniq[uniq.length - 1];
545
+ // Who asked for this exchange — set when an agent relays on a person's behalf
546
+ // (`apx send --for <who>`). Lets the UI show "a pedido de X" and connect a
547
+ // a2a the user triggered back to them, instead of it floating detached.
548
+ const requested_by = g.msgs.map((m) => m.meta?.requested_by).find(Boolean) || null;
549
+ out.push({
550
+ id,
551
+ channel: "a2a",
552
+ title: g.pair.join(" · "),
553
+ participants: g.pair,
554
+ ...(requested_by ? { requested_by } : {}),
555
+ messages: uniq.length,
556
+ started_at: uniq[0].ts,
557
+ last_ts: last.ts,
558
+ preview: `${last.author}: ${previewText(last.body, mediaFromMeta(last.meta))}`.slice(0, 140),
559
+ // Every utterance in an a2a thread is an agent's, so "when an agent last
560
+ // spoke" is simply the last row. The field exists so a notifier can ask
561
+ // that question of any thread without knowing which kind it is.
562
+ preview_at: last.ts,
563
+ });
564
+ }
565
+ out.sort((a, b) => (b.last_ts || "").localeCompare(a.last_ts || ""));
566
+ return out;
567
+ }
568
+
569
+ /** One a2a thread (by pair id) shaped for the web chat viewer: every utterance
570
+ * as an agent turn carrying its author, so the viewer can attribute each
571
+ * bubble to whichever agent spoke. Null when the pair has no messages. */
572
+ /**
573
+ * A pointer a PEER keeps for its a2a thread with `from` — the most recent
574
+ * `key` stamped on a row that peer itself spoke. Two live here: the runtime's
575
+ * own session (`runtime_session_id`) and, for a coding exchange, the Code
576
+ * module session it is mirrored into (`code_session_id`).
577
+ *
578
+ * The pointer lives on the ledger rather than in a side table, so a deleted
579
+ * thread takes it with it and there is no second store to keep in sync. Null is
580
+ * the ordinary first-turn answer: the peer then opens a session and reports it.
581
+ *
582
+ * `author === to` is load-bearing, not tidiness. A thread is keyed by the
583
+ * unordered PAIR, so claude-code→opencode and opencode→claude-code are ONE
584
+ * thread — and that one thread holds TWO sessions, one per peer. Without the
585
+ * check the newer of them wins and is handed to the wrong CLI, which rejects it
586
+ * outright ("Session not found").
587
+ */
588
+ export function readA2APeerSession(projectRoot, { from, to, key = "runtime_session_id" }) {
589
+ const pair = new Set([from, to]);
590
+ const rows = readProjectMessages(projectRoot, { channel: "a2a", limit: 300 }).filter((m) => {
591
+ if (!m.meta?.[key]) return false;
592
+ if (m.author !== to) return false;
593
+ const parts = [m.agent_slug, m.author, m.meta?.from, m.meta?.to].filter(Boolean);
594
+ return parts.length > 0 && parts.every((p) => pair.has(p));
595
+ });
596
+ const last = rows.sort((a, b) => (a.ts || "").localeCompare(b.ts || "")).pop();
597
+ return last?.meta?.[key] || null;
598
+ }
599
+
600
+ export function readProjectA2AThread(projectRoot, id) {
601
+ const want = String(id || "");
602
+ const msgs = readProjectMessages(projectRoot, { channel: "a2a", limit: 1000 })
603
+ .filter((m) => a2aPairId(a2aPair(m)) === want);
604
+ const uniq = dedupA2A(msgs).sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
605
+ if (!uniq.length) return null;
606
+ return {
607
+ id: want,
608
+ channel: "a2a",
609
+ participants: want.split("~"),
610
+ // Shaped through the ONE ledger interpreter (shapeLedgerMessage), same as
611
+ // every other channel — so model, token usage, tool summary and attribution
612
+ // all survive. a2a double-logs under both peers, so normalise agent_slug to
613
+ // the SPEAKER (m.author) first; the shaper does the rest.
614
+ // attribution-exempt: reader — shapeLedgerMessage READS model/usage from each row's meta; this shapes for display, it does not write a row.
615
+ messages: uniq.map((m) =>
616
+ shapeLedgerMessage({ ...m, type: "agent", agent_slug: m.author, actor_kind: m.actor_kind || "agent" }),
617
+ ),
618
+ };
619
+ }
620
+
621
+ /**
622
+ * Delete a whole a2a pair thread: every ledger row the two agents exchanged,
623
+ * across day files. Returns how many rows went.
624
+ *
625
+ * A pair id is DERIVED (see `a2aPair`) and never stored, so a line cannot be
626
+ * matched on a field the way a group's `meta.group_id` can. Each line is read
627
+ * back through `parseDayJsonl` — the one interpreter every reader uses — so a
628
+ * row that lists under this pair is exactly a row that is removed here, with
629
+ * no second derivation to drift from the first.
630
+ *
631
+ * The peers' session pointers (`runtime_session_id`, `code_session_id`) ride on
632
+ * these same rows, so the thread takes them with it: the next exchange between
633
+ * the two opens a fresh session instead of resuming one whose transcript is
634
+ * gone. See `readA2APeerSession`.
635
+ */
636
+ export function deleteA2AThread(projectRoot, id) {
637
+ const want = String(id || "");
638
+ const dir = path.join(projectRoot, "messages");
639
+ if (!want || !fs.existsSync(dir)) return { removed: 0 };
640
+ let removed = 0;
641
+ for (const f of fs.readdirSync(dir).filter((x) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(x))) {
642
+ const full = path.join(dir, f);
643
+ const lines = fs.readFileSync(full, "utf8").split("\n");
644
+ let hits = 0;
645
+ const kept = lines.filter((line) => {
646
+ if (!line.trim()) return true;
647
+ const [row] = parseDayJsonl(line);
648
+ if (!row || row.channel !== "a2a") return true;
649
+ if (a2aPairId(a2aPair(row)) !== want) return true;
650
+ hits += 1;
651
+ return false;
652
+ });
653
+ // Per FILE, not a running total: the day files that hold none of this pair
654
+ // must not be rewritten just because an earlier one did.
655
+ if (hits) {
656
+ fs.writeFileSync(full, kept.join("\n"));
657
+ removed += hits;
658
+ }
659
+ }
660
+ return { removed };
661
+ }
662
+
663
+ // ── Group chats ───────────────────────────────────────────────────────────
664
+ // A group is the owner + N agents in one room. It rides the SAME ledger as a2a
665
+ // (channel "group"), so it lists, opens, and renders through the same thread
666
+ // machinery — no separate store. What a2a derives from message pairs, a group
667
+ // keeps explicit: a `group_created` control row carries the roster + title, and
668
+ // every message tags its `meta.group_id`. Owner turns are `type:"user"` (so the
669
+ // viewer puts them on the right); agent turns are `type:"agent"` attributed to
670
+ // the speaker; control rows (`meta.kind`) never render.
671
+ //
672
+ // The channel name is exported because the daemon addresses a live group turn by
673
+ // it too (active-turns keys a turn as project+channel+thread), and a second
674
+ // literal is how the ledger and the live-turn registry drift apart.
675
+ export const GROUP_CHANNEL = "group";
676
+
677
+ function newGroupId() {
678
+ // base36 time + a little entropy → short, URL-safe, and collision-proof for
679
+ // two creations in the same millisecond.
680
+ return `grp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
681
+ }
682
+
683
+ /** Create a room. Writes the `group_created` control row (roster + title) so
684
+ * the thread exists — and shows in every list — before anyone speaks. Returns
685
+ * the new group id. `logMessage` is the project's writer (db entry method).
686
+ *
687
+ * `homes` (optional): `{ [slug]: projectId }` — when a room mixes agents from
688
+ * several projects, each speaker's home project is recorded so the turn runner
689
+ * can load the right `.apc` agent (and memory) instead of only the host's roster.
690
+ */
691
+ export function createGroupThread(logMessage, { participants = [], title = null, homes = null } = {}) {
692
+ const slugs = [...new Set(participants.filter(Boolean))];
693
+ if (!slugs.length) throw new Error("a group needs at least one agent");
694
+ const group_id = newGroupId();
695
+ const homesMeta = homes && typeof homes === "object" && Object.keys(homes).length
696
+ ? { homes }
697
+ : {};
698
+ logMessage({
699
+ channel: GROUP_CHANNEL, direction: "out", type: "system", author: "system",
700
+ body: "", meta: { group_id, kind: "group_created", participants: slugs, title, ...homesMeta },
701
+ });
702
+ return group_id;
703
+ }
704
+
705
+ /** Add an agent to a room (writes a `participant_added` control row carrying the
706
+ * full new roster, so the latest control row is always the source of truth).
707
+ * `added` is the joining slug, recorded so the transcript can show "… se sumó". */
708
+ export function addGroupParticipant(logMessage, group_id, participants, added = null) {
709
+ logMessage({
710
+ channel: GROUP_CHANNEL, direction: "out", type: "system", author: "system",
711
+ body: "", meta: { group_id, kind: "participant_added", participants: [...new Set(participants)], ...(added ? { added } : {}) },
712
+ });
713
+ }
714
+
715
+ /** Append the owner's line. `type:"user"` → the viewer renders it as sent-by-me.
716
+ * `media` (the `{media_kind, local_path, …}` shape from readTurnAttachments) is
717
+ * folded into meta so the transcript renders the photo/file the owner sent. */
718
+ export function appendGroupOwnerMessage(logMessage, group_id, body, media = null) {
719
+ return logMessage({
720
+ channel: GROUP_CHANNEL, direction: "in", type: "user", author: "owner",
721
+ actor_kind: "user", body, meta: { group_id, ...(media || {}) },
722
+ });
723
+ }
724
+
725
+ /** Append one agent's reply, attributed to it and carrying who summoned it.
726
+ * `media` is what it attached to this reply (attach_media) — archived and
727
+ * folded into meta so the room shows the image, not just the sentence. */
728
+ export function appendGroupAgentMessage(logMessage, group_id, { slug, body, reason = null, model = null, usage = null, trace = [], media = [] }) {
729
+ const steps = Array.isArray(trace) ? trace.filter((s) => s?.tool) : [];
730
+ const toolSummary = summarizeToolTrace(steps);
731
+ for (const step of steps) {
732
+ logMessage({
733
+ channel: GROUP_CHANNEL, direction: "out", type: "tool", agent_slug: slug, author: slug,
734
+ actor_id: step.tool, actor_kind: "tool",
735
+ body: `${step.tool}(${JSON.stringify(step.args || {}).slice(0, 200)})`,
736
+ meta: { group_id, tool: step.tool, args: step.args, result: step.result },
737
+ });
738
+ }
739
+ return logMessage({
740
+ channel: GROUP_CHANNEL, direction: "out", type: "agent", agent_slug: slug, author: slug,
741
+ actor_kind: "agent", body,
742
+ meta: {
743
+ group_id,
744
+ final: true,
745
+ ...(reason ? { reason } : {}),
746
+ ...(model ? { model } : {}),
747
+ ...(usage ? { usage } : {}),
748
+ ...(toolSummary ? { tool_summary: toolSummary } : {}),
749
+ ...attachmentsMeta(media),
750
+ },
751
+ });
752
+ }
753
+
754
+ function groupRows(projectRoot, group_id) {
755
+ return readProjectMessages(projectRoot, { channel: GROUP_CHANNEL, limit: 4000 })
756
+ .filter((m) => m.meta?.group_id && (group_id ? m.meta.group_id === group_id : true))
757
+ .sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
758
+ }
759
+ const isControlRow = (r) => !!r.meta?.kind || r.type === "system";
760
+
761
+ /** One thread row per group in this project, shaped like a listProjectA2AThreads
762
+ * row so the web sidebar and inbox render it in a group. */
763
+ export function listProjectGroupThreads(projectRoot) {
764
+ const byId = new Map();
765
+ for (const r of groupRows(projectRoot, null)) {
766
+ const gid = r.meta.group_id;
767
+ const g = byId.get(gid) || { id: gid, participants: [], title: null, homes: null, created: null, display: [], last: null, lastAgent: null };
768
+ if (Array.isArray(r.meta.participants)) g.participants = r.meta.participants; // latest control wins
769
+ if (r.meta.homes && typeof r.meta.homes === "object") g.homes = r.meta.homes;
770
+ if (r.meta.kind === "group_created") { g.title = r.meta.title || null; g.created = r.ts; }
771
+ if (!isControlRow(r)) {
772
+ g.display.push(r);
773
+ g.last = r;
774
+ // The last thing an AGENT said, tracked apart from the last thing said.
775
+ // A room where the owner spoke last has nothing new to be told about.
776
+ if (r.author !== "owner") g.lastAgent = r;
777
+ }
778
+ byId.set(gid, g);
779
+ }
780
+ const out = [];
781
+ for (const g of byId.values()) {
782
+ out.push({
783
+ id: g.id,
784
+ channel: GROUP_CHANNEL,
785
+ title: g.title || g.participants.join(" · "),
786
+ participants: g.participants,
787
+ ...(g.homes ? { homes: g.homes } : {}),
788
+ messages: g.display.length,
789
+ started_at: g.created || (g.display[0]?.ts) || "",
790
+ last_ts: g.last?.ts || g.created || "",
791
+ preview: g.last
792
+ ? `${g.last.author === "owner" ? "vos" : g.last.author}: ${previewText(g.last.body, mediaFromMeta(g.last.meta))}`.slice(0, 140)
793
+ : undefined,
794
+ preview_at: g.lastAgent?.ts || null,
795
+ });
796
+ }
797
+ out.sort((a, b) => (b.last_ts || "").localeCompare(a.last_ts || ""));
798
+ return out;
799
+ }
800
+
801
+ /** One group thread shaped for the web chat viewer: control rows drive roster +
802
+ * title; owner rows render as `user`, agent rows as `assistant` attributed to
803
+ * the speaker (same shaper every other channel uses). Null when unknown. */
804
+ export function readProjectGroupThread(projectRoot, group_id) {
805
+ const rows = groupRows(projectRoot, group_id);
806
+ if (!rows.length) return null;
807
+ let participants = [];
808
+ let title = null;
809
+ let homes = null;
810
+ for (const r of rows) {
811
+ if (Array.isArray(r.meta.participants)) participants = r.meta.participants;
812
+ if (r.meta.homes && typeof r.meta.homes === "object") homes = r.meta.homes;
813
+ if (r.meta.kind === "group_created") title = r.meta.title || null;
814
+ }
815
+ // Join/leave control rows surface as centred system notices in the transcript
816
+ // ("… se sumó al chat" / "… salió del chat"); group_created stays silent.
817
+ // attribution-exempt: reader — shapes ledger rows for display, writes nothing.
818
+ const messages = [];
819
+ for (const r of rows) {
820
+ if (r.meta.kind === "participant_added" && r.meta.added)
821
+ messages.push({ role: "system", event: "joined", who: r.meta.added, ts: r.ts });
822
+ else if (r.meta.kind === "participant_removed" && r.meta.left)
823
+ messages.push({ role: "system", event: "left", who: r.meta.left, ts: r.ts });
824
+ else if (isControlRow(r)) continue;
825
+ else if (r.type === "user") messages.push(shapeLedgerMessage(r));
826
+ else messages.push(shapeLedgerMessage({ ...r, agent_slug: r.actor_id || r.author, actor_kind: r.actor_kind || "agent" }));
827
+ }
828
+ return {
829
+ id: group_id,
830
+ channel: GROUP_CHANNEL,
831
+ title: title || participants.join(" · "),
832
+ participants,
833
+ ...(homes ? { homes } : {}),
834
+ messages,
835
+ };
836
+ }
837
+
838
+ /**
839
+ * Rewind a group: keep the first `keepVisible` PANE BUBBLES and drop the rest
840
+ * from the ledger, so "regenerate"/"edit & resend" can overwrite everything
841
+ * after a point — the same rewind the 1:1 chat's truncateConversation does, but
842
+ * on the append-only ledger. Control rows (roster/title) and every other channel
843
+ * are left untouched. Returns how many rows were removed.
844
+ *
845
+ * keepVisible matches what the web pane shows as bubbles:
846
+ * - tool rows ride with the agent turn that follows (do not count alone)
847
+ * - consecutive agent rows from the SAME speaker collapse into ONE bubble
848
+ * (threadToChatMsgs does the same). Counting each agent row separately
849
+ * under-counted keepVisible and deleted the owner's line when regenerating
850
+ * the next reply (e.g. two Candela rows → one UI bubble → cut too early).
851
+ *
852
+ * Unlike a2a (which refuses truncation — it is a record of two agents talking),
853
+ * a group is an interactive chat the owner drives, so rewinding it is expected.
854
+ */
855
+ export function truncateGroupThread(projectRoot, group_id, keepVisible) {
856
+ const dir = path.join(projectRoot, "messages");
857
+ if (!fs.existsSync(dir)) return { removed: 0 };
858
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort();
859
+ const raw = new Map(); // file -> raw line array
860
+ const display = []; // { file, idx, ts, type, author }
861
+ for (const f of files) {
862
+ const lines = fs.readFileSync(path.join(dir, f), "utf8").split("\n");
863
+ raw.set(f, lines);
864
+ lines.forEach((line, idx) => {
865
+ const t = line.trim();
866
+ if (!t) return;
867
+ let obj; try { obj = JSON.parse(t); } catch { return; }
868
+ if (!obj || obj.channel !== GROUP_CHANNEL || obj.meta?.group_id !== group_id) return;
869
+ if (obj.meta?.kind || obj.type === "system") return; // control row — keep
870
+ display.push({
871
+ file: f,
872
+ idx,
873
+ ts: obj.ts || "",
874
+ type: obj.type || "agent",
875
+ author: obj.author || obj.agent_slug || "",
876
+ });
877
+ });
878
+ }
879
+ display.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
880
+
881
+ // Collapse into pane bubbles (same rules as threadToChatMsgs).
882
+ const bubbles = []; // each: row refs
883
+ let pendingTools = [];
884
+ let lastActor = null; // "user" | "agent:<slug>"
885
+ for (const d of display) {
886
+ if (d.type === "tool") {
887
+ pendingTools.push(d);
888
+ continue;
889
+ }
890
+ if (d.type === "user") {
891
+ bubbles.push([...pendingTools, d]);
892
+ pendingTools = [];
893
+ lastActor = "user";
894
+ continue;
895
+ }
896
+ // agent (or any other non-tool display row attributed to a speaker)
897
+ const actor = `agent:${d.author || "agent"}`;
898
+ if (lastActor === actor && bubbles.length) {
899
+ bubbles[bubbles.length - 1].push(...pendingTools, d);
900
+ } else {
901
+ bubbles.push([...pendingTools, d]);
902
+ }
903
+ pendingTools = [];
904
+ lastActor = actor;
905
+ }
906
+ if (pendingTools.length) {
907
+ // Orphan tools after the last turn — attach to last bubble or own drop unit.
908
+ if (bubbles.length) bubbles[bubbles.length - 1].push(...pendingTools);
909
+ else bubbles.push(pendingTools);
910
+ }
911
+
912
+ const cut = Math.max(0, keepVisible);
913
+ const drop = new Set();
914
+ const keyOf = (d) => `${d.file}#${d.idx}`;
915
+ for (const bubble of bubbles.slice(cut)) {
916
+ for (const d of bubble) drop.add(keyOf(d));
917
+ }
918
+ if (!drop.size) return { removed: 0 };
919
+ for (const f of new Set([...drop].map((k) => k.slice(0, k.lastIndexOf("#"))))) {
920
+ const kept = raw.get(f).filter((_, idx) => !drop.has(`${f}#${idx}`));
921
+ fs.writeFileSync(path.join(dir, f), kept.join("\n"));
922
+ }
923
+ return { removed: drop.size };
924
+ }
925
+
926
+ /** The body of the last owner turn in a group — the seed a "regenerate" re-runs
927
+ * the cascade from, without appending a new owner message. Null if none. */
928
+ export function lastGroupOwnerMessage(projectRoot, group_id) {
929
+ const rows = groupRows(projectRoot, group_id).filter((r) => r.type === "user");
930
+ return rows.length ? (rows[rows.length - 1].body || "") : null;
931
+ }
932
+
933
+ /** The photo/file the last owner turn carried, as `{ path, name }` — so a
934
+ * regenerate can re-attach it and vision keeps working. Null when that turn had
935
+ * no media (or there is no owner turn). */
936
+ export function lastGroupOwnerMedia(projectRoot, group_id) {
937
+ const rows = groupRows(projectRoot, group_id).filter((r) => r.type === "user");
938
+ const last = rows[rows.length - 1];
939
+ const m = last?.meta;
940
+ if (!m?.local_path) return null;
941
+ return { path: m.local_path, name: m.file_name || undefined };
942
+ }
943
+
944
+ /** Delete a whole group room: every ledger row for it (messages AND control
945
+ * rows), across day files. Returns how many rows were removed. */
946
+ export function deleteGroupThread(projectRoot, group_id) {
947
+ const dir = path.join(projectRoot, "messages");
948
+ if (!fs.existsSync(dir)) return { removed: 0 };
949
+ let removed = 0;
950
+ for (const f of fs.readdirSync(dir).filter((x) => x.endsWith(".jsonl"))) {
951
+ const full = path.join(dir, f);
952
+ const lines = fs.readFileSync(full, "utf8").split("\n");
953
+ const kept = lines.filter((line) => {
954
+ const t = line.trim();
955
+ if (!t) return true;
956
+ let obj; try { obj = JSON.parse(t); } catch { return true; }
957
+ const hit = obj?.channel === GROUP_CHANNEL && obj.meta?.group_id === group_id;
958
+ if (hit) removed += 1;
959
+ return !hit;
960
+ });
961
+ if (removed) fs.writeFileSync(full, kept.join("\n"));
962
+ }
963
+ return { removed };
964
+ }
965
+
966
+ /** Remove an agent from a room, recording it as a visible notice ("… salió del
967
+ * chat") so the transcript shows who left and the agents stop citing them. */
968
+ export function removeGroupParticipant(logMessage, group_id, slug, participants) {
969
+ logMessage({
970
+ channel: GROUP_CHANNEL, direction: "out", type: "system", author: "system",
971
+ body: "", meta: { group_id, kind: "participant_removed", participants: [...participants], left: slug },
972
+ });
973
+ }
974
+
975
+ /** Repoint a room's roster after an agent's slug changed. The roster lives in
976
+ * the LATEST control row (and `homes` maps slug → home project), so a rename is
977
+ * one more control row: same room, same history, new key. Without it the runner
978
+ * cannot resolve the speaker (`bySlug`) and the agent falls out of the room in
979
+ * silence. Rows already written keep their old `agent_slug` — those are a
980
+ * record of who spoke, not a pointer.
981
+ *
982
+ * Takes `projectRoot` rather than a `logMessage` (unlike every other writer
983
+ * here) because it has to READ the threads to find the ones that name the
984
+ * slug, and it runs from the core sweep (apc/agent-rename-refs.js) where no
985
+ * daemon entry is in hand.
986
+ *
987
+ * `homeId` scopes a mixed-project room to the renamed agent's own project, so
988
+ * two projects that both own a "magui" never repoint each other's member;
989
+ * `hostId` is who a participant belongs to when the room has no `homes` map
990
+ * (single-project by construction). Passing neither renames every match.
991
+ *
992
+ * @returns {number} rooms repointed
993
+ */
994
+ export function renameGroupParticipant(projectRoot, oldSlug, newSlug, { homeId = null, hostId = null } = {}) {
995
+ if (!projectRoot || !oldSlug || !newSlug || oldSlug === newSlug) return 0;
996
+ let changed = 0;
997
+ for (const thread of listProjectGroupThreads(projectRoot)) {
998
+ if (!Array.isArray(thread.participants) || !thread.participants.includes(oldSlug)) continue;
999
+ const homes = thread.homes && typeof thread.homes === "object" ? thread.homes : null;
1000
+ const owner = homes?.[oldSlug] ?? hostId;
1001
+ if (homeId != null && String(owner ?? homeId) !== String(homeId)) continue;
1002
+ const participants = [...new Set(thread.participants.map((s) => (s === oldSlug ? newSlug : s)))];
1003
+ const nextHomes = homes
1004
+ ? Object.fromEntries(Object.entries(homes).map(([s, id]) => [s === oldSlug ? newSlug : s, id]))
1005
+ : null;
1006
+ appendMessageToFs({
1007
+ projectRoot, channel: GROUP_CHANNEL, direction: "out", type: "system", author: "system",
1008
+ body: "",
1009
+ meta: {
1010
+ group_id: thread.id, kind: "participant_renamed", participants,
1011
+ ...(nextHomes ? { homes: nextHomes } : {}), from: oldSlug, to: newSlug,
1012
+ },
1013
+ });
1014
+ changed += 1;
1015
+ }
1016
+ return changed;
1017
+ }
1018
+
1019
+ const TOOL_CONTEXT_CAP = 700; // chars of a tool result kept in model context
1020
+ const TOOL_CALL_CAP = 160; // chars of the invocation that introduces it
422
1021
 
423
- // Truncated, prefixed rendering of a tool result for model context (Pieza 3).
1022
+ // What a tool record actually returned. The record stores the invocation in
1023
+ // `body` ("run_shell({\"command\":…})") and the OUTCOME in `meta.result` —
1024
+ // this used to read `body`, so the rolling history replayed the agent's own
1025
+ // commands and never a single result. A turn could re-read the shell script it
1026
+ // wrote an hour ago and still not know what the shell said, which is what made
1027
+ // long Telegram threads feel amnesiac: the model kept re-deriving facts it had
1028
+ // already fetched. Prefer the result; fall back to the invocation only when a
1029
+ // record has none (older records, or a tool that returns undefined).
1030
+ function renderToolResultBody(m) {
1031
+ let r = m.meta?.result;
1032
+ if (r === undefined || r === null || r === "") return "";
1033
+ // Some writers store the result already serialized. Re-parse so the shell
1034
+ // envelope below still gets unwrapped instead of being replayed as escaped
1035
+ // JSON, which spends the whole budget on backslashes.
1036
+ if (typeof r === "string") {
1037
+ const t = r.trim();
1038
+ if (t.startsWith("{") || t.startsWith("[")) {
1039
+ try {
1040
+ r = JSON.parse(t);
1041
+ } catch {
1042
+ // Records written before the trace kept its shape are truncated JSON
1043
+ // text and will never parse. Dig the stdout out by hand rather than
1044
+ // replaying a wall of backslashes.
1045
+ const cut = t.match(/"stdout"\s*:\s*"((?:[^"\\]|\\.)*)/);
1046
+ if (!cut) return t;
1047
+ try { return JSON.parse(`"${cut[1]}"`); } catch { return cut[1]; }
1048
+ }
1049
+ } else {
1050
+ return r;
1051
+ }
1052
+ }
1053
+ // Shell-shaped results are mostly envelope; the stdout is the information.
1054
+ if (typeof r === "object" && !Array.isArray(r)) {
1055
+ const parts = [];
1056
+ if (typeof r.stdout === "string" && r.stdout.trim()) parts.push(r.stdout.trim());
1057
+ if (typeof r.stderr === "string" && r.stderr.trim()) parts.push(`stderr: ${r.stderr.trim()}`);
1058
+ if (r.error) parts.push(`error: ${typeof r.error === "string" ? r.error : JSON.stringify(r.error)}`);
1059
+ if (parts.length) {
1060
+ const code = r.exit_code;
1061
+ return (Number.isFinite(code) && code !== 0 ? `exit=${code} ` : "") + parts.join(" | ");
1062
+ }
1063
+ }
1064
+ try {
1065
+ return JSON.stringify(r);
1066
+ } catch {
1067
+ return String(r);
1068
+ }
1069
+ }
1070
+
1071
+ const flatten = (s) => String(s ?? "").replace(/\s+/g, " ").trim();
1072
+
1073
+ // The invocation that introduces a result, rendered so it cannot be mistaken
1074
+ // for one. This half is why the record is readable at all — "1247" means
1075
+ // nothing without "wc -l on X" — but the form matters as much as the content:
1076
+ // `{"command":"ls"}` IS a call, and a model that reads its own history finds a
1077
+ // worked example of writing calls as prose, copies it, and the loop (which
1078
+ // sees no tool_calls) delivers the transcript to the user as the answer.
1079
+ // `command=ls` carries the same question with no wire format to imitate.
1080
+ function renderToolCallSummary(m, name) {
1081
+ let args = m.meta?.args;
1082
+ const raw = flatten(m.body);
1083
+ // `body` is "<name>(<args>)" and the name is already in the prefix — keep
1084
+ // only the arguments so the cap buys context, not a repeated tool name.
1085
+ const inner = raw.startsWith(`${name}(`) && raw.endsWith(")")
1086
+ ? raw.slice(name.length + 1, -1)
1087
+ : raw;
1088
+ if (args === undefined || args === null) {
1089
+ try { args = JSON.parse(inner); } catch { /* not JSON — fall back to text */ }
1090
+ }
1091
+ const text =
1092
+ args && typeof args === "object" && !Array.isArray(args)
1093
+ ? Object.entries(args)
1094
+ .map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`)
1095
+ .join(", ")
1096
+ : inner;
1097
+ // Belt and braces, literally: an argument object is the one shape that reads
1098
+ // back as a real call, so no `{}` survives into the history at all.
1099
+ return flatten(text).replace(/[{}]/g, "").slice(0, TOOL_CALL_CAP);
1100
+ }
1101
+
1102
+ // Names the lines below for whoever reads them back. Not decoration: for Gemini
1103
+ // and Anthropic a mid-history system turn arrives as a USER turn, and a bare
1104
+ // `[tool result: …]` line there reads as something the user typed and is owed a
1105
+ // reply. This says what it is and that it is nobody's turn.
1106
+ const TOOL_LOG_HEADER =
1107
+ "[Tool log — what already ran in this conversation. Not from the user, not " +
1108
+ "your own words, and nothing here is waiting on a reply. Read it for facts; " +
1109
+ "to act again, call the tool.]";
1110
+
1111
+ // Truncated, prefixed rendering of a tool record for model context (Pieza 3).
1112
+ // Shape: `[tool result: <name>] (<short invocation>) → <result>`. The result
1113
+ // gets the bulk of the budget.
1114
+ //
1115
+ // The prefix says "result", not "<name>", on purpose: it is an annotation
1116
+ // ABOUT a past turn, `cleanTextOfPseudoToolCalls` strips the whole line out of
1117
+ // anything a model says, and it matches what the RAG indexer writes for the
1118
+ // same record. `[tool <name>] <args>` — what this rendered between 2026-08-19
1119
+ // and 2026-08-26 — looked instead like a call, and gemini-3.7-flash echoed it
1120
+ // straight back at the user, tools never running (see tool-markup-leak.test.js).
424
1121
  function renderToolResult(m) {
425
1122
  const name = m.meta?.tool_name || m.meta?.tool || m.actor_id || "tool";
426
- const body = String(m.body || "").replace(/\s+/g, " ").trim();
427
- return `[tool result: ${name}] ${body}`.slice(0, TOOL_CONTEXT_CAP);
1123
+ const call = renderToolCallSummary(m, name);
1124
+ const result = flatten(renderToolResultBody(m));
1125
+ const head = call ? `[tool result: ${name}] (${call})` : `[tool result: ${name}]`;
1126
+ if (!result) return head.slice(0, TOOL_CONTEXT_CAP);
1127
+ return `${head} → ` + result.slice(0, Math.max(120, TOOL_CONTEXT_CAP - head.length - 3));
428
1128
  }
429
1129
 
430
1130
  // Collapse consecutive same-role entries into one message. Keeps the model
@@ -448,8 +1148,8 @@ function coalesceTurns(turns) {
448
1148
  // - the latest `compact` record (if any) is prepended as a role:"system"
449
1149
  // turn "[RESUMEN COMPACTADO turnos a-b]: …"; the raw turns it covers are
450
1150
  // dropped (they live in the summary now)
451
- // - tool results are INCLUDED, truncated to 400 chars, prefixed
452
- // "[tool result: <tool>]" (kept on the assistant side)
1151
+ // - tool records are INCLUDED as "[tool result: <name>] (<call>) → <result>",
1152
+ // the RESULT truncated to fit TOOL_CONTEXT_CAP (kept on the assistant side)
453
1153
  // - the most recent `keepRecent` conversational turns are kept verbatim
454
1154
  // - consecutive same-role turns are coalesced
455
1155
  //
@@ -489,11 +1189,17 @@ export function getRecentChannelTurnsFromFs({
489
1189
  for (const m of mine) if (m.type === "compact") compact = m;
490
1190
  const coverUntil = compact ? compact.meta?.covers_until_ts || compact.ts : "";
491
1191
 
492
- const eligible = mine.filter(
493
- (m) =>
1192
+ const eligible = mine.filter((m) => {
1193
+ // Progress rows are for the person watching this turn, not conversation
1194
+ // history. Replaying a long Telegram run as 30 assistant messages crowds
1195
+ // out the owner's actual request and teaches the model to narrate tools.
1196
+ const transientProgress = m.meta?.streamed === true || m.meta?.progress === true;
1197
+ return (
1198
+ !transientProgress &&
494
1199
  (m.type === "user" || m.type === "agent" || (includeTools && m.type === "tool")) &&
495
1200
  (!coverUntil || m.ts > coverUntil)
496
- );
1201
+ );
1202
+ });
497
1203
 
498
1204
  // Keep the last `keep` conversational (user/agent) turns plus any tool
499
1205
  // results interleaved among them.
@@ -518,13 +1224,55 @@ export function getRecentChannelTurnsFromFs({
518
1224
  content: `[RESUMEN COMPACTADO ${label}]:\n${String(compact.body).trim()}`,
519
1225
  });
520
1226
  }
1227
+ // Whether the previous record was also a tool, so only the first line of each
1228
+ // contiguous run carries the header.
1229
+ let inToolRun = false;
521
1230
  for (const m of kept) {
522
1231
  if (m.type === "tool") {
523
- turns.push({ role: "assistant", content: renderToolResult(m) });
1232
+ // On the SYSTEM side, never the assistant's. A tool result is something
1233
+ // that was observed, not something the agent said — and putting it in the
1234
+ // agent's own voice is what taught the agent to fake it.
1235
+ //
1236
+ // With `role: "assistant"` these lines were coalesced into the model's
1237
+ // own turns, once per tool it had ever run: in a busy Telegram thread,
1238
+ // 62% of the window was the model apparently answering in the shape
1239
+ // `[tool result: run_shell] (command=…) → …` followed by prose. That is a
1240
+ // worked example of narrating tool calls instead of making them, and on
1241
+ // 2026-08-29 a fallback model copied it — five fabricated `[result:
1242
+ // shell]` lines and a confident "I sent the WhatsApp". Nothing had run.
1243
+ //
1244
+ // Fixing the SHAPE was tried first (see the tests); the shape is not what
1245
+ // makes it copyable, the SIDE is. Here the same text is unmistakably a
1246
+ // log about the conversation rather than a turn in it, which is exactly
1247
+ // how the compacted summary above already rides.
1248
+ //
1249
+ // Headed on the first line of each contiguous run, because for Gemini and
1250
+ // Anthropic a mid-history system turn is serialised as a USER turn — and
1251
+ // an unlabelled `[tool result: …]` arriving as the user's words is
1252
+ // something a model will try to answer. The header says whose lines these
1253
+ // are and that nobody is waiting on them.
1254
+ const line = renderToolResult(m);
1255
+ turns.push({ role: "system", content: inToolRun ? line : `${TOOL_LOG_HEADER}\n${line}` });
1256
+ inToolRun = true;
524
1257
  } else {
1258
+ inToolRun = false;
525
1259
  const role = m.type === "user" ? "user" : "assistant";
526
1260
  let content = m.body;
527
- if (role === "assistant") content = sanitizeAssistantForContext(content);
1261
+ if (role === "assistant") {
1262
+ content = sanitizeAssistantForContext(content);
1263
+ // Redacted: the answer is gone, but the fact that one happened here is
1264
+ // worth keeping so the model does not re-answer the question above it.
1265
+ // It rides on the system side with the tool log, for the same reason.
1266
+ if (content === null) {
1267
+ const prev = turns[turns.length - 1];
1268
+ // One note per run: three redacted answers in a row say nothing more
1269
+ // than one, and a repeated line is a pattern to imitate.
1270
+ if (!(prev && prev.role === "system" && prev.content === OMITTED_TURN_NOTE)) {
1271
+ turns.push({ role: "system", content: OMITTED_TURN_NOTE });
1272
+ }
1273
+ continue;
1274
+ }
1275
+ }
528
1276
  turns.push({ role, content });
529
1277
  }
530
1278
  }
@@ -561,6 +1309,26 @@ export function appendGlobalMessage({ channel, direction, type, actor_id, actor_
561
1309
  ...(Object.keys(fullMeta).length ? { meta: fullMeta } : {}),
562
1310
  };
563
1311
  fs.appendFileSync(file, JSON.stringify(record) + "\n");
1312
+ // Announce it: this is the funnel every channel writes through — Telegram in
1313
+ // and out, desktop, the web's own turns — so a surface subscribed to the bus
1314
+ // sees a conversation move no matter which device produced the turn.
1315
+ // `thread` is the day file's id because that is how a channel thread is
1316
+ // addressed everywhere else (readGlobalThread, the inbox row, the URL).
1317
+ emitMessageEvent({
1318
+ scope: "global",
1319
+ channel,
1320
+ thread: ts.slice(0, 10),
1321
+ project_id: fullMeta.project_id ?? null,
1322
+ agent_slug: agent_slug || null,
1323
+ direction,
1324
+ type: msgType,
1325
+ author: author || null,
1326
+ via: fullMeta.via || null,
1327
+ notify: fullMeta.notify || null,
1328
+ final: fullMeta.final === true ? true : null,
1329
+ streamed: fullMeta.streamed === true ? true : null,
1330
+ ts,
1331
+ });
564
1332
  return { ts, file };
565
1333
  }
566
1334
 
@@ -600,8 +1368,95 @@ export function readGlobalMessages({ channel, limit = 100, since } = {}) {
600
1368
 
601
1369
  const CHANNEL_NAME_RE = /^[a-z0-9_-]+$/i;
602
1370
 
1371
+ // The workspace an unstamped row belongs to. Telegram, desktop and deck each
1372
+ // write one daemon-wide channel with no project of their own, and so does every
1373
+ // row written before web turns started stamping one. Those chats are the default
1374
+ // workspace's, not every project's: calling them unowned — visible from every
1375
+ // project — is what put all ninety-odd Telegram threads in the sidebar of
1376
+ // projects none of them happened in.
1377
+ const DEFAULT_THREAD_PROJECT = "0";
1378
+
1379
+ // Which project a ledger row was written from, when the writer knew. Web turns
1380
+ // stamp it (api/super-agent.js), because the web panel is the one surface where
1381
+ // the same channel is used from several projects.
1382
+ function rowProject(r) {
1383
+ const v = r?.meta?.project_id;
1384
+ return v === undefined || v === null || v === "" ? null : String(v);
1385
+ }
1386
+
1387
+ // Whether a row belongs in `want`'s view of the ledger: a stamped row only in
1388
+ // its own project, an unstamped one in the default workspace.
1389
+ function rowBelongsTo(r, want) {
1390
+ return (rowProject(r) ?? DEFAULT_THREAD_PROJECT) === want;
1391
+ }
1392
+
1393
+ // Rows belonging to `project`. `undefined`/null means no filtering at all — the
1394
+ // cross-project shape the agent inbox and rebuild read.
1395
+ function keepForProject(rows, project) {
1396
+ if (project === undefined || project === null || project === "") return rows;
1397
+ const want = String(project);
1398
+ return rows.filter((r) => rowBelongsTo(r, want));
1399
+ }
1400
+
603
1401
  // List every non-empty channel+day thread, newest-last-activity first.
604
- export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
1402
+ // `project` narrows to the chats that happened in that project (see
1403
+ // keepForProject) — the web sidebar passes it so each project lists its own
1404
+ // conversations, with the channel groups inside it left intact.
1405
+ // ── Thread overrides ────────────────────────────────────────────────────────
1406
+ // A channel thread has no file of its own to carry metadata: it IS a day of a
1407
+ // channel's ledger, and the ledger rows are the record of what was said — not a
1408
+ // place to hang what the reader decided to call it. So the two decisions a
1409
+ // reader can make about a thread, its name and whether it has been put away,
1410
+ // live in one small index beside the ledger. Nothing here changes a single
1411
+ // message, and losing this file loses only the names.
1412
+ const THREAD_META_FILE = ".threads.json";
1413
+
1414
+ function threadMetaPath(base) {
1415
+ return path.join(base, THREAD_META_FILE);
1416
+ }
1417
+
1418
+ function readThreadMeta(base) {
1419
+ try {
1420
+ return JSON.parse(fs.readFileSync(threadMetaPath(base), "utf8")) || {};
1421
+ } catch {
1422
+ return {};
1423
+ }
1424
+ }
1425
+
1426
+ /** Key one thread. Channel and date both, because the same date exists in every
1427
+ * channel and they are different conversations. */
1428
+ function threadKey(channel, date) {
1429
+ return `${channel}/${date}`;
1430
+ }
1431
+
1432
+ /**
1433
+ * Name a thread, or put it away. `title: ""` drops the override and the
1434
+ * derived title (the first thing said) comes back.
1435
+ */
1436
+ export function setGlobalThreadMeta({ channel, date, title, archived, _globalMessagesDir } = {}) {
1437
+ if (!CHANNEL_NAME_RE.test(String(channel || ""))) return false;
1438
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ""))) return false;
1439
+ const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
1440
+ if (!fs.existsSync(base)) return false;
1441
+ const all = readThreadMeta(base);
1442
+ const key = threadKey(channel, date);
1443
+ const entry = { ...(all[key] || {}) };
1444
+ if (title !== undefined) {
1445
+ const clean = String(title).replace(/\s+/g, " ").trim().slice(0, 200);
1446
+ if (clean) entry.title = clean;
1447
+ else delete entry.title;
1448
+ }
1449
+ if (archived !== undefined) {
1450
+ if (archived) entry.archived = true;
1451
+ else delete entry.archived;
1452
+ }
1453
+ if (Object.keys(entry).length) all[key] = entry;
1454
+ else delete all[key];
1455
+ fs.writeFileSync(threadMetaPath(base), JSON.stringify(all, null, 2) + "\n");
1456
+ return true;
1457
+ }
1458
+
1459
+ export function listGlobalThreads({ channels, project, includeArchived = false, _globalMessagesDir } = {}) {
605
1460
  const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
606
1461
  if (!fs.existsSync(base)) return [];
607
1462
  const chans = (channels && channels.length
@@ -611,6 +1466,7 @@ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
611
1466
  })
612
1467
  ).filter((c) => CHANNEL_NAME_RE.test(c));
613
1468
 
1469
+ const meta = readThreadMeta(base);
614
1470
  const out = [];
615
1471
  for (const ch of chans) {
616
1472
  const dir = path.join(base, ch);
@@ -619,19 +1475,26 @@ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
619
1475
  for (const f of files) {
620
1476
  const m = f.match(/^(\d{4}-\d{2}-\d{2})\.jsonl$/);
621
1477
  if (!m) continue;
622
- const msgs = parseDayJsonl(fs.readFileSync(path.join(dir, f), "utf8")).filter(
623
- (r) => r.type === "user" || r.type === "agent"
1478
+ const msgs = keepForProject(
1479
+ parseDayJsonl(fs.readFileSync(path.join(dir, f), "utf8")).filter(
1480
+ (r) => r.type === "user" || r.type === "agent"
1481
+ ),
1482
+ project,
624
1483
  );
625
1484
  if (!msgs.length) continue;
1485
+ const over = meta[threadKey(ch, m[1])] || {};
1486
+ if (over.archived && !includeArchived) continue;
626
1487
  const firstUser = msgs.find((r) => r.type === "user");
627
- const title = String((firstUser || msgs[0]).body || "")
1488
+ const derived = String((firstUser || msgs[0]).body || "")
628
1489
  .replace(/\s+/g, " ")
629
1490
  .trim()
630
1491
  .slice(0, 80);
631
1492
  out.push({
632
1493
  id: m[1],
633
1494
  channel: ch,
634
- title: title || `${ch} · ${m[1]}`,
1495
+ // What the reader called it wins over the first thing that was said.
1496
+ title: over.title || derived || `${ch} · ${m[1]}`,
1497
+ archived: over.archived || undefined,
635
1498
  messages: msgs.length,
636
1499
  started_at: msgs[0].ts,
637
1500
  last_ts: msgs[msgs.length - 1].ts,
@@ -642,6 +1505,67 @@ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
642
1505
  return out;
643
1506
  }
644
1507
 
1508
+ // The attachment a stored turn carries, shaped for a viewer.
1509
+ //
1510
+ // The bytes live in ~/.apx/media and the turn records where: without this the
1511
+ // thread showed only the text marker the agent was given ("[document received:
1512
+ // … saved to /Users/…]"), which says nothing to the person who sent the file.
1513
+ //
1514
+ // Records written since dispatch merged the media row carry `media_kind`;
1515
+ // older ones predate it, so the kind is inferred from the metadata that IS
1516
+ // there — a transcription backend means a voice note, pixel dimensions mean a
1517
+ // photo, anything else with a file behind it is a plain file.
1518
+ //
1519
+ // What marks a row as carrying a file is the file: a Telegram `file_id` or a
1520
+ // `local_path`. Requiring the id would have made every web upload — which has
1521
+ // no Telegram anything — read back as a plain typed message.
1522
+ export function mediaFromMeta(meta) {
1523
+ if (!meta || (!meta.file_id && !meta.local_path)) return null;
1524
+ const kind = Array.isArray(meta.media_kind) ? meta.media_kind[0] : meta.media_kind;
1525
+ const resolved =
1526
+ kind ||
1527
+ (meta.transcription_backend !== undefined ? "audio" : meta.width ? "photo" : "file");
1528
+ return {
1529
+ kind: resolved,
1530
+ // null when the download failed: the turn still records what arrived, and
1531
+ // the viewer says the copy is missing instead of offering a dead player.
1532
+ path: meta.local_path || null,
1533
+ name: meta.file_name || (meta.local_path ? path.basename(meta.local_path) : null),
1534
+ mime: meta.mime_type || null,
1535
+ size: meta.file_size ?? null,
1536
+ duration: meta.duration ?? null,
1537
+ };
1538
+ }
1539
+
1540
+ const MEDIA_GLYPH = {
1541
+ photo: "📷", audio: "🎤", video: "🎬", animation: "🎞", document: "📄", file: "📎",
1542
+ };
1543
+
1544
+ /**
1545
+ * The one-line preview a list shows for a turn — the chat list, the inbox row,
1546
+ * a notification.
1547
+ *
1548
+ * A turn that carried a file has machine-facing text: the marker the agent was
1549
+ * handed inbound ("[image attached — saved to /Users/…]") or the placeholder
1550
+ * written outbound when the send had no caption ("[photo]"). Printing it raw is
1551
+ * how a photo showed up in the list as a file path. The thread already strips
1552
+ * that marker to render the file; this is the same decision for the one line
1553
+ * that stands in for the thread.
1554
+ *
1555
+ * A glyph rather than a word: this text is produced server-side and read in
1556
+ * whatever language the panel is set to, and "📷 el agarre" needs no catalog.
1557
+ *
1558
+ * @param {string} body the row's stored text
1559
+ * @param {object|null} media what mediaFromMeta returned for that row
1560
+ */
1561
+ export function previewText(body, media) {
1562
+ const text = String(body || "").replace(/\s+/g, " ").trim();
1563
+ if (!media) return text;
1564
+ const caption = text.replace(/^\[[^\]]*\]\s*/, "").trim();
1565
+ const glyph = MEDIA_GLYPH[media.kind] || MEDIA_GLYPH.file;
1566
+ return `${glyph} ${caption || media.name || media.kind}`.trim();
1567
+ }
1568
+
645
1569
  // Read one channel+day thread shaped for the web chat viewer:
646
1570
  // { id, channel, messages: [{ role, content, ts, … }] } — or null when missing.
647
1571
  // Tool records are INCLUDED (role:"tool" with structured tool/args/result from
@@ -655,81 +1579,141 @@ export function listGlobalThreads({ channels, _globalMessagesDir } = {}) {
655
1579
  // usage. All of it is already on disk in `meta`; dropping it here is what made
656
1580
  // a reloaded thread render "0 tok" with no model, even though the live stream
657
1581
  // showed both.
658
- export function readGlobalThread({ channel, date, _globalMessagesDir } = {}) {
1582
+ // THE ONE INTERPRETER FOR LEDGER ROWS viewer messages.
1583
+ //
1584
+ // Every channel's history is read back through THIS function: telegram, web,
1585
+ // desktop, a2a — all of them. It is what carries a turn's attribution (who
1586
+ // answered, its actor_kind), its MODEL, its token USAGE, its tool summary and
1587
+ // reasoning from the raw JSONL row out to the thread viewer. A channel that
1588
+ // shapes its own rows by hand WILL drop these (that is exactly how a2a first
1589
+ // shipped showing "0 tok" and no model). So: if you add a new channel, read it
1590
+ // back with shapeLedgerMessage — never re-map rows inline. The only thing a
1591
+ // caller does first is normalise the raw row (e.g. a2a sets agent_slug to the
1592
+ // speaker before shaping, because it double-logs under both peers).
1593
+ export function shapeLedgerMessage(r) {
1594
+ if (r.type === "tool") {
1595
+ return {
1596
+ role: "tool",
1597
+ content: r.body || "",
1598
+ ts: r.ts,
1599
+ tool: r.meta?.tool || r.meta?.tool_name || r.actor_id || "tool",
1600
+ args: r.meta?.args,
1601
+ result: r.meta?.result,
1602
+ };
1603
+ }
1604
+ if (r.type === "user") {
1605
+ const media = mediaFromMeta(r.meta);
1606
+ return { role: "user", content: r.body || "", ts: r.ts, ...(media ? { media } : {}) };
1607
+ }
1608
+ const usage = r.meta?.usage;
1609
+ // Images the agent attached to its own message (attach_media → routine
1610
+ // delivery). Stored on the row meta the same way an inbound upload is, so the
1611
+ // thread viewer can render an agent-sent photo, not only a user-sent one.
1612
+ const media = mediaFromMeta(r.meta);
1613
+ return {
1614
+ role: "assistant",
1615
+ content: r.body || "",
1616
+ ts: r.ts,
1617
+ ...(media ? { media } : {}),
1618
+ ...(Array.isArray(r.meta?.media) && r.meta.media.length ? { media_list: r.meta.media } : {}),
1619
+ // Stable id of who answered (super_agent | project-agent slug) plus the
1620
+ // display name that was shown on the channel.
1621
+ ...(r.agent_slug || r.actor_id ? { agent: r.agent_slug || r.actor_id } : {}),
1622
+ ...(r.author ? { agent_name: r.author } : {}),
1623
+ ...(r.actor_kind ? { actor_kind: r.actor_kind } : {}),
1624
+ // Group only: which agent's @mention pulled this speaker in ("traído por X").
1625
+ ...(r.meta?.reason ? { reason: r.meta.reason } : {}),
1626
+ ...(r.meta?.model ? { model: r.meta.model } : {}),
1627
+ ...(usage && typeof usage === "object" ? { usage } : {}),
1628
+ // What the turn actually did. Recorded compactly at write time
1629
+ // (core/agent/tool-summary.js) because the live tool events are gone
1630
+ // by the time anyone reads the thread back.
1631
+ ...(r.meta?.tool_summary ? { tool_summary: r.meta.tool_summary } : {}),
1632
+ // Which skills the per-turn RAG put in the prompt. Same reasoning as
1633
+ // tool_summary: the live `skill_inspector` event is gone by read time.
1634
+ ...(r.meta?.skill_inspector ? { skill_inspector: r.meta.skill_inspector } : {}),
1635
+ // The model's thinking for that turn, one entry per model pass. Kept in
1636
+ // meta and handed out only here, so it reaches the thread viewer and
1637
+ // nothing that feeds the model.
1638
+ ...(Array.isArray(r.meta?.reasoning) && r.meta.reasoning.length
1639
+ ? { reasoning: r.meta.reasoning }
1640
+ : {}),
1641
+ };
1642
+ }
1643
+
1644
+ export function readGlobalThread({ channel, date, project, _globalMessagesDir } = {}) {
659
1645
  if (!CHANNEL_NAME_RE.test(String(channel || ""))) return null;
660
1646
  if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ""))) return null;
661
1647
  const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
662
1648
  const file = path.join(base, channel, `${date}.jsonl`);
663
1649
  if (!fs.existsSync(file)) return null;
664
- const messages = parseDayJsonl(fs.readFileSync(file, "utf8"))
665
- .filter((r) => r.type === "user" || r.type === "agent" || r.type === "tool")
666
- .map((r) => {
667
- if (r.type === "tool") {
668
- return {
669
- role: "tool",
670
- content: r.body || "",
671
- ts: r.ts,
672
- tool: r.meta?.tool || r.meta?.tool_name || r.actor_id || "tool",
673
- args: r.meta?.args,
674
- result: r.meta?.result,
675
- };
676
- }
677
- if (r.type === "user") {
678
- return { role: "user", content: r.body || "", ts: r.ts };
679
- }
680
- const usage = r.meta?.usage;
681
- return {
682
- role: "assistant",
683
- content: r.body || "",
684
- ts: r.ts,
685
- // Stable id of who answered (super_agent | project-agent slug) plus the
686
- // display name that was shown on the channel.
687
- ...(r.agent_slug || r.actor_id ? { agent: r.agent_slug || r.actor_id } : {}),
688
- ...(r.author ? { agent_name: r.author } : {}),
689
- ...(r.actor_kind ? { actor_kind: r.actor_kind } : {}),
690
- ...(r.meta?.model ? { model: r.meta.model } : {}),
691
- ...(usage && typeof usage === "object" ? { usage } : {}),
692
- // What the turn actually did. Recorded compactly at write time
693
- // (core/agent/tool-summary.js) because the live tool events are gone
694
- // by the time anyone reads the thread back.
695
- ...(r.meta?.tool_summary ? { tool_summary: r.meta.tool_summary } : {}),
696
- };
697
- });
698
- return { id: date, channel, messages };
1650
+ const messages = keepForProject(
1651
+ parseDayJsonl(fs.readFileSync(file, "utf8"))
1652
+ .filter((r) => r.type === "user" || r.type === "agent" || r.type === "tool"),
1653
+ project,
1654
+ ).map(shapeLedgerMessage);
1655
+ // A day-file the asking project owns no turn in is not its thread to read.
1656
+ // The file exists — another project's chat is in it — so without this a
1657
+ // stale link or a bookmarked URL opened an empty pane instead of taking the
1658
+ // not-found path. Unscoped reads keep answering for the whole file.
1659
+ if (!messages.length && project !== undefined && project !== null && project !== "") return null;
1660
+ // The thread's name travels with it. Deriving it a second time on the client
1661
+ // would put the same rule in two places, and the reader's own name for it —
1662
+ // which only this index knows — would never reach the header at all.
1663
+ const over = readThreadMeta(base)[threadKey(channel, date)] || {};
1664
+ const firstUser = messages.find((m) => m.role === "user");
1665
+ const derived = String((firstUser || messages[0] || {}).content || "")
1666
+ .replace(/\s+/g, " ")
1667
+ .trim()
1668
+ .slice(0, 80);
1669
+ return {
1670
+ id: date,
1671
+ channel,
1672
+ title: over.title || derived || `${channel} · ${date}`,
1673
+ archived: over.archived || undefined,
1674
+ messages,
1675
+ };
699
1676
  }
700
1677
 
701
1678
  // Delete one channel+day thread by removing its JSONL file. The global ledger
702
1679
  // is FS-backed (listGlobalThreads/readGlobalThread read files directly), so
703
1680
  // unlinking the day-file drops the thread from the sidebar. Returns false for a
704
1681
  // bad channel/date or a file that is already gone.
705
- export function deleteGlobalThread({ channel, date, _globalMessagesDir } = {}) {
1682
+ export function deleteGlobalThread({ channel, date, project, _globalMessagesDir } = {}) {
706
1683
  if (!CHANNEL_NAME_RE.test(String(channel || ""))) return false;
707
1684
  if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ""))) return false;
708
1685
  const base = _globalMessagesDir || GLOBAL_MESSAGES_DIR;
709
1686
  const file = path.join(base, channel, `${date}.jsonl`);
710
1687
  if (!fs.existsSync(file)) return false;
1688
+ // Scoped delete: a day-file can hold turns from several projects, and the
1689
+ // sidebar that offered the Delete button was showing only one of them.
1690
+ // Dropping the whole file there would take another project's chat with it —
1691
+ // including the default workspace's unstamped Telegram and desktop turns,
1692
+ // which the old predicate swept up on every scoped delete.
1693
+ if (project !== undefined && project !== null && project !== "") {
1694
+ const rows = parseDayJsonl(fs.readFileSync(file, "utf8"));
1695
+ const keep = rows.filter((r) => !rowBelongsTo(r, String(project)));
1696
+ if (keep.length === rows.length) return false;
1697
+ if (keep.length === 0) fs.unlinkSync(file);
1698
+ else fs.writeFileSync(file, keep.map((r) => JSON.stringify(r)).join("\n") + "\n");
1699
+ return true;
1700
+ }
711
1701
  fs.unlinkSync(file);
712
1702
  return true;
713
1703
  }
714
1704
 
715
- // Wipe the cache and re-populate from APX project messages. Reads BOTH `.jsonl`
716
- // (current format) and `.md` (legacy). Called by rebuild.
1705
+ // Wipe the cache and re-populate from APX project messages. Called by rebuild.
717
1706
  export function rebuildMessagesFromFs(db, projectRoot) {
718
1707
  const dir = path.join(projectRoot, "messages");
719
1708
  if (!fs.existsSync(dir)) return { count: 0 };
720
1709
  db.prepare("DELETE FROM messages").run();
721
1710
 
722
- // Collect every line from every .jsonl + .md, parse, sort by ts so the
723
- // SQL row ids end up in the right order.
1711
+ // Collect every line from every .jsonl, parse, sort by ts so the SQL row
1712
+ // ids end up in the right order.
724
1713
  const all = [];
725
1714
  for (const f of fs.readdirSync(dir).sort()) {
726
- const full = path.join(dir, f);
727
- const text = fs.readFileSync(full, "utf8");
728
- if (/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) {
729
- all.push(...parseDayJsonl(text));
730
- } else if (/^\d{4}-\d{2}-\d{2}\.md$/.test(f)) {
731
- all.push(...parseDayFile(text));
732
- }
1715
+ if (!/^\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)) continue;
1716
+ all.push(...parseDayJsonl(fs.readFileSync(path.join(dir, f), "utf8")));
733
1717
  }
734
1718
  all.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
735
1719