@hybridaione/hybridclaw 0.2.2 → 0.2.6

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 (277) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +85 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +41 -16
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +71 -8
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1303 -0
  12. package/container/src/browser-tools.ts +431 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +34 -13
  15. package/container/src/index.ts +451 -109
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +20 -10
  18. package/container/src/tools.ts +599 -225
  19. package/container/src/types.ts +32 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +10 -2
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +19 -3
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +4 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +45 -4
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +6 -3
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +621 -125
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +16 -8
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +47 -22
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +19 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +103 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +58 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +563 -73
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +179 -24
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +48 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/instruction-approval-audit.d.ts.map +1 -1
  132. package/dist/instruction-approval-audit.js.map +1 -1
  133. package/dist/instruction-integrity.d.ts.map +1 -1
  134. package/dist/instruction-integrity.js +8 -2
  135. package/dist/instruction-integrity.js.map +1 -1
  136. package/dist/ipc.d.ts.map +1 -1
  137. package/dist/ipc.js +6 -1
  138. package/dist/ipc.js.map +1 -1
  139. package/dist/logger.js.map +1 -1
  140. package/dist/memory-consolidation.d.ts +17 -0
  141. package/dist/memory-consolidation.d.ts.map +1 -0
  142. package/dist/memory-consolidation.js +25 -0
  143. package/dist/memory-consolidation.js.map +1 -0
  144. package/dist/memory-service.d.ts +200 -0
  145. package/dist/memory-service.d.ts.map +1 -0
  146. package/dist/memory-service.js +294 -0
  147. package/dist/memory-service.js.map +1 -0
  148. package/dist/mount-security.d.ts.map +1 -1
  149. package/dist/mount-security.js +31 -7
  150. package/dist/mount-security.js.map +1 -1
  151. package/dist/observability-ingest.d.ts.map +1 -1
  152. package/dist/observability-ingest.js +32 -11
  153. package/dist/observability-ingest.js.map +1 -1
  154. package/dist/onboarding.d.ts.map +1 -1
  155. package/dist/onboarding.js +32 -9
  156. package/dist/onboarding.js.map +1 -1
  157. package/dist/proactive-policy.d.ts.map +1 -1
  158. package/dist/proactive-policy.js +2 -1
  159. package/dist/proactive-policy.js.map +1 -1
  160. package/dist/prompt-hooks.d.ts.map +1 -1
  161. package/dist/prompt-hooks.js +9 -7
  162. package/dist/prompt-hooks.js.map +1 -1
  163. package/dist/runtime-config.d.ts +98 -1
  164. package/dist/runtime-config.d.ts.map +1 -1
  165. package/dist/runtime-config.js +477 -23
  166. package/dist/runtime-config.js.map +1 -1
  167. package/dist/scheduled-task-runner.d.ts +1 -0
  168. package/dist/scheduled-task-runner.d.ts.map +1 -1
  169. package/dist/scheduled-task-runner.js +29 -10
  170. package/dist/scheduled-task-runner.js.map +1 -1
  171. package/dist/scheduler.d.ts +43 -4
  172. package/dist/scheduler.d.ts.map +1 -1
  173. package/dist/scheduler.js +530 -56
  174. package/dist/scheduler.js.map +1 -1
  175. package/dist/session-export.d.ts +26 -0
  176. package/dist/session-export.d.ts.map +1 -0
  177. package/dist/session-export.js +149 -0
  178. package/dist/session-export.js.map +1 -0
  179. package/dist/session-maintenance.d.ts.map +1 -1
  180. package/dist/session-maintenance.js +75 -13
  181. package/dist/session-maintenance.js.map +1 -1
  182. package/dist/session-transcripts.d.ts.map +1 -1
  183. package/dist/session-transcripts.js.map +1 -1
  184. package/dist/side-effects.d.ts.map +1 -1
  185. package/dist/side-effects.js +14 -2
  186. package/dist/side-effects.js.map +1 -1
  187. package/dist/skills-guard.d.ts.map +1 -1
  188. package/dist/skills-guard.js +893 -130
  189. package/dist/skills-guard.js.map +1 -1
  190. package/dist/skills.d.ts +5 -0
  191. package/dist/skills.d.ts.map +1 -1
  192. package/dist/skills.js +29 -15
  193. package/dist/skills.js.map +1 -1
  194. package/dist/token-efficiency.d.ts.map +1 -1
  195. package/dist/token-efficiency.js.map +1 -1
  196. package/dist/tui.js +92 -11
  197. package/dist/tui.js.map +1 -1
  198. package/dist/types.d.ts +146 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +24 -1
  201. package/dist/types.js.map +1 -1
  202. package/dist/update.d.ts.map +1 -1
  203. package/dist/update.js +42 -14
  204. package/dist/update.js.map +1 -1
  205. package/dist/workspace.d.ts.map +1 -1
  206. package/dist/workspace.js +49 -9
  207. package/dist/workspace.js.map +1 -1
  208. package/docs/chat.html +9 -3
  209. package/docs/index.html +37 -13
  210. package/package.json +8 -2
  211. package/src/agent.ts +16 -3
  212. package/src/audit-cli.ts +44 -16
  213. package/src/audit-events.ts +69 -5
  214. package/src/audit-trail.ts +41 -15
  215. package/src/channels/discord/attachments.ts +81 -27
  216. package/src/channels/discord/debounce.ts +25 -0
  217. package/src/channels/discord/delivery.ts +57 -13
  218. package/src/channels/discord/human-delay.ts +48 -0
  219. package/src/channels/discord/inbound.ts +66 -7
  220. package/src/channels/discord/mentions.ts +42 -18
  221. package/src/channels/discord/presence.ts +148 -0
  222. package/src/channels/discord/rate-limiter.ts +58 -0
  223. package/src/channels/discord/reactions.ts +211 -0
  224. package/src/channels/discord/runtime.ts +1048 -182
  225. package/src/channels/discord/stream.ts +73 -27
  226. package/src/channels/discord/tool-actions.ts +78 -37
  227. package/src/channels/discord/typing.ts +140 -0
  228. package/src/chunk.ts +12 -4
  229. package/src/cli.ts +141 -56
  230. package/src/config.ts +192 -34
  231. package/src/container-runner.ts +132 -42
  232. package/src/container-setup.ts +57 -22
  233. package/src/conversation.ts +9 -7
  234. package/src/db.ts +2217 -84
  235. package/src/delegation-manager.ts +6 -2
  236. package/src/gateway-client.ts +41 -17
  237. package/src/gateway-service.ts +1019 -201
  238. package/src/gateway-types.ts +33 -0
  239. package/src/gateway.ts +321 -48
  240. package/src/health.ts +66 -26
  241. package/src/heartbeat.ts +84 -22
  242. package/src/hybridai-bots.ts +14 -5
  243. package/src/instruction-approval-audit.ts +4 -1
  244. package/src/instruction-integrity.ts +30 -9
  245. package/src/ipc.ts +23 -5
  246. package/src/logger.ts +4 -1
  247. package/src/memory-consolidation.ts +41 -0
  248. package/src/memory-service.ts +606 -0
  249. package/src/mount-security.ts +58 -13
  250. package/src/observability-ingest.ts +134 -35
  251. package/src/onboarding.ts +126 -35
  252. package/src/proactive-policy.ts +3 -1
  253. package/src/prompt-hooks.ts +40 -17
  254. package/src/runtime-config.ts +1114 -99
  255. package/src/scheduled-task-runner.ts +63 -11
  256. package/src/scheduler.ts +683 -60
  257. package/src/session-export.ts +196 -0
  258. package/src/session-maintenance.ts +125 -22
  259. package/src/session-transcripts.ts +12 -3
  260. package/src/side-effects.ts +28 -5
  261. package/src/skills-guard.ts +1067 -219
  262. package/src/skills.ts +163 -65
  263. package/src/token-efficiency.ts +31 -9
  264. package/src/tui.ts +166 -25
  265. package/src/types.ts +195 -2
  266. package/src/update.ts +79 -23
  267. package/src/workspace.ts +63 -11
  268. package/tests/approval-policy.test.ts +224 -0
  269. package/tests/discord.basic.test.ts +82 -2
  270. package/tests/discord.human-presence.test.ts +85 -0
  271. package/tests/gateway-service.media-routing.test.ts +8 -2
  272. package/tests/memory-service.test.ts +1114 -0
  273. package/tests/token-efficiency.basic.test.ts +8 -2
  274. package/vitest.e2e.config.ts +3 -1
  275. package/vitest.integration.config.ts +3 -1
  276. package/vitest.live.config.ts +3 -1
  277. package/vitest.unit.config.ts +9 -0
