@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.
- package/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/dist/gateway-service.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 {
|
|
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 {
|
|
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 = [
|
|
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()
|
|
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
|
|
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' ||
|
|
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
|
|
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([
|
|
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 =
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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 ||
|
|
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 ||
|
|
492
|
-
const promptTokens = apiUsageAvailable
|
|
493
|
-
|
|
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
|
|
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
|
|
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)
|
|
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 {
|
|
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
|
|
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
|
-
{
|
|
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({
|
|
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({
|
|
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
|
|
780
|
-
|
|
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(
|
|
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({
|
|
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,
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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:
|
|
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
|
-
...
|
|
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({
|
|
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({
|
|
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 >
|
|
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,
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
'`
|
|
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
|
|
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 ||
|
|
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 &&
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|