@@ -1,25 +1,27 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { CronExpressionParser } from 'cron-parser';
2
- import { spawnSync } from 'child_process';
3
- import { APP_VERSION, DISCORD_COMMANDS_ONLY, DISCORD_RESPOND_TO_ALL_MESSAGES, HYBRIDAI_CHATBOT_ID, HYBRIDAI_ENABLE_RAG, HYBRIDAI_MODEL, HYBRIDAI_MODELS, PROACTIVE_AUTO_RETRY_BASE_DELAY_MS, PROACTIVE_AUTO_RETRY_ENABLED, PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS, PROACTIVE_DELEGATION_MAX_DEPTH, PROACTIVE_DELEGATION_MAX_PER_TURN, PROACTIVE_RALPH_MAX_ITERATIONS, } from './config.js';
4
3
  import { runAgent } from './agent.js';
5
- import { getActiveContainerCount, stopSessionContainer } from './container-runner.js';
6
- import { clearSessionHistory, createTask, deleteTask, getAllSessions, getConversationHistory, getOrCreateSession, getQueuedProactiveMessageCount, getRecentStructuredAuditForSession, getSessionCount, getTasksForSession, logAudit, storeMessage, toggleTask, updateSessionChatbot, updateSessionModel, updateSessionRag, } from './db.js';
7
- import { emitToolExecutionAuditEvents, makeAuditRunId, recordAuditEvent } from './audit-events.js';
4
+ import { emitToolExecutionAuditEvents, makeAuditRunId, recordAuditEvent, } from './audit-events.js';
5
+ import { APP_VERSION, DISCORD_COMMANDS_ONLY, DISCORD_FREE_RESPONSE_CHANNELS, DISCORD_GROUP_POLICY, DISCORD_GUILDS, DISCORD_RESPOND_TO_ALL_MESSAGES, HYBRIDAI_CHATBOT_ID, HYBRIDAI_ENABLE_RAG, HYBRIDAI_MODEL, HYBRIDAI_MODELS, PROACTIVE_AUTO_RETRY_BASE_DELAY_MS, PROACTIVE_AUTO_RETRY_ENABLED, PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS, PROACTIVE_DELEGATION_MAX_DEPTH, PROACTIVE_DELEGATION_MAX_PER_TURN, PROACTIVE_RALPH_MAX_ITERATIONS, } from './config.js';
6
+ import { getActiveContainerCount, stopSessionContainer, } from './container-runner.js';
7
+ import { buildConversationContext } from './conversation.js';
8
+ import { createTask, deleteTask, getAllSessions, getQueuedProactiveMessageCount, getRecentStructuredAuditForSession, getSessionCount, getTasksForSession, getUsageTotals, listUsageByAgent, listUsageByModel, logAudit, pauseTask, recordUsageEvent, resumeTask, updateSessionChatbot, updateSessionModel, updateSessionRag, } from './db.js';
9
+ import { delegationQueueStatus, enqueueDelegation, } from './delegation-manager.js';
10
+ import { renderGatewayCommand, } from './gateway-types.js';
8
11
  import { fetchHybridAIBots } from './hybridai-bots.js';
9
12
  import { logger } from './logger.js';
13
+ import { memoryService } from './memory-service.js';
10
14
  import { getObservabilityIngestState } from './observability-ingest.js';
11
- import { rearmScheduler } from './scheduler.js';
15
+ import { updateRuntimeConfig } from './runtime-config.js';
16
+ import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
17
+ import { getSchedulerStatus, rearmScheduler } from './scheduler.js';
18
+ import { exportSessionSnapshotJsonl } from './session-export.js';
12
19
  import { maybeCompactSession } from './session-maintenance.js';
13
20
  import { appendSessionTranscript } from './session-transcripts.js';
14
21
  import { processSideEffects } from './side-effects.js';
15
22
  import { expandSkillInvocation } from './skills.js';
16
- import { renderGatewayCommand, } from './gateway-types.js';
23
+ import { estimateTokenCountFromMessages, estimateTokenCountFromText, } from './token-efficiency.js';
17
24
  import { ensureBootstrapFiles } from './workspace.js';
18
- import { buildConversationContext } from './conversation.js';
19
- import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
20
- import { delegationQueueStatus, enqueueDelegation } from './delegation-manager.js';
21
- import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
22
- import { updateRuntimeConfig } from './runtime-config.js';
23
25
  const BOT_CACHE_TTL = 300_000; // 5 minutes
24
26
  const MAX_HISTORY_MESSAGES = 40;
25
27
  const BASE_SUBAGENT_ALLOWED_TOOLS = [
@@ -50,10 +52,15 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
50
52
  'browser_network',
51
53
  'browser_close',
52
54
  ];
53
- const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [...BASE_SUBAGENT_ALLOWED_TOOLS, 'delegate'];
55
+ const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
56
+ ...BASE_SUBAGENT_ALLOWED_TOOLS,
57
+ 'delegate',
58
+ ];
54
59
  const MAX_DELEGATION_TASKS = 6;
55
60
  const MAX_DELEGATION_USER_CHARS = 500;
56
61
  const MAX_RALPH_ITERATIONS = 64;
62
+ const DISCORD_CHANNEL_MODE_VALUES = new Set(['off', 'mention', 'free']);
63
+ const DISCORD_GROUP_POLICY_VALUES = new Set(['open', 'allowlist', 'disabled']);
57
64
  const IMAGE_QUESTION_RE = /(what(?:'s| is)? on (?:the )?(?:image|picture|photo|screenshot)|describe (?:this|the) (?:image|picture|photo)|image|picture|photo|screenshot|ocr|diagram|chart|grafik|bild|foto|was steht|was ist auf dem bild)/i;
58
65
  const BROWSER_TAB_RE = /(browser|tab|current tab|web page|website|seite im browser|aktuellen tab)/i;
59
66
  const TRANSIENT_DELEGATION_ERROR_PATTERNS = [
@@ -106,7 +113,9 @@ function normalizeMediaContextItems(raw) {
106
113
  for (const item of raw) {
107
114
  if (!item || typeof item !== 'object')
108
115
  continue;
109
- const path = typeof item.path === 'string' && item.path.trim() ? item.path.trim() : null;
116
+ const path = typeof item.path === 'string' && item.path.trim()
117
+ ? item.path.trim()
118
+ : null;
110
119
  const url = typeof item.url === 'string' ? item.url.trim() : '';
111
120
  const originalUrl = typeof item.originalUrl === 'string' ? item.originalUrl.trim() : '';
112
121
  const filename = typeof item.filename === 'string' ? item.filename.trim() : '';
@@ -132,7 +141,9 @@ function normalizeMediaContextItems(raw) {
132
141
  function buildMediaPromptContext(media) {
133
142
  if (media.length === 0)
134
143
  return '';
135
- const mediaPaths = media.map((item) => item.path).filter((path) => Boolean(path));
144
+ const mediaPaths = media
145
+ .map((item) => item.path)
146
+ .filter((path) => Boolean(path));
136
147
  const mediaUrls = media.map((item) => item.url);
137
148
  const mediaTypes = media.map((item) => item.mimeType || 'unknown');
138
149
  const payload = media.map((item, index) => ({
@@ -237,7 +248,9 @@ function formatRelativeTime(raw) {
237
248
  return `${Math.max(1, Math.floor(deltaMs / 86_400_000))}d ago`;
238
249
  }
239
250
  function numberFromUnknown(value) {
240
- if (typeof value !== 'number' || Number.isNaN(value) || !Number.isFinite(value))
251
+ if (typeof value !== 'number' ||
252
+ Number.isNaN(value) ||
253
+ !Number.isFinite(value))
241
254
  return null;
242
255
  return value;
243
256
  }
@@ -265,7 +278,9 @@ function formatCompactNumber(value) {
265
278
  return 'n/a';
266
279
  const abs = Math.abs(value);
267
280
  if (abs >= 1_000_000) {
268
- const scaled = abs >= 10_000_000 ? (value / 1_000_000).toFixed(0) : (value / 1_000_000).toFixed(1);
281
+ const scaled = abs >= 10_000_000
282
+ ? (value / 1_000_000).toFixed(0)
283
+ : (value / 1_000_000).toFixed(1);
269
284
  return `${scaled.replace(/\.0$/, '')}M`;
270
285
  }
271
286
  if (abs >= 1_000) {
@@ -279,13 +294,108 @@ function formatPercent(value) {
279
294
  return 'n/a';
280
295
  return `${Math.max(0, Math.min(100, Math.round(value)))}%`;
281
296
  }
297
+ function formatUsd(value) {
298
+ if (value == null || Number.isNaN(value) || !Number.isFinite(value)) {
299
+ return 'n/a';
300
+ }
301
+ if (value <= 0)
302
+ return '$0.0000';
303
+ if (value >= 1)
304
+ return `$${value.toFixed(2)}`;
305
+ if (value >= 0.01)
306
+ return `$${value.toFixed(4)}`;
307
+ return `$${value.toFixed(6)}`;
308
+ }
309
+ function resolveSessionAgentId(session) {
310
+ const sessionAgent = session.chatbot_id?.trim();
311
+ if (sessionAgent)
312
+ return sessionAgent;
313
+ const defaultAgent = HYBRIDAI_CHATBOT_ID?.trim();
314
+ if (defaultAgent)
315
+ return defaultAgent;
316
+ return null;
317
+ }
318
+ function extractUsageCostUsd(tokenUsage) {
319
+ if (!tokenUsage)
320
+ return 0;
321
+ const costCarrier = tokenUsage;
322
+ const value = firstNumber([
323
+ costCarrier.costUsd,
324
+ costCarrier.costUSD,
325
+ costCarrier.cost_usd,
326
+ costCarrier.estimatedCostUsd,
327
+ costCarrier.estimated_cost_usd,
328
+ ]);
329
+ if (value == null)
330
+ return 0;
331
+ return Math.max(0, value);
332
+ }
333
+ function formatCanonicalContextPrompt(params) {
334
+ const sections = [];
335
+ const summary = (params.summary || '').trim();
336
+ if (summary) {
337
+ sections.push(['### Canonical Session Summary', summary].join('\n'));
338
+ }
339
+ if (params.recentMessages.length > 0) {
340
+ const lines = params.recentMessages.slice(-6).map((entry) => {
341
+ const role = (entry.role || 'user').trim().toLowerCase();
342
+ const who = role === 'assistant' ? 'Assistant' : 'User';
343
+ const from = entry.channel_id && entry.channel_id.trim()
344
+ ? `${entry.channel_id.trim()} (${entry.session_id})`
345
+ : entry.session_id;
346
+ const compact = entry.content.replace(/\s+/g, ' ').trim();
347
+ const short = compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
348
+ return `- ${who} [${from}]: ${short}`;
349
+ });
350
+ sections.push([
351
+ '### Cross-Channel Recall',
352
+ 'Recent context from other sessions/channels for this user:',
353
+ ...lines,
354
+ ].join('\n'));
355
+ }
356
+ const merged = sections.join('\n\n').trim();
357
+ return merged || null;
358
+ }
282
359
  function resolveActivationModeLabel() {
283
360
  if (DISCORD_COMMANDS_ONLY)
284
361
  return 'commands-only';
362
+ if (DISCORD_GROUP_POLICY === 'disabled')
363
+ return 'disabled';
364
+ if (DISCORD_GROUP_POLICY === 'allowlist')
365
+ return 'allowlist';
366
+ if (DISCORD_FREE_RESPONSE_CHANNELS.length > 0)
367
+ return `mention + ${DISCORD_FREE_RESPONSE_CHANNELS.length} free channel(s)`;
285
368
  if (DISCORD_RESPOND_TO_ALL_MESSAGES)
286
369
  return 'all messages';
287
370
  return 'mention';
288
371
  }
372
+ function resolveGuildChannelMode(guildId, channelId) {
373
+ if (!guildId)
374
+ return 'free';
375
+ if (DISCORD_GROUP_POLICY === 'disabled')
376
+ return 'off';
377
+ const guild = DISCORD_GUILDS[guildId];
378
+ const explicit = guild?.channels[channelId]?.mode;
379
+ if (DISCORD_GROUP_POLICY === 'allowlist') {
380
+ return explicit ?? 'off';
381
+ }
382
+ if (explicit === 'off' || explicit === 'mention' || explicit === 'free') {
383
+ return explicit;
384
+ }
385
+ if (DISCORD_FREE_RESPONSE_CHANNELS.includes(channelId))
386
+ return 'free';
387
+ if (guild) {
388
+ const defaultMode = guild.defaultMode;
389
+ if (defaultMode === 'off' ||
390
+ defaultMode === 'mention' ||
391
+ defaultMode === 'free') {
392
+ return defaultMode;
393
+ }
394
+ }
395
+ if (DISCORD_RESPOND_TO_ALL_MESSAGES)
396
+ return 'free';
397
+ return 'mention';
398
+ }
289
399
  function readSessionStatusSnapshot(sessionId) {
290
400
  const entries = getRecentStructuredAuditForSession(sessionId, 160);
291
401
  let usagePayload = null;
@@ -326,16 +436,18 @@ function readSessionStatusSnapshot(sessionId) {
326
436
  const cacheWrite = Math.max(0, cacheWriteTokens || 0);
327
437
  const cacheTotal = cacheRead + cacheWrite;
328
438
  const cacheHitPercent = cacheTotal > 0 ? (cacheRead / cacheTotal) * 100 : null;
329
- const contextUsedTokens = firstNumber([contextPayload?.historyEstimatedTokens]);
439
+ const contextUsedTokens = firstNumber([
440
+ contextPayload?.historyEstimatedTokens,
441
+ ]);
330
442
  const contextBudgetTokens = (() => {
331
443
  const maxChars = firstNumber([contextPayload?.historyMaxChars]);
332
444
  if (maxChars == null || maxChars <= 0)
333
445
  return null;
334
446
  return Math.max(1, Math.round(maxChars / 4));
335
447
  })();
336
- const contextUsagePercent = (contextUsedTokens != null
337
- && contextBudgetTokens != null
338
- && contextBudgetTokens > 0)
448
+ const contextUsagePercent = contextUsedTokens != null &&
449
+ contextBudgetTokens != null &&
450
+ contextBudgetTokens > 0
339
451
  ? (contextUsedTokens / contextBudgetTokens) * 100
340
452
  : null;
341
453
  return {
@@ -387,17 +499,53 @@ function isVersionOnlyQuestion(raw) {
387
499
  if (detailedRuntimeTokens.some((token) => text.includes(token)))
388
500
  return false;
389
501
  const words = text.split(' ').filter(Boolean);
390
- if (words.length > 8
391
- && !text.includes('welche version')
392
- && !text.includes('what version')
393
- && !text.includes('which version')) {
502
+ if (words.length > 8 &&
503
+ !text.includes('welche version') &&
504
+ !text.includes('what version') &&
505
+ !text.includes('which version')) {
394
506
  return false;
395
507
  }
396
508
  return true;
397
509
  }
398
510
  function recordSuccessfulTurn(opts) {
399
- storeMessage(opts.sessionId, opts.userId, opts.username, 'user', opts.userContent);
400
- storeMessage(opts.sessionId, 'assistant', null, 'assistant', opts.resultText);
511
+ memoryService.storeTurn({
512
+ sessionId: opts.sessionId,
513
+ user: {
514
+ userId: opts.userId,
515
+ username: opts.username,
516
+ content: opts.userContent,
517
+ },
518
+ assistant: {
519
+ userId: 'assistant',
520
+ username: null,
521
+ content: opts.resultText,
522
+ },
523
+ });
524
+ try {
525
+ if (opts.userId.trim()) {
526
+ memoryService.appendCanonicalMessages({
527
+ agentId: opts.agentId,
528
+ userId: opts.userId,
529
+ newMessages: [
530
+ {
531
+ role: 'user',
532
+ content: opts.userContent,
533
+ sessionId: opts.sessionId,
534
+ channelId: opts.channelId,
535
+ },
536
+ {
537
+ role: 'assistant',
538
+ content: opts.resultText,
539
+ sessionId: opts.sessionId,
540
+ channelId: opts.channelId,
541
+ },
542
+ ],
543
+ });
544
+ }
545
+ }
546
+ catch (err) {
547
+ logger.debug({ sessionId: opts.sessionId, userId: opts.userId, err }, 'Failed to append canonical session memory');
548
+ }
401
549
  appendSessionTranscript(opts.agentId, {
402
550
  sessionId: opts.sessionId,
403
551
  channelId: opts.channelId,
@@ -484,13 +632,18 @@ function buildTokenUsageAuditPayload(messages, resultText, tokenUsage) {
484
632
  const fallbackEstimatedCompletionTokens = estimateTokenCountFromText(resultText || '');
485
633
  const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || fallbackEstimatedPromptTokens;
486
634
  const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || fallbackEstimatedCompletionTokens;
487
- const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens || (estimatedPromptTokens + estimatedCompletionTokens);
635
+ const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens ||
636
+ estimatedPromptTokens + estimatedCompletionTokens;
488
637
  const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
489
638
  const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
490
639
  const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
491
- const apiTotalTokens = tokenUsage?.apiTotalTokens || (apiPromptTokens + apiCompletionTokens);
492
- const promptTokens = apiUsageAvailable ? apiPromptTokens : estimatedPromptTokens;
493
- const completionTokens = apiUsageAvailable ? apiCompletionTokens : estimatedCompletionTokens;
640
+ const apiTotalTokens = tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
641
+ const promptTokens = apiUsageAvailable
642
+ ? apiPromptTokens
643
+ : estimatedPromptTokens;
644
+ const completionTokens = apiUsageAvailable
645
+ ? apiCompletionTokens
646
+ : estimatedCompletionTokens;
494
647
  const totalTokens = apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens;
495
648
  return {
496
649
  modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
@@ -520,10 +673,15 @@ export function getGatewayStatus() {
520
673
  ragDefault: HYBRIDAI_ENABLE_RAG,
521
674
  timestamp: new Date().toISOString(),
522
675
  observability: getObservabilityIngestState(),
676
+ scheduler: {
677
+ jobs: getSchedulerStatus(),
678
+ },
523
679
  };
524
680
  }
525
681
  export function getGatewayHistory(sessionId, limit = MAX_HISTORY_MESSAGES) {
526
- return getConversationHistory(sessionId, Math.max(1, Math.min(limit, 200))).reverse();
682
+ return memoryService
683
+ .getConversationHistory(sessionId, Math.max(1, Math.min(limit, 200)))
684
+ .reverse();
527
685
  }
528
686
  function extractDelegationDepth(sessionId) {
529
687
  const match = sessionId.match(/^delegate:d(\d+):/);
@@ -533,7 +691,9 @@ function extractDelegationDepth(sessionId) {
533
691
  return Number.isFinite(parsed) ? parsed : 0;
534
692
  }
535
693
  function nextDelegationSessionId(parentSessionId, nextDepth) {
536
- const safeParent = parentSessionId.replace(/[^a-zA-Z0-9:_-]/g, '-').slice(0, 48);
694
+ const safeParent = parentSessionId
695
+ .replace(/[^a-zA-Z0-9:_-]/g, '-')
696
+ .slice(0, 48);
537
697
  const nonce = Math.random().toString(36).slice(2, 8);
538
698
  return `delegate:d${nextDepth}:${safeParent}:${Date.now()}:${nonce}`;
539
699
  }
@@ -602,7 +762,9 @@ function classifyDelegationError(errorText) {
602
762
  return 'unknown';
603
763
  }
604
764
  function inferDelegationStatus(errorText) {
605
- return /timeout|timed out|deadline exceeded/i.test(errorText) ? 'timeout' : 'failed';
765
+ return /timeout|timed out|deadline exceeded/i.test(errorText)
766
+ ? 'timeout'
767
+ : 'failed';
606
768
  }
607
769
  function normalizeDelegationTask(raw, fallbackModel) {
608
770
  if (!raw || typeof raw !== 'object' || Array.isArray(raw))
@@ -661,7 +823,9 @@ function normalizeDelegationEffect(effect, fallbackModel) {
661
823
  return { error: `${mode} delegation requires at least one task` };
662
824
  }
663
825
  if (sourceTasks.length > MAX_DELEGATION_TASKS) {
664
- return { error: `${mode} delegation exceeds max tasks (${MAX_DELEGATION_TASKS})` };
826
+ return {
827
+ error: `${mode} delegation exceeds max tasks (${MAX_DELEGATION_TASKS})`,
828
+ };
665
829
  }
666
830
  const tasks = [];
667
831
  for (let i = 0; i < sourceTasks.length; i++) {
@@ -700,7 +864,9 @@ async function runDelegationTaskWithRetry(input) {
700
864
  const { parentSessionId, childDepth, channelId, chatbotId, enableRag, agentId, mode, task, } = input;
701
865
  const allowedTools = resolveSubagentAllowedTools(childDepth);
702
866
  const canDelegate = allowedTools.includes('delegate');
703
- const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED ? PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS : 1;
867
+ const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
868
+ ? PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS
869
+ : 1;
704
870
  let attempt = 0;
705
871
  let delayMs = PROACTIVE_AUTO_RETRY_BASE_DELAY_MS;
706
872
  let lastError = 'Delegation failed with unknown error';
@@ -716,7 +882,14 @@ async function runDelegationTaskWithRetry(input) {
716
882
  const startedAt = Date.now();
717
883
  try {
718
884
  const output = await runAgent(sessionId, [
719
- { role: 'system', content: buildSubagentSystemPrompt({ depth: childDepth, canDelegate, mode }) },
885
+ {
886
+ role: 'system',
887
+ content: buildSubagentSystemPrompt({
888
+ depth: childDepth,
889
+ canDelegate,
890
+ mode,
891
+ }),
892
+ },
720
893
  { role: 'user', content: task.prompt },
721
894
  ], chatbotId, enableRag, task.model, agentId, channelId, undefined, allowedTools);
722
895
  const durationMs = Date.now() - startedAt;
@@ -742,7 +915,14 @@ async function runDelegationTaskWithRetry(input) {
742
915
  const shouldRetry = classification === 'transient' && attempt < maxAttempts;
743
916
  if (!shouldRetry)
744
917
  break;
745
- logger.warn({ parentSessionId, sessionId, attempt, maxAttempts, delayMs, errorText }, 'Delegation retry scheduled after transient error');
918
+ logger.warn({
919
+ parentSessionId,
920
+ sessionId,
921
+ attempt,
922
+ maxAttempts,
923
+ delayMs,
924
+ errorText,
925
+ }, 'Delegation retry scheduled after transient error');
746
926
  await sleep(delayMs);
747
927
  delayMs = Math.min(delayMs * 2, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS);
748
928
  }
@@ -756,7 +936,14 @@ async function runDelegationTaskWithRetry(input) {
756
936
  const shouldRetry = classification === 'transient' && attempt < maxAttempts;
757
937
  if (!shouldRetry)
758
938
  break;
759
- logger.warn({ parentSessionId, sessionId, attempt, maxAttempts, delayMs, errorText }, 'Delegation retry scheduled after transient exception');
939
+ logger.warn({
940
+ parentSessionId,
941
+ sessionId,
942
+ attempt,
943
+ maxAttempts,
944
+ delayMs,
945
+ errorText,
946
+ }, 'Delegation retry scheduled after transient exception');
760
947
  await sleep(delayMs);
761
948
  delayMs = Math.min(delayMs * 2, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS);
762
949
  }
@@ -776,8 +963,14 @@ function formatDelegationCompletion(params) {
776
963
  const { mode, label, entries, totalDurationMs } = params;
777
964
  const completedCount = entries.filter((entry) => entry.run.status === 'completed').length;
778
965
  const failedCount = entries.length - completedCount;
779
- const overallStatus = failedCount === 0 ? 'completed' : completedCount === 0 ? 'failed' : 'partial';
780
- const heading = label?.trim() ? `[Delegate: ${label.trim()}]` : `[Delegate ${mode}]`;
966
+ const overallStatus = failedCount === 0
967
+ ? 'completed'
968
+ : completedCount === 0
969
+ ? 'failed'
970
+ : 'partial';
971
+ const heading = label?.trim()
972
+ ? `[Delegate: ${label.trim()}]`
973
+ : `[Delegate ${mode}]`;
781
974
  const userLines = [
782
975
  `${heading} ${overallStatus} (${completedCount}/${entries.length} completed, ${formatDurationMs(totalDurationMs)}).`,
783
976
  ];
@@ -836,7 +1029,13 @@ function formatDelegationCompletion(params) {
836
1029
  }
837
1030
  async function publishDelegationCompletion(params) {
838
1031
  const { parentSessionId, channelId, agentId, forLLM, forUser, artifacts, onProactiveMessage, } = params;
839
- storeMessage(parentSessionId, 'assistant', null, 'assistant', forLLM);
1032
+ memoryService.storeMessage({
1033
+ sessionId: parentSessionId,
1034
+ userId: 'assistant',
1035
+ username: null,
1036
+ role: 'assistant',
1037
+ content: forLLM,
1038
+ });
840
1039
  appendSessionTranscript(agentId, {
841
1040
  sessionId: parentSessionId,
842
1041
  channelId,
@@ -849,7 +1048,11 @@ async function publishDelegationCompletion(params) {
849
1048
  await onProactiveMessage({ text: forUser, artifacts });
850
1049
  return;
851
1050
  }
852
- logger.info({ parentSessionId, message: forUser, artifactCount: artifacts?.length || 0 }, 'Delegation completion (no proactive channel callback)');
1051
+ logger.info({
1052
+ parentSessionId,
1053
+ message: forUser,
1054
+ artifactCount: artifacts?.length || 0,
1055
+ }, 'Delegation completion (no proactive channel callback)');
853
1056
  }
854
1057
  function enqueueDelegationFromSideEffect(params) {
855
1058
  const { plan, parentSessionId, channelId, chatbotId, enableRag, agentId, onProactiveMessage, parentDepth, } = params;
@@ -951,7 +1154,7 @@ function enqueueDelegationFromSideEffect(params) {
951
1154
  export async function handleGatewayMessage(req) {
952
1155
  const startedAt = Date.now();
953
1156
  const runId = makeAuditRunId('turn');
954
- const session = getOrCreateSession(req.sessionId, req.guildId, req.channelId);
1157
+ const session = memoryService.getOrCreateSession(req.sessionId, req.guildId, req.channelId);
955
1158
  const chatbotId = req.chatbotId ?? session.chatbot_id ?? HYBRIDAI_CHATBOT_ID;
956
1159
  const enableRag = req.enableRag ?? session.enable_rag === 1;
957
1160
  const model = req.model ?? session.model ?? HYBRIDAI_MODEL;
@@ -1027,10 +1230,20 @@ export async function handleGatewayMessage(req) {
1027
1230
  if (isVersionOnlyQuestion(req.content)) {
1028
1231
  const resultText = `HybridClaw v${APP_VERSION}`;
1029
1232
  recordSuccessfulTurn({
1030
- sessionId: req.sessionId, agentId, chatbotId, enableRag, model,
1031
- channelId: req.channelId, runId, turnIndex,
1032
- userId: req.userId, username: req.username,
1033
- userContent: req.content, resultText, toolCallCount: 0, startedAt,
1233
+ sessionId: req.sessionId,
1234
+ agentId,
1235
+ chatbotId,
1236
+ enableRag,
1237
+ model,
1238
+ channelId: req.channelId,
1239
+ runId,
1240
+ turnIndex,
1241
+ userId: req.userId,
1242
+ username: req.username,
1243
+ userContent: req.content,
1244
+ resultText,
1245
+ toolCallCount: 0,
1246
+ startedAt,
1034
1247
  });
1035
1248
  return {
1036
1249
  status: 'success',
@@ -1038,10 +1251,39 @@ export async function handleGatewayMessage(req) {
1038
1251
  toolsUsed: [],
1039
1252
  };
1040
1253
  }
1041
- const history = getConversationHistory(req.sessionId, MAX_HISTORY_MESSAGES);
1254
+ const history = memoryService.getConversationHistory(req.sessionId, MAX_HISTORY_MESSAGES);
1255
+ let canonicalContext = {
1256
+ summary: null,
1257
+ recent_messages: [],
1258
+ };
1259
+ if (req.userId.trim()) {
1260
+ try {
1261
+ canonicalContext = memoryService.getCanonicalContext({
1262
+ agentId,
1263
+ userId: req.userId,
1264
+ windowSize: 12,
1265
+ excludeSessionId: req.sessionId,
1266
+ });
1267
+ }
1268
+ catch (err) {
1269
+ logger.debug({ sessionId: req.sessionId, userId: req.userId, err }, 'Failed to load canonical session context');
1270
+ }
1271
+ }
1272
+ const canonicalPromptSummary = formatCanonicalContextPrompt({
1273
+ summary: canonicalContext.summary,
1274
+ recentMessages: canonicalContext.recent_messages,
1275
+ });
1276
+ const memoryContext = memoryService.buildPromptMemoryContext({
1277
+ session,
1278
+ query: req.content,
1279
+ });
1280
+ const mergedSessionSummary = [canonicalPromptSummary, memoryContext.promptSummary]
1281
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
1282
+ .join('\n\n')
1283
+ .trim() || null;
1042
1284
  const { messages, skills, historyStats } = buildConversationContext({
1043
1285
  agentId,
1044
- sessionSummary: session.session_summary,
1286
+ sessionSummary: mergedSessionSummary,
1045
1287
  history,
1046
1288
  runtimeInfo: {
1047
1289
  chatbotId,
@@ -1069,6 +1311,8 @@ export async function handleGatewayMessage(req) {
1069
1311
  perMessageTruncatedCount: historyStats.perMessageTruncatedCount,
1070
1312
  middleCompressionApplied: historyStats.middleCompressionApplied,
1071
1313
  historyEstimatedTokens: estimateTokenCountFromMessages(messages.slice(historyStart)),
1314
+ canonicalSummaryIncluded: Boolean(canonicalPromptSummary),
1315
+ canonicalRecentMessagesIncluded: canonicalContext.recent_messages.length,
1072
1316
  },
1073
1317
  });
1074
1318
  const mediaPolicy = resolveMediaToolPolicy(req.content, media);
@@ -1091,12 +1335,17 @@ export async function handleGatewayMessage(req) {
1091
1335
  try {
1092
1336
  const scheduledTasks = getTasksForSession(req.sessionId);
1093
1337
  const output = await runAgent(req.sessionId, messages, chatbotId, enableRag, model, agentId, req.channelId, scheduledTasks, undefined, mediaPolicy.blockedTools, req.onTextDelta, req.onToolProgress, req.abortSignal, media);
1338
+ const effectiveUserContent = typeof output.effectiveUserPrompt === 'string' &&
1339
+ output.effectiveUserPrompt.trim()
1340
+ ? output.effectiveUserPrompt.trim()
1341
+ : req.content;
1094
1342
  const toolExecutions = output.toolExecutions || [];
1095
1343
  emitToolExecutionAuditEvents({
1096
1344
  sessionId: req.sessionId,
1097
1345
  runId,
1098
1346
  toolExecutions,
1099
1347
  });
1348
+ const usagePayload = buildTokenUsageAuditPayload(messages, output.result, output.tokenUsage);
1100
1349
  recordAuditEvent({
1101
1350
  sessionId: req.sessionId,
1102
1351
  runId,
@@ -1106,25 +1355,44 @@ export async function handleGatewayMessage(req) {
1106
1355
  model,
1107
1356
  durationMs: Date.now() - startedAt,
1108
1357
  toolCallCount: toolExecutions.length,
1109
- ...buildTokenUsageAuditPayload(messages, output.result, output.tokenUsage),
1358
+ ...usagePayload,
1110
1359
  },
1111
1360
  });
1361
+ recordUsageEvent({
1362
+ sessionId: req.sessionId,
1363
+ agentId,
1364
+ model,
1365
+ inputTokens: firstNumber([usagePayload.promptTokens]) || 0,
1366
+ outputTokens: firstNumber([usagePayload.completionTokens]) || 0,
1367
+ totalTokens: firstNumber([usagePayload.totalTokens]) || 0,
1368
+ toolCalls: toolExecutions.length,
1369
+ costUsd: extractUsageCostUsd(output.tokenUsage),
1370
+ });
1112
1371
  const parentDepth = extractDelegationDepth(req.sessionId);
1113
1372
  let acceptedDelegations = 0;
1114
1373
  processSideEffects(output, req.sessionId, req.channelId, {
1115
1374
  onDelegation: (effect) => {
1116
1375
  const normalized = normalizeDelegationEffect(effect, model);
1117
1376
  if (!normalized.plan) {
1118
- logger.warn({ sessionId: req.sessionId, error: normalized.error || 'unknown', effect }, 'Delegation skipped — invalid payload');
1377
+ logger.warn({
1378
+ sessionId: req.sessionId,
1379
+ error: normalized.error || 'unknown',
1380
+ effect,
1381
+ }, 'Delegation skipped — invalid payload');
1119
1382
  return;
1120
1383
  }
1121
1384
  const childDepth = parentDepth + 1;
1122
1385
  if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
1123
- logger.info({ sessionId: req.sessionId, childDepth, maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH }, 'Delegation skipped — depth limit reached');
1386
+ logger.info({
1387
+ sessionId: req.sessionId,
1388
+ childDepth,
1389
+ maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH,
1390
+ }, 'Delegation skipped — depth limit reached');
1124
1391
  return;
1125
1392
  }
1126
1393
  const requestedRuns = normalized.plan.tasks.length;
1127
- if (acceptedDelegations + requestedRuns > PROACTIVE_DELEGATION_MAX_PER_TURN) {
1394
+ if (acceptedDelegations + requestedRuns >
1395
+ PROACTIVE_DELEGATION_MAX_PER_TURN) {
1128
1396
  logger.info({
1129
1397
  sessionId: req.sessionId,
1130
1398
  limit: PROACTIVE_DELEGATION_MAX_PER_TURN,
@@ -1193,10 +1461,20 @@ export async function handleGatewayMessage(req) {
1193
1461
  }
1194
1462
  const resultText = output.result || 'No response from agent.';
1195
1463
  recordSuccessfulTurn({
1196
- sessionId: req.sessionId, agentId, chatbotId, enableRag, model,
1197
- channelId: req.channelId, runId, turnIndex,
1198
- userId: req.userId, username: req.username,
1199
- userContent: req.content, resultText, toolCallCount: toolExecutions.length, startedAt,
1464
+ sessionId: req.sessionId,
1465
+ agentId,
1466
+ chatbotId,
1467
+ enableRag,
1468
+ model,
1469
+ channelId: req.channelId,
1470
+ runId,
1471
+ turnIndex,
1472
+ userId: req.userId,
1473
+ username: req.username,
1474
+ userContent: effectiveUserContent,
1475
+ resultText,
1476
+ toolCallCount: toolExecutions.length,
1477
+ startedAt,
1200
1478
  });
1201
1479
  return {
1202
1480
  status: 'success',
@@ -1205,6 +1483,7 @@ export async function handleGatewayMessage(req) {
1205
1483
  artifacts: output.artifacts,
1206
1484
  toolExecutions,
1207
1485
  tokenUsage: output.tokenUsage,
1486
+ effectiveUserPrompt: output.effectiveUserPrompt,
1208
1487
  };
1209
1488
  }
1210
1489
  catch (err) {
@@ -1253,8 +1532,8 @@ export async function handleGatewayMessage(req) {
1253
1532
  };
1254
1533
  }
1255
1534
  }
1256
- export async function runGatewayScheduledTask(origSessionId, channelId, prompt, taskId, onResult, onError) {
1257
- const session = getOrCreateSession(origSessionId, null, channelId);
1535
+ export async function runGatewayScheduledTask(origSessionId, channelId, prompt, taskId, onResult, onError, runKey) {
1536
+ const session = memoryService.getOrCreateSession(origSessionId, null, channelId);
1258
1537
  const chatbotId = session.chatbot_id || HYBRIDAI_CHATBOT_ID;
1259
1538
  if (!chatbotId)
1260
1539
  return;
@@ -1267,13 +1546,14 @@ export async function runGatewayScheduledTask(origSessionId, channelId, prompt,
1267
1546
  chatbotId,
1268
1547
  model,
1269
1548
  agentId,
1549
+ sessionKey: runKey,
1270
1550
  onResult,
1271
1551
  onError,
1272
1552
  });
1273
1553
  }
1274
1554
  export async function handleGatewayCommand(req) {
1275
1555
  const cmd = (req.args[0] || '').toLowerCase();
1276
- const session = getOrCreateSession(req.sessionId, req.guildId, req.channelId);
1556
+ const session = memoryService.getOrCreateSession(req.sessionId, req.guildId, req.channelId);
1277
1557
  switch (cmd) {
1278
1558
  case 'help': {
1279
1559
  const help = [
@@ -1284,11 +1564,19 @@ export async function handleGatewayCommand(req) {
1284
1564
  '`model set <name>` — Set model for this session',
1285
1565
  '`model info` — Show current model',
1286
1566
  '`rag [on|off]` — Toggle or set RAG mode',
1567
+ '`channel mode [off|mention|free]` — Set or inspect this Discord channel response mode',
1568
+ '`channel policy [open|allowlist|disabled]` — Set or inspect guild channel policy',
1287
1569
  '`ralph [on|off|set <n>|info]` — Configure Ralph loop (0 off, -1 unlimited)',
1288
1570
  '`clear` — Clear session history',
1289
1571
  '`/status` — Show runtime status (Discord slash command, private to caller)',
1572
+ '`/channel-mode <off|mention|free>` — Set this Discord channel response mode',
1573
+ '`/channel-policy <open|allowlist|disabled>` — Set Discord guild channel policy',
1290
1574
  '`sessions` — List active sessions',
1291
- '`schedule add "<cron>" <prompt>`Add scheduled task',
1575
+ '`usage [summary|daily|monthly|model [daily|monthly] [agentId]]`Usage/cost aggregates',
1576
+ '`export session [sessionId]` — Export session JSONL snapshot for debugging',
1577
+ '`schedule add "<cron>" <prompt>` — Add cron scheduled task',
1578
+ '`schedule add at "<ISO time>" <prompt>` — Add one-shot task',
1579
+ '`schedule add every <ms> <prompt>` — Add interval task',
1292
1580
  '`schedule list` — List scheduled tasks',
1293
1581
  '`schedule remove <id>` — Remove a task',
1294
1582
  '`schedule toggle <id>` — Enable/disable a task',
@@ -1302,7 +1590,9 @@ export async function handleGatewayCommand(req) {
1302
1590
  const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
1303
1591
  if (bots.length === 0)
1304
1592
  return plainCommand('No bots available.');
1305
- const list = bots.map((b) => `• ${b.name} (${b.id})${b.description ? ` — ${b.description}` : ''}`).join('\n');
1593
+ const list = bots
1594
+ .map((b) => `• ${b.name} (${b.id})${b.description ? ` — ${b.description}` : ''}`)
1595
+ .join('\n');
1306
1596
  return infoCommand('Available Bots', list);
1307
1597
  }
1308
1598
  catch (err) {
@@ -1316,7 +1606,8 @@ export async function handleGatewayCommand(req) {
1316
1606
  let resolvedBotId = requested;
1317
1607
  try {
1318
1608
  const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
1319
- const matched = bots.find((b) => b.id === requested || b.name.toLowerCase() === requested.toLowerCase());
1609
+ const matched = bots.find((b) => b.id === requested ||
1610
+ b.name.toLowerCase() === requested.toLowerCase());
1320
1611
  if (matched)
1321
1612
  resolvedBotId = matched.id;
1322
1613
  }
@@ -1355,7 +1646,8 @@ export async function handleGatewayCommand(req) {
1355
1646
  const modelName = req.args[2];
1356
1647
  if (!modelName)
1357
1648
  return badCommand('Usage', 'Usage: `model set <name>`');
1358
- if (HYBRIDAI_MODELS.length > 0 && !HYBRIDAI_MODELS.includes(modelName)) {
1649
+ if (HYBRIDAI_MODELS.length > 0 &&
1650
+ !HYBRIDAI_MODELS.includes(modelName)) {
1359
1651
  return badCommand('Unknown Model', `\`${modelName}\` is not in the available models list.`);
1360
1652
  }
1361
1653
  updateSessionModel(session.id, modelName);
@@ -1380,6 +1672,60 @@ export async function handleGatewayCommand(req) {
1380
1672
  }
1381
1673
  return badCommand('Usage', 'Usage: `rag [on|off]`');
1382
1674
  }
1675
+ case 'channel': {
1676
+ const sub = (req.args[1] || '').toLowerCase();
1677
+ if (sub === 'mode' || !sub) {
1678
+ const guildId = req.guildId;
1679
+ if (!guildId) {
1680
+ return badCommand('Guild Only', '`channel mode` is only available in Discord guild channels.');
1681
+ }
1682
+ const requestedMode = (req.args[sub ? 2 : 1] || '').toLowerCase();
1683
+ if (!requestedMode) {
1684
+ const currentMode = resolveGuildChannelMode(guildId, req.channelId);
1685
+ return infoCommand('Channel Mode', [
1686
+ `Current mode: \`${currentMode}\``,
1687
+ `Group policy: \`${DISCORD_GROUP_POLICY}\``,
1688
+ `Config path: \`discord.guilds.${guildId}.channels.${req.channelId}.mode\``,
1689
+ 'Usage: `channel mode off|mention|free`',
1690
+ ].join('\n'));
1691
+ }
1692
+ if (!DISCORD_CHANNEL_MODE_VALUES.has(requestedMode)) {
1693
+ return badCommand('Usage', 'Usage: `channel mode off|mention|free`');
1694
+ }
1695
+ const mode = requestedMode;
1696
+ updateRuntimeConfig((draft) => {
1697
+ const guild = draft.discord.guilds[guildId] ?? {
1698
+ defaultMode: 'mention',
1699
+ channels: {},
1700
+ };
1701
+ guild.channels[req.channelId] = { mode };
1702
+ draft.discord.guilds[guildId] = guild;
1703
+ });
1704
+ return plainCommand(`Set channel mode to \`${mode}\` for this channel. (Policy: \`${DISCORD_GROUP_POLICY}\`)`);
1705
+ }
1706
+ if (sub === 'policy') {
1707
+ const requestedPolicy = (req.args[2] || '').toLowerCase();
1708
+ if (!requestedPolicy) {
1709
+ return infoCommand('Channel Policy', [
1710
+ `Current policy: \`${DISCORD_GROUP_POLICY}\``,
1711
+ 'Policies:',
1712
+ '• `open` — all guild channels are active unless a per-channel mode overrides',
1713
+ '• `allowlist` — only channels listed under `discord.guilds.<guild>.channels` are active',
1714
+ '• `disabled` — all guild channels are disabled',
1715
+ 'Usage: `channel policy open|allowlist|disabled`',
1716
+ ].join('\n'));
1717
+ }
1718
+ if (!DISCORD_GROUP_POLICY_VALUES.has(requestedPolicy)) {
1719
+ return badCommand('Usage', 'Usage: `channel policy open|allowlist|disabled`');
1720
+ }
1721
+ const policy = requestedPolicy;
1722
+ updateRuntimeConfig((draft) => {
1723
+ draft.discord.groupPolicy = policy;
1724
+ });
1725
+ return plainCommand(`Discord group policy set to \`${policy}\`.`);
1726
+ }
1727
+ return badCommand('Usage', 'Usage: `channel mode [off|mention|free]` or `channel policy [open|allowlist|disabled]`');
1728
+ }
1383
1729
  case 'ralph': {
1384
1730
  const sub = (req.args[1] || '').toLowerCase();
1385
1731
  if (!sub || sub === 'info' || sub === 'status') {
@@ -1392,7 +1738,10 @@ export async function handleGatewayCommand(req) {
1392
1738
  }
1393
1739
  let nextValue = null;
1394
1740
  if (sub === 'on') {
1395
- nextValue = PROACTIVE_RALPH_MAX_ITERATIONS === 0 ? 3 : PROACTIVE_RALPH_MAX_ITERATIONS;
1741
+ nextValue =
1742
+ PROACTIVE_RALPH_MAX_ITERATIONS === 0
1743
+ ? 3
1744
+ : PROACTIVE_RALPH_MAX_ITERATIONS;
1396
1745
  }
1397
1746
  else if (sub === 'off') {
1398
1747
  nextValue = 0;
@@ -1431,7 +1780,7 @@ export async function handleGatewayCommand(req) {
1431
1780
  return plainCommand(`Ralph loop set to ${formatRalphIterations(normalized)}.${restartNote}`);
1432
1781
  }
1433
1782
  case 'clear': {
1434
- const deleted = clearSessionHistory(session.id);
1783
+ const deleted = memoryService.clearSessionHistory(session.id);
1435
1784
  return infoCommand('Session Cleared', `Deleted ${deleted} messages. Workspace files preserved.`);
1436
1785
  }
1437
1786
  case 'status': {
@@ -1443,7 +1792,7 @@ export async function handleGatewayCommand(req) {
1443
1792
  const queueLabel = `${delegationStatus.active} active / ${delegationStatus.queued} queued`;
1444
1793
  const proactiveQueued = getQueuedProactiveMessageCount();
1445
1794
  const cacheKnown = metrics.cacheReadTokens != null || metrics.cacheWriteTokens != null;
1446
- const contextLabel = (metrics.contextUsedTokens != null && metrics.contextBudgetTokens != null)
1795
+ const contextLabel = metrics.contextUsedTokens != null && metrics.contextBudgetTokens != null
1447
1796
  ? `${formatCompactNumber(metrics.contextUsedTokens)}/${formatCompactNumber(metrics.contextBudgetTokens)} (${formatPercent(metrics.contextUsagePercent)})`
1448
1797
  : metrics.contextUsedTokens != null
1449
1798
  ? `${formatCompactNumber(metrics.contextUsedTokens)} est`
@@ -1467,16 +1816,137 @@ export async function handleGatewayCommand(req) {
1467
1816
  const sessions = getAllSessions();
1468
1817
  if (sessions.length === 0)
1469
1818
  return plainCommand('No active sessions.');
1470
- const list = sessions.slice(0, 20).map((s) => `${s.id} — ${s.message_count} msgs, last active ${s.last_active}`).join('\n');
1819
+ const list = sessions
1820
+ .slice(0, 20)
1821
+ .map((s) => `${s.id} — ${s.message_count} msgs, last active ${s.last_active}`)
1822
+ .join('\n');
1471
1823
  return infoCommand('Sessions', list);
1472
1824
  }
1825
+ case 'usage': {
1826
+ const sub = (req.args[1] || 'summary').toLowerCase();
1827
+ if (sub === 'daily' || sub === 'monthly') {
1828
+ const rows = listUsageByAgent({ window: sub });
1829
+ if (rows.length === 0) {
1830
+ return plainCommand(`No usage events recorded for ${sub} window.`);
1831
+ }
1832
+ const lines = rows.slice(0, 20).map((row) => {
1833
+ return `${row.agent_id} — ${formatCompactNumber(row.total_tokens)} tokens (${formatCompactNumber(row.total_input_tokens)} in / ${formatCompactNumber(row.total_output_tokens)} out) · ${row.call_count} calls · ${formatUsd(row.total_cost_usd)}`;
1834
+ });
1835
+ return infoCommand(`Usage (${sub} · by agent)`, lines.join('\n'));
1836
+ }
1837
+ if (sub === 'model') {
1838
+ const maybeWindow = (req.args[2] || '').toLowerCase();
1839
+ const window = maybeWindow === 'daily' || maybeWindow === 'monthly'
1840
+ ? maybeWindow
1841
+ : 'monthly';
1842
+ const modelAgentId = maybeWindow === 'daily' || maybeWindow === 'monthly'
1843
+ ? (req.args[3] || '').trim()
1844
+ : (req.args[2] || '').trim();
1845
+ const rows = listUsageByModel({
1846
+ window,
1847
+ agentId: modelAgentId || undefined,
1848
+ });
1849
+ if (rows.length === 0) {
1850
+ return plainCommand('No usage events recorded for model breakdown.');
1851
+ }
1852
+ const lines = rows.slice(0, 20).map((row) => {
1853
+ return `${row.model} — ${formatCompactNumber(row.total_tokens)} tokens · ${row.call_count} calls · ${formatUsd(row.total_cost_usd)}`;
1854
+ });
1855
+ const scope = modelAgentId ? `agent ${modelAgentId}` : 'all agents';
1856
+ return infoCommand(`Usage (${window} · by model · ${scope})`, lines.join('\n'));
1857
+ }
1858
+ if (sub !== 'summary') {
1859
+ return badCommand('Usage', 'Usage: `usage [summary|daily|monthly|model [daily|monthly] [agentId]]`');
1860
+ }
1861
+ const currentAgentId = resolveSessionAgentId(session);
1862
+ const daily = getUsageTotals({
1863
+ agentId: currentAgentId || undefined,
1864
+ window: 'daily',
1865
+ });
1866
+ const monthly = getUsageTotals({
1867
+ agentId: currentAgentId || undefined,
1868
+ window: 'monthly',
1869
+ });
1870
+ const topModels = listUsageByModel({
1871
+ agentId: currentAgentId || undefined,
1872
+ window: 'monthly',
1873
+ }).slice(0, 5);
1874
+ const scopeLabel = currentAgentId || 'all agents';
1875
+ const lines = [
1876
+ `Scope: ${scopeLabel}`,
1877
+ `Today: ${formatCompactNumber(daily.total_tokens)} tokens · ${daily.call_count} calls · ${formatUsd(daily.total_cost_usd)}`,
1878
+ `Month: ${formatCompactNumber(monthly.total_tokens)} tokens · ${monthly.call_count} calls · ${formatUsd(monthly.total_cost_usd)}`,
1879
+ ];
1880
+ if (topModels.length > 0) {
1881
+ lines.push('Top models (monthly):');
1882
+ lines.push(...topModels.map((row) => `- ${row.model}: ${formatCompactNumber(row.total_tokens)} tokens · ${formatUsd(row.total_cost_usd)}`));
1883
+ }
1884
+ return infoCommand('Usage Summary', lines.join('\n'));
1885
+ }
1886
+ case 'export': {
1887
+ const sub = (req.args[1] || 'session').toLowerCase();
1888
+ if (sub !== 'session') {
1889
+ return badCommand('Usage', 'Usage: `export session [sessionId]`');
1890
+ }
1891
+ const targetSessionId = (req.args[2] || session.id || '').trim();
1892
+ if (!targetSessionId) {
1893
+ return badCommand('Usage', 'Usage: `export session [sessionId]`');
1894
+ }
1895
+ const targetSession = memoryService.getSessionById(targetSessionId);
1896
+ if (!targetSession) {
1897
+ return badCommand('Not Found', `Session \`${targetSessionId}\` was not found.`);
1898
+ }
1899
+ const exportAgentId = resolveSessionAgentId(targetSession) || resolveSessionAgentId(session);
1900
+ if (!exportAgentId) {
1901
+ return badCommand('Missing Agent', 'Cannot export session: no agent/chatbot is configured for the target session.');
1902
+ }
1903
+ const messages = memoryService.getRecentMessages(targetSessionId);
1904
+ const exported = exportSessionSnapshotJsonl({
1905
+ agentId: exportAgentId,
1906
+ sessionId: targetSessionId,
1907
+ channelId: targetSession.channel_id,
1908
+ summary: targetSession.session_summary,
1909
+ messages,
1910
+ reason: 'manual',
1911
+ });
1912
+ if (!exported) {
1913
+ return badCommand('Export Failed', 'Failed to write session export JSONL file. Check gateway logs for details.');
1914
+ }
1915
+ return infoCommand('Session Exported', [
1916
+ `File: ${exported.path}`,
1917
+ `Messages: ${messages.length}`,
1918
+ `Summary: ${targetSession.session_summary ? 'yes' : 'no'}`,
1919
+ ].join('\n'));
1920
+ }
1473
1921
  case 'schedule': {
1474
1922
  const sub = req.args[1]?.toLowerCase();
1475
1923
  if (sub === 'add') {
1476
1924
  const rest = req.args.slice(2).join(' ');
1925
+ const atMatch = rest.match(/^at\s+"([^"]+)"\s+(.+)$/i);
1926
+ if (atMatch) {
1927
+ const [, runAtRaw, prompt] = atMatch;
1928
+ const parsedDate = new Date(runAtRaw);
1929
+ if (Number.isNaN(parsedDate.getTime())) {
1930
+ return badCommand('Invalid Time', `\`${runAtRaw}\` is not a valid ISO timestamp.`);
1931
+ }
1932
+ const taskId = createTask(session.id, req.channelId, '', prompt, parsedDate.toISOString());
1933
+ rearmScheduler();
1934
+ return plainCommand(`Task #${taskId} created: one-shot at \`${parsedDate.toISOString()}\` — ${prompt}`);
1935
+ }
1936
+ const everyMatch = rest.match(/^every\s+(\d+)\s+(.+)$/i);
1937
+ if (everyMatch) {
1938
+ const [, everyRaw, prompt] = everyMatch;
1939
+ const everyMs = Number.parseInt(everyRaw, 10);
1940
+ if (!Number.isFinite(everyMs) || everyMs < 10_000) {
1941
+ return badCommand('Invalid Interval', 'Interval must be at least 10000ms.');
1942
+ }
1943
+ const taskId = createTask(session.id, req.channelId, '', prompt, undefined, everyMs);
1944
+ rearmScheduler();
1945
+ return plainCommand(`Task #${taskId} created: every \`${everyMs}ms\` — ${prompt}`);
1946
+ }
1477
1947
  const cronMatch = rest.match(/^"([^"]+)"\s+(.+)$/);
1478
1948
  if (!cronMatch) {
1479
- return badCommand('Usage', 'Usage: `schedule add "<cron>" <prompt>`');
1949
+ return badCommand('Usage', 'Usage: `schedule add "<cron>" <prompt>` or `schedule add at "<ISO time>" <prompt>` or `schedule add every <ms> <prompt>`');
1480
1950
  }
1481
1951
  const [, cronExpr, prompt] = cronMatch;
1482
1952
  try {
@@ -1487,13 +1957,28 @@ export async function handleGatewayCommand(req) {
1487
1957
  }
1488
1958
  const taskId = createTask(session.id, req.channelId, cronExpr, prompt);
1489
1959
  rearmScheduler();
1490
- return plainCommand(`Task #${taskId} created: \`${cronExpr}\` — ${prompt}`);
1960
+ return plainCommand(`Task #${taskId} created: cron \`${cronExpr}\` — ${prompt}`);
1491
1961
  }
1492
1962
  if (sub === 'list') {
1493
1963
  const tasks = getTasksForSession(session.id);
1494
1964
  if (tasks.length === 0)
1495
1965
  return plainCommand('No scheduled tasks.');
1496
- const list = tasks.map((task) => `#${task.id} ${task.enabled ? 'enabled' : 'disabled'} \`${task.cron_expr}\` — ${task.prompt.slice(0, 60)}`).join('\n');
1966
+ const list = tasks
1967
+ .map((task) => {
1968
+ const scheduleLabel = task.run_at
1969
+ ? `at ${task.run_at}`
1970
+ : task.every_ms
1971
+ ? `every ${task.every_ms}ms`
1972
+ : task.cron_expr
1973
+ ? `cron ${task.cron_expr}`
1974
+ : 'unspecified';
1975
+ const statusLabel = task.last_status || 'n/a';
1976
+ const errorSuffix = task.consecutive_errors > 0
1977
+ ? ` · errors ${task.consecutive_errors}`
1978
+ : '';
1979
+ return `#${task.id} ${task.enabled ? 'enabled' : 'disabled'} (${scheduleLabel}) [${statusLabel}${errorSuffix}] — ${task.prompt.slice(0, 60)}`;
1980
+ })
1981
+ .join('\n');
1497
1982
  return infoCommand('Scheduled Tasks', list);
1498
1983
  }
1499
1984
  if (sub === 'remove') {
@@ -1512,7 +1997,12 @@ export async function handleGatewayCommand(req) {
1512
1997
  const task = tasks.find((t) => t.id === taskId);
1513
1998
  if (!task)
1514
1999
  return badCommand('Not Found', `Task #${taskId} was not found in this session.`);
1515
- toggleTask(taskId, !Boolean(task.enabled));
2000
+ if (task.enabled) {
2001
+ pauseTask(taskId);
2002
+ }
2003
+ else {
2004
+ resumeTask(taskId);
2005
+ }
1516
2006
  rearmScheduler();
1517
2007
  return plainCommand(`Task #${taskId} ${task.enabled ? 'disabled' : 'enabled'}.`);
1518
2008
  }