@hybridaione/hybridclaw 0.2.3 → 0.2.7
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 +95 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +57 -19
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +72 -7
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1405 -0
- package/container/src/browser-tools.ts +672 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +65 -16
- package/container/src/index.ts +463 -110
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +107 -18
- package/container/src/tools.ts +600 -225
- package/container/src/types.ts +49 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -4
- 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 +24 -4
- 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 +31 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +156 -6
- 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 +7 -4
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +755 -150
- 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 +19 -12
- 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 +77 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +113 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +59 -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 +624 -87
- 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 +303 -26
- 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 +62 -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/hybridai-models.d.ts +8 -0
- package/dist/hybridai-models.d.ts.map +1 -0
- package/dist/hybridai-models.js +94 -0
- package/dist/hybridai-models.js.map +1 -0
- 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 +11 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +104 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +503 -24
- 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 +43 -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 +150 -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 +72 -13
- package/package.json +8 -2
- package/src/agent.ts +31 -4
- 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 +65 -14
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +190 -8
- 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 +1161 -205
- package/src/channels/discord/stream.ts +85 -31
- 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 +174 -57
- package/src/config.ts +208 -36
- package/src/container-runner.ts +134 -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 +996 -220
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +487 -52
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +99 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/hybridai-models.ts +158 -0
- 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 +42 -17
- package/src/runtime-config.ts +1112 -122
- package/src/scheduled-task-runner.ts +77 -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 +199 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +335 -0
- package/tests/discord.basic.test.ts +213 -4
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/hybridai-client.test.ts +112 -0
- package/tests/hybridai-models.test.ts +46 -0
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/tests/token-usage.cache.test.ts +128 -0
- 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,28 @@
|
|
|
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';
|
|
12
|
+
import { resolveModelContextWindowFallback, } from './hybridai-models.js';
|
|
9
13
|
import { logger } from './logger.js';
|
|
14
|
+
import { memoryService } from './memory-service.js';
|
|
10
15
|
import { getObservabilityIngestState } from './observability-ingest.js';
|
|
11
|
-
import {
|
|
16
|
+
import { updateRuntimeConfig } from './runtime-config.js';
|
|
17
|
+
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
18
|
+
import { getSchedulerStatus, rearmScheduler } from './scheduler.js';
|
|
19
|
+
import { exportSessionSnapshotJsonl } from './session-export.js';
|
|
12
20
|
import { maybeCompactSession } from './session-maintenance.js';
|
|
13
21
|
import { appendSessionTranscript } from './session-transcripts.js';
|
|
14
22
|
import { processSideEffects } from './side-effects.js';
|
|
15
23
|
import { expandSkillInvocation } from './skills.js';
|
|
16
|
-
import {
|
|
24
|
+
import { estimateTokenCountFromMessages, estimateTokenCountFromText, } from './token-efficiency.js';
|
|
17
25
|
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
26
|
const BOT_CACHE_TTL = 300_000; // 5 minutes
|
|
24
27
|
const MAX_HISTORY_MESSAGES = 40;
|
|
25
28
|
const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
@@ -37,6 +40,7 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
|
37
40
|
'browser_snapshot',
|
|
38
41
|
'browser_click',
|
|
39
42
|
'browser_type',
|
|
43
|
+
'browser_upload',
|
|
40
44
|
'browser_press',
|
|
41
45
|
'browser_scroll',
|
|
42
46
|
'browser_back',
|
|
@@ -50,10 +54,15 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
|
50
54
|
'browser_network',
|
|
51
55
|
'browser_close',
|
|
52
56
|
];
|
|
53
|
-
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
57
|
+
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
58
|
+
...BASE_SUBAGENT_ALLOWED_TOOLS,
|
|
59
|
+
'delegate',
|
|
60
|
+
];
|
|
54
61
|
const MAX_DELEGATION_TASKS = 6;
|
|
55
62
|
const MAX_DELEGATION_USER_CHARS = 500;
|
|
56
63
|
const MAX_RALPH_ITERATIONS = 64;
|
|
64
|
+
const DISCORD_CHANNEL_MODE_VALUES = new Set(['off', 'mention', 'free']);
|
|
65
|
+
const DISCORD_GROUP_POLICY_VALUES = new Set(['open', 'allowlist', 'disabled']);
|
|
57
66
|
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
67
|
const BROWSER_TAB_RE = /(browser|tab|current tab|web page|website|seite im browser|aktuellen tab)/i;
|
|
59
68
|
const TRANSIENT_DELEGATION_ERROR_PATTERNS = [
|
|
@@ -106,7 +115,9 @@ function normalizeMediaContextItems(raw) {
|
|
|
106
115
|
for (const item of raw) {
|
|
107
116
|
if (!item || typeof item !== 'object')
|
|
108
117
|
continue;
|
|
109
|
-
const path = typeof item.path === 'string' && item.path.trim()
|
|
118
|
+
const path = typeof item.path === 'string' && item.path.trim()
|
|
119
|
+
? item.path.trim()
|
|
120
|
+
: null;
|
|
110
121
|
const url = typeof item.url === 'string' ? item.url.trim() : '';
|
|
111
122
|
const originalUrl = typeof item.originalUrl === 'string' ? item.originalUrl.trim() : '';
|
|
112
123
|
const filename = typeof item.filename === 'string' ? item.filename.trim() : '';
|
|
@@ -132,7 +143,9 @@ function normalizeMediaContextItems(raw) {
|
|
|
132
143
|
function buildMediaPromptContext(media) {
|
|
133
144
|
if (media.length === 0)
|
|
134
145
|
return '';
|
|
135
|
-
const mediaPaths = media
|
|
146
|
+
const mediaPaths = media
|
|
147
|
+
.map((item) => item.path)
|
|
148
|
+
.filter((path) => Boolean(path));
|
|
136
149
|
const mediaUrls = media.map((item) => item.url);
|
|
137
150
|
const mediaTypes = media.map((item) => item.mimeType || 'unknown');
|
|
138
151
|
const payload = media.map((item, index) => ({
|
|
@@ -237,7 +250,9 @@ function formatRelativeTime(raw) {
|
|
|
237
250
|
return `${Math.max(1, Math.floor(deltaMs / 86_400_000))}d ago`;
|
|
238
251
|
}
|
|
239
252
|
function numberFromUnknown(value) {
|
|
240
|
-
if (typeof value !== 'number' ||
|
|
253
|
+
if (typeof value !== 'number' ||
|
|
254
|
+
Number.isNaN(value) ||
|
|
255
|
+
!Number.isFinite(value))
|
|
241
256
|
return null;
|
|
242
257
|
return value;
|
|
243
258
|
}
|
|
@@ -265,7 +280,9 @@ function formatCompactNumber(value) {
|
|
|
265
280
|
return 'n/a';
|
|
266
281
|
const abs = Math.abs(value);
|
|
267
282
|
if (abs >= 1_000_000) {
|
|
268
|
-
const scaled = abs >= 10_000_000
|
|
283
|
+
const scaled = abs >= 10_000_000
|
|
284
|
+
? (value / 1_000_000).toFixed(0)
|
|
285
|
+
: (value / 1_000_000).toFixed(1);
|
|
269
286
|
return `${scaled.replace(/\.0$/, '')}M`;
|
|
270
287
|
}
|
|
271
288
|
if (abs >= 1_000) {
|
|
@@ -279,17 +296,111 @@ function formatPercent(value) {
|
|
|
279
296
|
return 'n/a';
|
|
280
297
|
return `${Math.max(0, Math.min(100, Math.round(value)))}%`;
|
|
281
298
|
}
|
|
299
|
+
function formatUsd(value) {
|
|
300
|
+
if (value == null || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
301
|
+
return 'n/a';
|
|
302
|
+
}
|
|
303
|
+
if (value <= 0)
|
|
304
|
+
return '$0.0000';
|
|
305
|
+
if (value >= 1)
|
|
306
|
+
return `$${value.toFixed(2)}`;
|
|
307
|
+
if (value >= 0.01)
|
|
308
|
+
return `$${value.toFixed(4)}`;
|
|
309
|
+
return `$${value.toFixed(6)}`;
|
|
310
|
+
}
|
|
311
|
+
function resolveSessionAgentId(session) {
|
|
312
|
+
const sessionAgent = session.chatbot_id?.trim();
|
|
313
|
+
if (sessionAgent)
|
|
314
|
+
return sessionAgent;
|
|
315
|
+
const defaultAgent = HYBRIDAI_CHATBOT_ID?.trim();
|
|
316
|
+
if (defaultAgent)
|
|
317
|
+
return defaultAgent;
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
function extractUsageCostUsd(tokenUsage) {
|
|
321
|
+
if (!tokenUsage)
|
|
322
|
+
return 0;
|
|
323
|
+
const costCarrier = tokenUsage;
|
|
324
|
+
const value = firstNumber([
|
|
325
|
+
costCarrier.costUsd,
|
|
326
|
+
costCarrier.costUSD,
|
|
327
|
+
costCarrier.cost_usd,
|
|
328
|
+
costCarrier.estimatedCostUsd,
|
|
329
|
+
costCarrier.estimated_cost_usd,
|
|
330
|
+
]);
|
|
331
|
+
if (value == null)
|
|
332
|
+
return 0;
|
|
333
|
+
return Math.max(0, value);
|
|
334
|
+
}
|
|
335
|
+
function formatCanonicalContextPrompt(params) {
|
|
336
|
+
const sections = [];
|
|
337
|
+
const summary = (params.summary || '').trim();
|
|
338
|
+
if (summary) {
|
|
339
|
+
sections.push(['### Canonical Session Summary', summary].join('\n'));
|
|
340
|
+
}
|
|
341
|
+
if (params.recentMessages.length > 0) {
|
|
342
|
+
const lines = params.recentMessages.slice(-6).map((entry) => {
|
|
343
|
+
const role = (entry.role || 'user').trim().toLowerCase();
|
|
344
|
+
const who = role === 'assistant' ? 'Assistant' : 'User';
|
|
345
|
+
const from = entry.channel_id && entry.channel_id.trim()
|
|
346
|
+
? `${entry.channel_id.trim()} (${entry.session_id})`
|
|
347
|
+
: entry.session_id;
|
|
348
|
+
const compact = entry.content.replace(/\s+/g, ' ').trim();
|
|
349
|
+
const short = compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
|
|
350
|
+
return `- ${who} [${from}]: ${short}`;
|
|
351
|
+
});
|
|
352
|
+
sections.push([
|
|
353
|
+
'### Cross-Channel Recall',
|
|
354
|
+
'Recent context from other sessions/channels for this user:',
|
|
355
|
+
...lines,
|
|
356
|
+
].join('\n'));
|
|
357
|
+
}
|
|
358
|
+
const merged = sections.join('\n\n').trim();
|
|
359
|
+
return merged || null;
|
|
360
|
+
}
|
|
282
361
|
function resolveActivationModeLabel() {
|
|
283
362
|
if (DISCORD_COMMANDS_ONLY)
|
|
284
363
|
return 'commands-only';
|
|
364
|
+
if (DISCORD_GROUP_POLICY === 'disabled')
|
|
365
|
+
return 'disabled';
|
|
366
|
+
if (DISCORD_GROUP_POLICY === 'allowlist')
|
|
367
|
+
return 'allowlist';
|
|
368
|
+
if (DISCORD_FREE_RESPONSE_CHANNELS.length > 0)
|
|
369
|
+
return `mention + ${DISCORD_FREE_RESPONSE_CHANNELS.length} free channel(s)`;
|
|
285
370
|
if (DISCORD_RESPOND_TO_ALL_MESSAGES)
|
|
286
371
|
return 'all messages';
|
|
287
372
|
return 'mention';
|
|
288
373
|
}
|
|
289
|
-
function
|
|
374
|
+
function resolveGuildChannelMode(guildId, channelId) {
|
|
375
|
+
if (!guildId)
|
|
376
|
+
return 'free';
|
|
377
|
+
if (DISCORD_GROUP_POLICY === 'disabled')
|
|
378
|
+
return 'off';
|
|
379
|
+
const guild = DISCORD_GUILDS[guildId];
|
|
380
|
+
const explicit = guild?.channels[channelId]?.mode;
|
|
381
|
+
if (DISCORD_GROUP_POLICY === 'allowlist') {
|
|
382
|
+
return explicit ?? 'off';
|
|
383
|
+
}
|
|
384
|
+
if (explicit === 'off' || explicit === 'mention' || explicit === 'free') {
|
|
385
|
+
return explicit;
|
|
386
|
+
}
|
|
387
|
+
if (DISCORD_FREE_RESPONSE_CHANNELS.includes(channelId))
|
|
388
|
+
return 'free';
|
|
389
|
+
if (guild) {
|
|
390
|
+
const defaultMode = guild.defaultMode;
|
|
391
|
+
if (defaultMode === 'off' ||
|
|
392
|
+
defaultMode === 'mention' ||
|
|
393
|
+
defaultMode === 'free') {
|
|
394
|
+
return defaultMode;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (DISCORD_RESPOND_TO_ALL_MESSAGES)
|
|
398
|
+
return 'free';
|
|
399
|
+
return 'mention';
|
|
400
|
+
}
|
|
401
|
+
function readSessionStatusSnapshot(sessionId, options) {
|
|
290
402
|
const entries = getRecentStructuredAuditForSession(sessionId, 160);
|
|
291
403
|
let usagePayload = null;
|
|
292
|
-
let contextPayload = null;
|
|
293
404
|
for (const entry of entries) {
|
|
294
405
|
const payload = parseAuditPayload(entry);
|
|
295
406
|
if (!payload)
|
|
@@ -298,10 +409,7 @@ function readSessionStatusSnapshot(sessionId) {
|
|
|
298
409
|
if (!usagePayload && payloadType === 'model.usage') {
|
|
299
410
|
usagePayload = payload;
|
|
300
411
|
}
|
|
301
|
-
|
|
302
|
-
contextPayload = payload;
|
|
303
|
-
}
|
|
304
|
-
if (usagePayload && contextPayload)
|
|
412
|
+
if (usagePayload)
|
|
305
413
|
break;
|
|
306
414
|
}
|
|
307
415
|
const promptTokens = firstNumber([
|
|
@@ -317,25 +425,58 @@ function readSessionStatusSnapshot(sessionId) {
|
|
|
317
425
|
const cacheReadTokens = firstNumber([
|
|
318
426
|
usagePayload?.cacheReadTokens,
|
|
319
427
|
usagePayload?.cacheReadInputTokens,
|
|
428
|
+
usagePayload?.apiCacheReadTokens,
|
|
429
|
+
usagePayload?.cacheRead,
|
|
430
|
+
usagePayload?.cache_read,
|
|
431
|
+
usagePayload?.cache_read_tokens,
|
|
432
|
+
usagePayload?.cache_read_input_tokens,
|
|
433
|
+
usagePayload?.cached_tokens,
|
|
434
|
+
usagePayload?.prompt_tokens_details
|
|
435
|
+
?.cached_tokens,
|
|
320
436
|
]);
|
|
321
437
|
const cacheWriteTokens = firstNumber([
|
|
322
438
|
usagePayload?.cacheWriteTokens,
|
|
323
439
|
usagePayload?.cacheWriteInputTokens,
|
|
440
|
+
usagePayload?.apiCacheWriteTokens,
|
|
441
|
+
usagePayload?.cacheWrite,
|
|
442
|
+
usagePayload?.cache_write,
|
|
443
|
+
usagePayload?.cache_write_tokens,
|
|
444
|
+
usagePayload?.cache_write_input_tokens,
|
|
445
|
+
usagePayload?.cache_creation_input_tokens,
|
|
324
446
|
]);
|
|
325
447
|
const cacheRead = Math.max(0, cacheReadTokens || 0);
|
|
326
448
|
const cacheWrite = Math.max(0, cacheWriteTokens || 0);
|
|
327
449
|
const cacheTotal = cacheRead + cacheWrite;
|
|
328
450
|
const cacheHitPercent = cacheTotal > 0 ? (cacheRead / cacheTotal) * 100 : null;
|
|
329
|
-
const contextUsedTokens = firstNumber([
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
451
|
+
const contextUsedTokens = firstNumber([
|
|
452
|
+
usagePayload?.contextTokens,
|
|
453
|
+
usagePayload?.context_tokens,
|
|
454
|
+
usagePayload?.tokensInContext,
|
|
455
|
+
usagePayload?.tokens_in_context,
|
|
456
|
+
usagePayload?.promptTokens,
|
|
457
|
+
usagePayload?.apiPromptTokens,
|
|
458
|
+
usagePayload?.estimatedPromptTokens,
|
|
459
|
+
]);
|
|
460
|
+
const contextBudgetTokens = firstNumber([
|
|
461
|
+
usagePayload?.contextWindowTokens,
|
|
462
|
+
usagePayload?.context_window_tokens,
|
|
463
|
+
usagePayload?.modelContextWindowTokens,
|
|
464
|
+
usagePayload?.model_context_window_tokens,
|
|
465
|
+
usagePayload?.modelContextWindow,
|
|
466
|
+
usagePayload?.model_context_window,
|
|
467
|
+
usagePayload?.maxContextTokens,
|
|
468
|
+
usagePayload?.max_context_tokens,
|
|
469
|
+
usagePayload?.contextWindow,
|
|
470
|
+
usagePayload?.context_window,
|
|
471
|
+
usagePayload?.contextLength,
|
|
472
|
+
usagePayload?.context_length,
|
|
473
|
+
usagePayload?.maxContextSize,
|
|
474
|
+
usagePayload?.max_context_size,
|
|
475
|
+
options?.modelContextWindowTokens,
|
|
476
|
+
]);
|
|
477
|
+
const contextUsagePercent = contextUsedTokens != null &&
|
|
478
|
+
contextBudgetTokens != null &&
|
|
479
|
+
contextBudgetTokens > 0
|
|
339
480
|
? (contextUsedTokens / contextBudgetTokens) * 100
|
|
340
481
|
: null;
|
|
341
482
|
return {
|
|
@@ -387,17 +528,53 @@ function isVersionOnlyQuestion(raw) {
|
|
|
387
528
|
if (detailedRuntimeTokens.some((token) => text.includes(token)))
|
|
388
529
|
return false;
|
|
389
530
|
const words = text.split(' ').filter(Boolean);
|
|
390
|
-
if (words.length > 8
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
531
|
+
if (words.length > 8 &&
|
|
532
|
+
!text.includes('welche version') &&
|
|
533
|
+
!text.includes('what version') &&
|
|
534
|
+
!text.includes('which version')) {
|
|
394
535
|
return false;
|
|
395
536
|
}
|
|
396
537
|
return true;
|
|
397
538
|
}
|
|
398
539
|
function recordSuccessfulTurn(opts) {
|
|
399
|
-
|
|
400
|
-
|
|
540
|
+
memoryService.storeTurn({
|
|
541
|
+
sessionId: opts.sessionId,
|
|
542
|
+
user: {
|
|
543
|
+
userId: opts.userId,
|
|
544
|
+
username: opts.username,
|
|
545
|
+
content: opts.userContent,
|
|
546
|
+
},
|
|
547
|
+
assistant: {
|
|
548
|
+
userId: 'assistant',
|
|
549
|
+
username: null,
|
|
550
|
+
content: opts.resultText,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
try {
|
|
554
|
+
if (opts.userId.trim()) {
|
|
555
|
+
memoryService.appendCanonicalMessages({
|
|
556
|
+
agentId: opts.agentId,
|
|
557
|
+
userId: opts.userId,
|
|
558
|
+
newMessages: [
|
|
559
|
+
{
|
|
560
|
+
role: 'user',
|
|
561
|
+
content: opts.userContent,
|
|
562
|
+
sessionId: opts.sessionId,
|
|
563
|
+
channelId: opts.channelId,
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
role: 'assistant',
|
|
567
|
+
content: opts.resultText,
|
|
568
|
+
sessionId: opts.sessionId,
|
|
569
|
+
channelId: opts.channelId,
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
logger.debug({ sessionId: opts.sessionId, userId: opts.userId, err }, 'Failed to append canonical session memory');
|
|
577
|
+
}
|
|
401
578
|
appendSessionTranscript(opts.agentId, {
|
|
402
579
|
sessionId: opts.sessionId,
|
|
403
580
|
channelId: opts.channelId,
|
|
@@ -484,13 +661,21 @@ function buildTokenUsageAuditPayload(messages, resultText, tokenUsage) {
|
|
|
484
661
|
const fallbackEstimatedCompletionTokens = estimateTokenCountFromText(resultText || '');
|
|
485
662
|
const estimatedPromptTokens = tokenUsage?.estimatedPromptTokens || fallbackEstimatedPromptTokens;
|
|
486
663
|
const estimatedCompletionTokens = tokenUsage?.estimatedCompletionTokens || fallbackEstimatedCompletionTokens;
|
|
487
|
-
const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens ||
|
|
664
|
+
const estimatedTotalTokens = tokenUsage?.estimatedTotalTokens ||
|
|
665
|
+
estimatedPromptTokens + estimatedCompletionTokens;
|
|
488
666
|
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
489
667
|
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
490
668
|
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
491
|
-
const apiTotalTokens = tokenUsage?.apiTotalTokens ||
|
|
492
|
-
const
|
|
493
|
-
const
|
|
669
|
+
const apiTotalTokens = tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
|
|
670
|
+
const apiCacheUsageAvailable = tokenUsage?.apiCacheUsageAvailable === true;
|
|
671
|
+
const apiCacheReadTokens = tokenUsage?.apiCacheReadTokens || 0;
|
|
672
|
+
const apiCacheWriteTokens = tokenUsage?.apiCacheWriteTokens || 0;
|
|
673
|
+
const promptTokens = apiUsageAvailable
|
|
674
|
+
? apiPromptTokens
|
|
675
|
+
: estimatedPromptTokens;
|
|
676
|
+
const completionTokens = apiUsageAvailable
|
|
677
|
+
? apiCompletionTokens
|
|
678
|
+
: estimatedCompletionTokens;
|
|
494
679
|
const totalTokens = apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens;
|
|
495
680
|
return {
|
|
496
681
|
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
@@ -506,6 +691,17 @@ function buildTokenUsageAuditPayload(messages, resultText, tokenUsage) {
|
|
|
506
691
|
apiPromptTokens,
|
|
507
692
|
apiCompletionTokens,
|
|
508
693
|
apiTotalTokens,
|
|
694
|
+
...(apiCacheUsageAvailable
|
|
695
|
+
? {
|
|
696
|
+
apiCacheUsageAvailable,
|
|
697
|
+
apiCacheReadTokens,
|
|
698
|
+
apiCacheWriteTokens,
|
|
699
|
+
cacheReadTokens: apiCacheReadTokens,
|
|
700
|
+
cacheReadInputTokens: apiCacheReadTokens,
|
|
701
|
+
cacheWriteTokens: apiCacheWriteTokens,
|
|
702
|
+
cacheWriteInputTokens: apiCacheWriteTokens,
|
|
703
|
+
}
|
|
704
|
+
: {}),
|
|
509
705
|
};
|
|
510
706
|
}
|
|
511
707
|
export function getGatewayStatus() {
|
|
@@ -520,10 +716,15 @@ export function getGatewayStatus() {
|
|
|
520
716
|
ragDefault: HYBRIDAI_ENABLE_RAG,
|
|
521
717
|
timestamp: new Date().toISOString(),
|
|
522
718
|
observability: getObservabilityIngestState(),
|
|
719
|
+
scheduler: {
|
|
720
|
+
jobs: getSchedulerStatus(),
|
|
721
|
+
},
|
|
523
722
|
};
|
|
524
723
|
}
|
|
525
724
|
export function getGatewayHistory(sessionId, limit = MAX_HISTORY_MESSAGES) {
|
|
526
|
-
return
|
|
725
|
+
return memoryService
|
|
726
|
+
.getConversationHistory(sessionId, Math.max(1, Math.min(limit, 200)))
|
|
727
|
+
.reverse();
|
|
527
728
|
}
|
|
528
729
|
function extractDelegationDepth(sessionId) {
|
|
529
730
|
const match = sessionId.match(/^delegate:d(\d+):/);
|
|
@@ -533,7 +734,9 @@ function extractDelegationDepth(sessionId) {
|
|
|
533
734
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
534
735
|
}
|
|
535
736
|
function nextDelegationSessionId(parentSessionId, nextDepth) {
|
|
536
|
-
const safeParent = parentSessionId
|
|
737
|
+
const safeParent = parentSessionId
|
|
738
|
+
.replace(/[^a-zA-Z0-9:_-]/g, '-')
|
|
739
|
+
.slice(0, 48);
|
|
537
740
|
const nonce = Math.random().toString(36).slice(2, 8);
|
|
538
741
|
return `delegate:d${nextDepth}:${safeParent}:${Date.now()}:${nonce}`;
|
|
539
742
|
}
|
|
@@ -602,7 +805,9 @@ function classifyDelegationError(errorText) {
|
|
|
602
805
|
return 'unknown';
|
|
603
806
|
}
|
|
604
807
|
function inferDelegationStatus(errorText) {
|
|
605
|
-
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
808
|
+
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
809
|
+
? 'timeout'
|
|
810
|
+
: 'failed';
|
|
606
811
|
}
|
|
607
812
|
function normalizeDelegationTask(raw, fallbackModel) {
|
|
608
813
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
@@ -661,7 +866,9 @@ function normalizeDelegationEffect(effect, fallbackModel) {
|
|
|
661
866
|
return { error: `${mode} delegation requires at least one task` };
|
|
662
867
|
}
|
|
663
868
|
if (sourceTasks.length > MAX_DELEGATION_TASKS) {
|
|
664
|
-
return {
|
|
869
|
+
return {
|
|
870
|
+
error: `${mode} delegation exceeds max tasks (${MAX_DELEGATION_TASKS})`,
|
|
871
|
+
};
|
|
665
872
|
}
|
|
666
873
|
const tasks = [];
|
|
667
874
|
for (let i = 0; i < sourceTasks.length; i++) {
|
|
@@ -700,7 +907,9 @@ async function runDelegationTaskWithRetry(input) {
|
|
|
700
907
|
const { parentSessionId, childDepth, channelId, chatbotId, enableRag, agentId, mode, task, } = input;
|
|
701
908
|
const allowedTools = resolveSubagentAllowedTools(childDepth);
|
|
702
909
|
const canDelegate = allowedTools.includes('delegate');
|
|
703
|
-
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
910
|
+
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
911
|
+
? PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS
|
|
912
|
+
: 1;
|
|
704
913
|
let attempt = 0;
|
|
705
914
|
let delayMs = PROACTIVE_AUTO_RETRY_BASE_DELAY_MS;
|
|
706
915
|
let lastError = 'Delegation failed with unknown error';
|
|
@@ -716,7 +925,14 @@ async function runDelegationTaskWithRetry(input) {
|
|
|
716
925
|
const startedAt = Date.now();
|
|
717
926
|
try {
|
|
718
927
|
const output = await runAgent(sessionId, [
|
|
719
|
-
{
|
|
928
|
+
{
|
|
929
|
+
role: 'system',
|
|
930
|
+
content: buildSubagentSystemPrompt({
|
|
931
|
+
depth: childDepth,
|
|
932
|
+
canDelegate,
|
|
933
|
+
mode,
|
|
934
|
+
}),
|
|
935
|
+
},
|
|
720
936
|
{ role: 'user', content: task.prompt },
|
|
721
937
|
], chatbotId, enableRag, task.model, agentId, channelId, undefined, allowedTools);
|
|
722
938
|
const durationMs = Date.now() - startedAt;
|
|
@@ -742,7 +958,14 @@ async function runDelegationTaskWithRetry(input) {
|
|
|
742
958
|
const shouldRetry = classification === 'transient' && attempt < maxAttempts;
|
|
743
959
|
if (!shouldRetry)
|
|
744
960
|
break;
|
|
745
|
-
logger.warn({
|
|
961
|
+
logger.warn({
|
|
962
|
+
parentSessionId,
|
|
963
|
+
sessionId,
|
|
964
|
+
attempt,
|
|
965
|
+
maxAttempts,
|
|
966
|
+
delayMs,
|
|
967
|
+
errorText,
|
|
968
|
+
}, 'Delegation retry scheduled after transient error');
|
|
746
969
|
await sleep(delayMs);
|
|
747
970
|
delayMs = Math.min(delayMs * 2, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS);
|
|
748
971
|
}
|
|
@@ -756,7 +979,14 @@ async function runDelegationTaskWithRetry(input) {
|
|
|
756
979
|
const shouldRetry = classification === 'transient' && attempt < maxAttempts;
|
|
757
980
|
if (!shouldRetry)
|
|
758
981
|
break;
|
|
759
|
-
logger.warn({
|
|
982
|
+
logger.warn({
|
|
983
|
+
parentSessionId,
|
|
984
|
+
sessionId,
|
|
985
|
+
attempt,
|
|
986
|
+
maxAttempts,
|
|
987
|
+
delayMs,
|
|
988
|
+
errorText,
|
|
989
|
+
}, 'Delegation retry scheduled after transient exception');
|
|
760
990
|
await sleep(delayMs);
|
|
761
991
|
delayMs = Math.min(delayMs * 2, PROACTIVE_AUTO_RETRY_MAX_DELAY_MS);
|
|
762
992
|
}
|
|
@@ -776,8 +1006,14 @@ function formatDelegationCompletion(params) {
|
|
|
776
1006
|
const { mode, label, entries, totalDurationMs } = params;
|
|
777
1007
|
const completedCount = entries.filter((entry) => entry.run.status === 'completed').length;
|
|
778
1008
|
const failedCount = entries.length - completedCount;
|
|
779
|
-
const overallStatus = failedCount === 0
|
|
780
|
-
|
|
1009
|
+
const overallStatus = failedCount === 0
|
|
1010
|
+
? 'completed'
|
|
1011
|
+
: completedCount === 0
|
|
1012
|
+
? 'failed'
|
|
1013
|
+
: 'partial';
|
|
1014
|
+
const heading = label?.trim()
|
|
1015
|
+
? `[Delegate: ${label.trim()}]`
|
|
1016
|
+
: `[Delegate ${mode}]`;
|
|
781
1017
|
const userLines = [
|
|
782
1018
|
`${heading} ${overallStatus} (${completedCount}/${entries.length} completed, ${formatDurationMs(totalDurationMs)}).`,
|
|
783
1019
|
];
|
|
@@ -836,7 +1072,13 @@ function formatDelegationCompletion(params) {
|
|
|
836
1072
|
}
|
|
837
1073
|
async function publishDelegationCompletion(params) {
|
|
838
1074
|
const { parentSessionId, channelId, agentId, forLLM, forUser, artifacts, onProactiveMessage, } = params;
|
|
839
|
-
storeMessage(
|
|
1075
|
+
memoryService.storeMessage({
|
|
1076
|
+
sessionId: parentSessionId,
|
|
1077
|
+
userId: 'assistant',
|
|
1078
|
+
username: null,
|
|
1079
|
+
role: 'assistant',
|
|
1080
|
+
content: forLLM,
|
|
1081
|
+
});
|
|
840
1082
|
appendSessionTranscript(agentId, {
|
|
841
1083
|
sessionId: parentSessionId,
|
|
842
1084
|
channelId,
|
|
@@ -849,7 +1091,11 @@ async function publishDelegationCompletion(params) {
|
|
|
849
1091
|
await onProactiveMessage({ text: forUser, artifacts });
|
|
850
1092
|
return;
|
|
851
1093
|
}
|
|
852
|
-
logger.info({
|
|
1094
|
+
logger.info({
|
|
1095
|
+
parentSessionId,
|
|
1096
|
+
message: forUser,
|
|
1097
|
+
artifactCount: artifacts?.length || 0,
|
|
1098
|
+
}, 'Delegation completion (no proactive channel callback)');
|
|
853
1099
|
}
|
|
854
1100
|
function enqueueDelegationFromSideEffect(params) {
|
|
855
1101
|
const { plan, parentSessionId, channelId, chatbotId, enableRag, agentId, onProactiveMessage, parentDepth, } = params;
|
|
@@ -951,7 +1197,7 @@ function enqueueDelegationFromSideEffect(params) {
|
|
|
951
1197
|
export async function handleGatewayMessage(req) {
|
|
952
1198
|
const startedAt = Date.now();
|
|
953
1199
|
const runId = makeAuditRunId('turn');
|
|
954
|
-
const session = getOrCreateSession(req.sessionId, req.guildId, req.channelId);
|
|
1200
|
+
const session = memoryService.getOrCreateSession(req.sessionId, req.guildId, req.channelId);
|
|
955
1201
|
const chatbotId = req.chatbotId ?? session.chatbot_id ?? HYBRIDAI_CHATBOT_ID;
|
|
956
1202
|
const enableRag = req.enableRag ?? session.enable_rag === 1;
|
|
957
1203
|
const model = req.model ?? session.model ?? HYBRIDAI_MODEL;
|
|
@@ -1027,10 +1273,20 @@ export async function handleGatewayMessage(req) {
|
|
|
1027
1273
|
if (isVersionOnlyQuestion(req.content)) {
|
|
1028
1274
|
const resultText = `HybridClaw v${APP_VERSION}`;
|
|
1029
1275
|
recordSuccessfulTurn({
|
|
1030
|
-
sessionId: req.sessionId,
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1276
|
+
sessionId: req.sessionId,
|
|
1277
|
+
agentId,
|
|
1278
|
+
chatbotId,
|
|
1279
|
+
enableRag,
|
|
1280
|
+
model,
|
|
1281
|
+
channelId: req.channelId,
|
|
1282
|
+
runId,
|
|
1283
|
+
turnIndex,
|
|
1284
|
+
userId: req.userId,
|
|
1285
|
+
username: req.username,
|
|
1286
|
+
userContent: req.content,
|
|
1287
|
+
resultText,
|
|
1288
|
+
toolCallCount: 0,
|
|
1289
|
+
startedAt,
|
|
1034
1290
|
});
|
|
1035
1291
|
return {
|
|
1036
1292
|
status: 'success',
|
|
@@ -1038,10 +1294,39 @@ export async function handleGatewayMessage(req) {
|
|
|
1038
1294
|
toolsUsed: [],
|
|
1039
1295
|
};
|
|
1040
1296
|
}
|
|
1041
|
-
const history = getConversationHistory(req.sessionId, MAX_HISTORY_MESSAGES);
|
|
1297
|
+
const history = memoryService.getConversationHistory(req.sessionId, MAX_HISTORY_MESSAGES);
|
|
1298
|
+
let canonicalContext = {
|
|
1299
|
+
summary: null,
|
|
1300
|
+
recent_messages: [],
|
|
1301
|
+
};
|
|
1302
|
+
if (req.userId.trim()) {
|
|
1303
|
+
try {
|
|
1304
|
+
canonicalContext = memoryService.getCanonicalContext({
|
|
1305
|
+
agentId,
|
|
1306
|
+
userId: req.userId,
|
|
1307
|
+
windowSize: 12,
|
|
1308
|
+
excludeSessionId: req.sessionId,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
catch (err) {
|
|
1312
|
+
logger.debug({ sessionId: req.sessionId, userId: req.userId, err }, 'Failed to load canonical session context');
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const canonicalPromptSummary = formatCanonicalContextPrompt({
|
|
1316
|
+
summary: canonicalContext.summary,
|
|
1317
|
+
recentMessages: canonicalContext.recent_messages,
|
|
1318
|
+
});
|
|
1319
|
+
const memoryContext = memoryService.buildPromptMemoryContext({
|
|
1320
|
+
session,
|
|
1321
|
+
query: req.content,
|
|
1322
|
+
});
|
|
1323
|
+
const mergedSessionSummary = [canonicalPromptSummary, memoryContext.promptSummary]
|
|
1324
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
1325
|
+
.join('\n\n')
|
|
1326
|
+
.trim() || null;
|
|
1042
1327
|
const { messages, skills, historyStats } = buildConversationContext({
|
|
1043
1328
|
agentId,
|
|
1044
|
-
sessionSummary:
|
|
1329
|
+
sessionSummary: mergedSessionSummary,
|
|
1045
1330
|
history,
|
|
1046
1331
|
runtimeInfo: {
|
|
1047
1332
|
chatbotId,
|
|
@@ -1069,6 +1354,8 @@ export async function handleGatewayMessage(req) {
|
|
|
1069
1354
|
perMessageTruncatedCount: historyStats.perMessageTruncatedCount,
|
|
1070
1355
|
middleCompressionApplied: historyStats.middleCompressionApplied,
|
|
1071
1356
|
historyEstimatedTokens: estimateTokenCountFromMessages(messages.slice(historyStart)),
|
|
1357
|
+
canonicalSummaryIncluded: Boolean(canonicalPromptSummary),
|
|
1358
|
+
canonicalRecentMessagesIncluded: canonicalContext.recent_messages.length,
|
|
1072
1359
|
},
|
|
1073
1360
|
});
|
|
1074
1361
|
const mediaPolicy = resolveMediaToolPolicy(req.content, media);
|
|
@@ -1091,12 +1378,17 @@ export async function handleGatewayMessage(req) {
|
|
|
1091
1378
|
try {
|
|
1092
1379
|
const scheduledTasks = getTasksForSession(req.sessionId);
|
|
1093
1380
|
const output = await runAgent(req.sessionId, messages, chatbotId, enableRag, model, agentId, req.channelId, scheduledTasks, undefined, mediaPolicy.blockedTools, req.onTextDelta, req.onToolProgress, req.abortSignal, media);
|
|
1381
|
+
const effectiveUserContent = typeof output.effectiveUserPrompt === 'string' &&
|
|
1382
|
+
output.effectiveUserPrompt.trim()
|
|
1383
|
+
? output.effectiveUserPrompt.trim()
|
|
1384
|
+
: req.content;
|
|
1094
1385
|
const toolExecutions = output.toolExecutions || [];
|
|
1095
1386
|
emitToolExecutionAuditEvents({
|
|
1096
1387
|
sessionId: req.sessionId,
|
|
1097
1388
|
runId,
|
|
1098
1389
|
toolExecutions,
|
|
1099
1390
|
});
|
|
1391
|
+
const usagePayload = buildTokenUsageAuditPayload(messages, output.result, output.tokenUsage);
|
|
1100
1392
|
recordAuditEvent({
|
|
1101
1393
|
sessionId: req.sessionId,
|
|
1102
1394
|
runId,
|
|
@@ -1106,25 +1398,44 @@ export async function handleGatewayMessage(req) {
|
|
|
1106
1398
|
model,
|
|
1107
1399
|
durationMs: Date.now() - startedAt,
|
|
1108
1400
|
toolCallCount: toolExecutions.length,
|
|
1109
|
-
...
|
|
1401
|
+
...usagePayload,
|
|
1110
1402
|
},
|
|
1111
1403
|
});
|
|
1404
|
+
recordUsageEvent({
|
|
1405
|
+
sessionId: req.sessionId,
|
|
1406
|
+
agentId,
|
|
1407
|
+
model,
|
|
1408
|
+
inputTokens: firstNumber([usagePayload.promptTokens]) || 0,
|
|
1409
|
+
outputTokens: firstNumber([usagePayload.completionTokens]) || 0,
|
|
1410
|
+
totalTokens: firstNumber([usagePayload.totalTokens]) || 0,
|
|
1411
|
+
toolCalls: toolExecutions.length,
|
|
1412
|
+
costUsd: extractUsageCostUsd(output.tokenUsage),
|
|
1413
|
+
});
|
|
1112
1414
|
const parentDepth = extractDelegationDepth(req.sessionId);
|
|
1113
1415
|
let acceptedDelegations = 0;
|
|
1114
1416
|
processSideEffects(output, req.sessionId, req.channelId, {
|
|
1115
1417
|
onDelegation: (effect) => {
|
|
1116
1418
|
const normalized = normalizeDelegationEffect(effect, model);
|
|
1117
1419
|
if (!normalized.plan) {
|
|
1118
|
-
logger.warn({
|
|
1420
|
+
logger.warn({
|
|
1421
|
+
sessionId: req.sessionId,
|
|
1422
|
+
error: normalized.error || 'unknown',
|
|
1423
|
+
effect,
|
|
1424
|
+
}, 'Delegation skipped — invalid payload');
|
|
1119
1425
|
return;
|
|
1120
1426
|
}
|
|
1121
1427
|
const childDepth = parentDepth + 1;
|
|
1122
1428
|
if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
|
|
1123
|
-
logger.info({
|
|
1429
|
+
logger.info({
|
|
1430
|
+
sessionId: req.sessionId,
|
|
1431
|
+
childDepth,
|
|
1432
|
+
maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH,
|
|
1433
|
+
}, 'Delegation skipped — depth limit reached');
|
|
1124
1434
|
return;
|
|
1125
1435
|
}
|
|
1126
1436
|
const requestedRuns = normalized.plan.tasks.length;
|
|
1127
|
-
if (acceptedDelegations + requestedRuns >
|
|
1437
|
+
if (acceptedDelegations + requestedRuns >
|
|
1438
|
+
PROACTIVE_DELEGATION_MAX_PER_TURN) {
|
|
1128
1439
|
logger.info({
|
|
1129
1440
|
sessionId: req.sessionId,
|
|
1130
1441
|
limit: PROACTIVE_DELEGATION_MAX_PER_TURN,
|
|
@@ -1193,10 +1504,20 @@ export async function handleGatewayMessage(req) {
|
|
|
1193
1504
|
}
|
|
1194
1505
|
const resultText = output.result || 'No response from agent.';
|
|
1195
1506
|
recordSuccessfulTurn({
|
|
1196
|
-
sessionId: req.sessionId,
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1507
|
+
sessionId: req.sessionId,
|
|
1508
|
+
agentId,
|
|
1509
|
+
chatbotId,
|
|
1510
|
+
enableRag,
|
|
1511
|
+
model,
|
|
1512
|
+
channelId: req.channelId,
|
|
1513
|
+
runId,
|
|
1514
|
+
turnIndex,
|
|
1515
|
+
userId: req.userId,
|
|
1516
|
+
username: req.username,
|
|
1517
|
+
userContent: effectiveUserContent,
|
|
1518
|
+
resultText,
|
|
1519
|
+
toolCallCount: toolExecutions.length,
|
|
1520
|
+
startedAt,
|
|
1200
1521
|
});
|
|
1201
1522
|
return {
|
|
1202
1523
|
status: 'success',
|
|
@@ -1205,6 +1526,7 @@ export async function handleGatewayMessage(req) {
|
|
|
1205
1526
|
artifacts: output.artifacts,
|
|
1206
1527
|
toolExecutions,
|
|
1207
1528
|
tokenUsage: output.tokenUsage,
|
|
1529
|
+
effectiveUserPrompt: output.effectiveUserPrompt,
|
|
1208
1530
|
};
|
|
1209
1531
|
}
|
|
1210
1532
|
catch (err) {
|
|
@@ -1253,8 +1575,8 @@ export async function handleGatewayMessage(req) {
|
|
|
1253
1575
|
};
|
|
1254
1576
|
}
|
|
1255
1577
|
}
|
|
1256
|
-
export async function runGatewayScheduledTask(origSessionId, channelId, prompt, taskId, onResult, onError) {
|
|
1257
|
-
const session = getOrCreateSession(origSessionId, null, channelId);
|
|
1578
|
+
export async function runGatewayScheduledTask(origSessionId, channelId, prompt, taskId, onResult, onError, runKey) {
|
|
1579
|
+
const session = memoryService.getOrCreateSession(origSessionId, null, channelId);
|
|
1258
1580
|
const chatbotId = session.chatbot_id || HYBRIDAI_CHATBOT_ID;
|
|
1259
1581
|
if (!chatbotId)
|
|
1260
1582
|
return;
|
|
@@ -1267,13 +1589,14 @@ export async function runGatewayScheduledTask(origSessionId, channelId, prompt,
|
|
|
1267
1589
|
chatbotId,
|
|
1268
1590
|
model,
|
|
1269
1591
|
agentId,
|
|
1592
|
+
sessionKey: runKey,
|
|
1270
1593
|
onResult,
|
|
1271
1594
|
onError,
|
|
1272
1595
|
});
|
|
1273
1596
|
}
|
|
1274
1597
|
export async function handleGatewayCommand(req) {
|
|
1275
1598
|
const cmd = (req.args[0] || '').toLowerCase();
|
|
1276
|
-
const session = getOrCreateSession(req.sessionId, req.guildId, req.channelId);
|
|
1599
|
+
const session = memoryService.getOrCreateSession(req.sessionId, req.guildId, req.channelId);
|
|
1277
1600
|
switch (cmd) {
|
|
1278
1601
|
case 'help': {
|
|
1279
1602
|
const help = [
|
|
@@ -1284,11 +1607,20 @@ export async function handleGatewayCommand(req) {
|
|
|
1284
1607
|
'`model set <name>` — Set model for this session',
|
|
1285
1608
|
'`model info` — Show current model',
|
|
1286
1609
|
'`rag [on|off]` — Toggle or set RAG mode',
|
|
1610
|
+
'`channel mode [off|mention|free]` — Set or inspect this Discord channel response mode',
|
|
1611
|
+
'`channel policy [open|allowlist|disabled]` — Set or inspect guild channel policy',
|
|
1287
1612
|
'`ralph [on|off|set <n>|info]` — Configure Ralph loop (0 off, -1 unlimited)',
|
|
1288
1613
|
'`clear` — Clear session history',
|
|
1289
1614
|
'`/status` — Show runtime status (Discord slash command, private to caller)',
|
|
1615
|
+
'`/approve [view|yes|session|agent|no] [approval_id]` — View/respond to pending approvals privately',
|
|
1616
|
+
'`/channel-mode <off|mention|free>` — Set this Discord channel response mode',
|
|
1617
|
+
'`/channel-policy <open|allowlist|disabled>` — Set Discord guild channel policy',
|
|
1290
1618
|
'`sessions` — List active sessions',
|
|
1291
|
-
'`
|
|
1619
|
+
'`usage [summary|daily|monthly|model [daily|monthly] [agentId]]` — Usage/cost aggregates',
|
|
1620
|
+
'`export session [sessionId]` — Export session JSONL snapshot for debugging',
|
|
1621
|
+
'`schedule add "<cron>" <prompt>` — Add cron scheduled task',
|
|
1622
|
+
'`schedule add at "<ISO time>" <prompt>` — Add one-shot task',
|
|
1623
|
+
'`schedule add every <ms> <prompt>` — Add interval task',
|
|
1292
1624
|
'`schedule list` — List scheduled tasks',
|
|
1293
1625
|
'`schedule remove <id>` — Remove a task',
|
|
1294
1626
|
'`schedule toggle <id>` — Enable/disable a task',
|
|
@@ -1302,7 +1634,9 @@ export async function handleGatewayCommand(req) {
|
|
|
1302
1634
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1303
1635
|
if (bots.length === 0)
|
|
1304
1636
|
return plainCommand('No bots available.');
|
|
1305
|
-
const list = bots
|
|
1637
|
+
const list = bots
|
|
1638
|
+
.map((b) => `• ${b.name} (${b.id})${b.description ? ` — ${b.description}` : ''}`)
|
|
1639
|
+
.join('\n');
|
|
1306
1640
|
return infoCommand('Available Bots', list);
|
|
1307
1641
|
}
|
|
1308
1642
|
catch (err) {
|
|
@@ -1316,7 +1650,8 @@ export async function handleGatewayCommand(req) {
|
|
|
1316
1650
|
let resolvedBotId = requested;
|
|
1317
1651
|
try {
|
|
1318
1652
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1319
|
-
const matched = bots.find((b) => b.id === requested ||
|
|
1653
|
+
const matched = bots.find((b) => b.id === requested ||
|
|
1654
|
+
b.name.toLowerCase() === requested.toLowerCase());
|
|
1320
1655
|
if (matched)
|
|
1321
1656
|
resolvedBotId = matched.id;
|
|
1322
1657
|
}
|
|
@@ -1355,7 +1690,8 @@ export async function handleGatewayCommand(req) {
|
|
|
1355
1690
|
const modelName = req.args[2];
|
|
1356
1691
|
if (!modelName)
|
|
1357
1692
|
return badCommand('Usage', 'Usage: `model set <name>`');
|
|
1358
|
-
if (HYBRIDAI_MODELS.length > 0 &&
|
|
1693
|
+
if (HYBRIDAI_MODELS.length > 0 &&
|
|
1694
|
+
!HYBRIDAI_MODELS.includes(modelName)) {
|
|
1359
1695
|
return badCommand('Unknown Model', `\`${modelName}\` is not in the available models list.`);
|
|
1360
1696
|
}
|
|
1361
1697
|
updateSessionModel(session.id, modelName);
|
|
@@ -1380,6 +1716,60 @@ export async function handleGatewayCommand(req) {
|
|
|
1380
1716
|
}
|
|
1381
1717
|
return badCommand('Usage', 'Usage: `rag [on|off]`');
|
|
1382
1718
|
}
|
|
1719
|
+
case 'channel': {
|
|
1720
|
+
const sub = (req.args[1] || '').toLowerCase();
|
|
1721
|
+
if (sub === 'mode' || !sub) {
|
|
1722
|
+
const guildId = req.guildId;
|
|
1723
|
+
if (!guildId) {
|
|
1724
|
+
return badCommand('Guild Only', '`channel mode` is only available in Discord guild channels.');
|
|
1725
|
+
}
|
|
1726
|
+
const requestedMode = (req.args[sub ? 2 : 1] || '').toLowerCase();
|
|
1727
|
+
if (!requestedMode) {
|
|
1728
|
+
const currentMode = resolveGuildChannelMode(guildId, req.channelId);
|
|
1729
|
+
return infoCommand('Channel Mode', [
|
|
1730
|
+
`Current mode: \`${currentMode}\``,
|
|
1731
|
+
`Group policy: \`${DISCORD_GROUP_POLICY}\``,
|
|
1732
|
+
`Config path: \`discord.guilds.${guildId}.channels.${req.channelId}.mode\``,
|
|
1733
|
+
'Usage: `channel mode off|mention|free`',
|
|
1734
|
+
].join('\n'));
|
|
1735
|
+
}
|
|
1736
|
+
if (!DISCORD_CHANNEL_MODE_VALUES.has(requestedMode)) {
|
|
1737
|
+
return badCommand('Usage', 'Usage: `channel mode off|mention|free`');
|
|
1738
|
+
}
|
|
1739
|
+
const mode = requestedMode;
|
|
1740
|
+
updateRuntimeConfig((draft) => {
|
|
1741
|
+
const guild = draft.discord.guilds[guildId] ?? {
|
|
1742
|
+
defaultMode: 'mention',
|
|
1743
|
+
channels: {},
|
|
1744
|
+
};
|
|
1745
|
+
guild.channels[req.channelId] = { mode };
|
|
1746
|
+
draft.discord.guilds[guildId] = guild;
|
|
1747
|
+
});
|
|
1748
|
+
return plainCommand(`Set channel mode to \`${mode}\` for this channel. (Policy: \`${DISCORD_GROUP_POLICY}\`)`);
|
|
1749
|
+
}
|
|
1750
|
+
if (sub === 'policy') {
|
|
1751
|
+
const requestedPolicy = (req.args[2] || '').toLowerCase();
|
|
1752
|
+
if (!requestedPolicy) {
|
|
1753
|
+
return infoCommand('Channel Policy', [
|
|
1754
|
+
`Current policy: \`${DISCORD_GROUP_POLICY}\``,
|
|
1755
|
+
'Policies:',
|
|
1756
|
+
'• `open` — all guild channels are active unless a per-channel mode overrides',
|
|
1757
|
+
'• `allowlist` — only channels listed under `discord.guilds.<guild>.channels` are active',
|
|
1758
|
+
'• `disabled` — all guild channels are disabled',
|
|
1759
|
+
'Usage: `channel policy open|allowlist|disabled`',
|
|
1760
|
+
].join('\n'));
|
|
1761
|
+
}
|
|
1762
|
+
if (!DISCORD_GROUP_POLICY_VALUES.has(requestedPolicy)) {
|
|
1763
|
+
return badCommand('Usage', 'Usage: `channel policy open|allowlist|disabled`');
|
|
1764
|
+
}
|
|
1765
|
+
const policy = requestedPolicy;
|
|
1766
|
+
updateRuntimeConfig((draft) => {
|
|
1767
|
+
draft.discord.groupPolicy = policy;
|
|
1768
|
+
});
|
|
1769
|
+
return plainCommand(`Discord group policy set to \`${policy}\`.`);
|
|
1770
|
+
}
|
|
1771
|
+
return badCommand('Usage', 'Usage: `channel mode [off|mention|free]` or `channel policy [open|allowlist|disabled]`');
|
|
1772
|
+
}
|
|
1383
1773
|
case 'ralph': {
|
|
1384
1774
|
const sub = (req.args[1] || '').toLowerCase();
|
|
1385
1775
|
if (!sub || sub === 'info' || sub === 'status') {
|
|
@@ -1392,7 +1782,10 @@ export async function handleGatewayCommand(req) {
|
|
|
1392
1782
|
}
|
|
1393
1783
|
let nextValue = null;
|
|
1394
1784
|
if (sub === 'on') {
|
|
1395
|
-
nextValue =
|
|
1785
|
+
nextValue =
|
|
1786
|
+
PROACTIVE_RALPH_MAX_ITERATIONS === 0
|
|
1787
|
+
? 3
|
|
1788
|
+
: PROACTIVE_RALPH_MAX_ITERATIONS;
|
|
1396
1789
|
}
|
|
1397
1790
|
else if (sub === 'off') {
|
|
1398
1791
|
nextValue = 0;
|
|
@@ -1431,22 +1824,25 @@ export async function handleGatewayCommand(req) {
|
|
|
1431
1824
|
return plainCommand(`Ralph loop set to ${formatRalphIterations(normalized)}.${restartNote}`);
|
|
1432
1825
|
}
|
|
1433
1826
|
case 'clear': {
|
|
1434
|
-
const deleted = clearSessionHistory(session.id);
|
|
1827
|
+
const deleted = memoryService.clearSessionHistory(session.id);
|
|
1435
1828
|
return infoCommand('Session Cleared', `Deleted ${deleted} messages. Workspace files preserved.`);
|
|
1436
1829
|
}
|
|
1437
1830
|
case 'status': {
|
|
1438
1831
|
const status = getGatewayStatus();
|
|
1439
1832
|
const delegationStatus = delegationQueueStatus();
|
|
1440
|
-
const metrics = readSessionStatusSnapshot(session.id);
|
|
1441
1833
|
const commitShort = resolveGitCommitShort();
|
|
1442
1834
|
const sessionModel = session.model || HYBRIDAI_MODEL;
|
|
1835
|
+
const modelContextWindowTokens = resolveModelContextWindowFallback(sessionModel);
|
|
1836
|
+
const metrics = readSessionStatusSnapshot(session.id, {
|
|
1837
|
+
modelContextWindowTokens,
|
|
1838
|
+
});
|
|
1443
1839
|
const queueLabel = `${delegationStatus.active} active / ${delegationStatus.queued} queued`;
|
|
1444
1840
|
const proactiveQueued = getQueuedProactiveMessageCount();
|
|
1445
1841
|
const cacheKnown = metrics.cacheReadTokens != null || metrics.cacheWriteTokens != null;
|
|
1446
|
-
const contextLabel =
|
|
1842
|
+
const contextLabel = metrics.contextUsedTokens != null && metrics.contextBudgetTokens != null
|
|
1447
1843
|
? `${formatCompactNumber(metrics.contextUsedTokens)}/${formatCompactNumber(metrics.contextBudgetTokens)} (${formatPercent(metrics.contextUsagePercent)})`
|
|
1448
1844
|
: metrics.contextUsedTokens != null
|
|
1449
|
-
? `${formatCompactNumber(metrics.contextUsedTokens)}
|
|
1845
|
+
? `${formatCompactNumber(metrics.contextUsedTokens)}/? (window unknown)`
|
|
1450
1846
|
: 'n/a';
|
|
1451
1847
|
const lines = [
|
|
1452
1848
|
`🦞 HybridClaw v${status.version}${commitShort ? ` (${commitShort})` : ''}`,
|
|
@@ -1467,16 +1863,137 @@ export async function handleGatewayCommand(req) {
|
|
|
1467
1863
|
const sessions = getAllSessions();
|
|
1468
1864
|
if (sessions.length === 0)
|
|
1469
1865
|
return plainCommand('No active sessions.');
|
|
1470
|
-
const list = sessions
|
|
1866
|
+
const list = sessions
|
|
1867
|
+
.slice(0, 20)
|
|
1868
|
+
.map((s) => `${s.id} — ${s.message_count} msgs, last active ${s.last_active}`)
|
|
1869
|
+
.join('\n');
|
|
1471
1870
|
return infoCommand('Sessions', list);
|
|
1472
1871
|
}
|
|
1872
|
+
case 'usage': {
|
|
1873
|
+
const sub = (req.args[1] || 'summary').toLowerCase();
|
|
1874
|
+
if (sub === 'daily' || sub === 'monthly') {
|
|
1875
|
+
const rows = listUsageByAgent({ window: sub });
|
|
1876
|
+
if (rows.length === 0) {
|
|
1877
|
+
return plainCommand(`No usage events recorded for ${sub} window.`);
|
|
1878
|
+
}
|
|
1879
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
1880
|
+
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)}`;
|
|
1881
|
+
});
|
|
1882
|
+
return infoCommand(`Usage (${sub} · by agent)`, lines.join('\n'));
|
|
1883
|
+
}
|
|
1884
|
+
if (sub === 'model') {
|
|
1885
|
+
const maybeWindow = (req.args[2] || '').toLowerCase();
|
|
1886
|
+
const window = maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
1887
|
+
? maybeWindow
|
|
1888
|
+
: 'monthly';
|
|
1889
|
+
const modelAgentId = maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
1890
|
+
? (req.args[3] || '').trim()
|
|
1891
|
+
: (req.args[2] || '').trim();
|
|
1892
|
+
const rows = listUsageByModel({
|
|
1893
|
+
window,
|
|
1894
|
+
agentId: modelAgentId || undefined,
|
|
1895
|
+
});
|
|
1896
|
+
if (rows.length === 0) {
|
|
1897
|
+
return plainCommand('No usage events recorded for model breakdown.');
|
|
1898
|
+
}
|
|
1899
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
1900
|
+
return `${row.model} — ${formatCompactNumber(row.total_tokens)} tokens · ${row.call_count} calls · ${formatUsd(row.total_cost_usd)}`;
|
|
1901
|
+
});
|
|
1902
|
+
const scope = modelAgentId ? `agent ${modelAgentId}` : 'all agents';
|
|
1903
|
+
return infoCommand(`Usage (${window} · by model · ${scope})`, lines.join('\n'));
|
|
1904
|
+
}
|
|
1905
|
+
if (sub !== 'summary') {
|
|
1906
|
+
return badCommand('Usage', 'Usage: `usage [summary|daily|monthly|model [daily|monthly] [agentId]]`');
|
|
1907
|
+
}
|
|
1908
|
+
const currentAgentId = resolveSessionAgentId(session);
|
|
1909
|
+
const daily = getUsageTotals({
|
|
1910
|
+
agentId: currentAgentId || undefined,
|
|
1911
|
+
window: 'daily',
|
|
1912
|
+
});
|
|
1913
|
+
const monthly = getUsageTotals({
|
|
1914
|
+
agentId: currentAgentId || undefined,
|
|
1915
|
+
window: 'monthly',
|
|
1916
|
+
});
|
|
1917
|
+
const topModels = listUsageByModel({
|
|
1918
|
+
agentId: currentAgentId || undefined,
|
|
1919
|
+
window: 'monthly',
|
|
1920
|
+
}).slice(0, 5);
|
|
1921
|
+
const scopeLabel = currentAgentId || 'all agents';
|
|
1922
|
+
const lines = [
|
|
1923
|
+
`Scope: ${scopeLabel}`,
|
|
1924
|
+
`Today: ${formatCompactNumber(daily.total_tokens)} tokens · ${daily.call_count} calls · ${formatUsd(daily.total_cost_usd)}`,
|
|
1925
|
+
`Month: ${formatCompactNumber(monthly.total_tokens)} tokens · ${monthly.call_count} calls · ${formatUsd(monthly.total_cost_usd)}`,
|
|
1926
|
+
];
|
|
1927
|
+
if (topModels.length > 0) {
|
|
1928
|
+
lines.push('Top models (monthly):');
|
|
1929
|
+
lines.push(...topModels.map((row) => `- ${row.model}: ${formatCompactNumber(row.total_tokens)} tokens · ${formatUsd(row.total_cost_usd)}`));
|
|
1930
|
+
}
|
|
1931
|
+
return infoCommand('Usage Summary', lines.join('\n'));
|
|
1932
|
+
}
|
|
1933
|
+
case 'export': {
|
|
1934
|
+
const sub = (req.args[1] || 'session').toLowerCase();
|
|
1935
|
+
if (sub !== 'session') {
|
|
1936
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
1937
|
+
}
|
|
1938
|
+
const targetSessionId = (req.args[2] || session.id || '').trim();
|
|
1939
|
+
if (!targetSessionId) {
|
|
1940
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
1941
|
+
}
|
|
1942
|
+
const targetSession = memoryService.getSessionById(targetSessionId);
|
|
1943
|
+
if (!targetSession) {
|
|
1944
|
+
return badCommand('Not Found', `Session \`${targetSessionId}\` was not found.`);
|
|
1945
|
+
}
|
|
1946
|
+
const exportAgentId = resolveSessionAgentId(targetSession) || resolveSessionAgentId(session);
|
|
1947
|
+
if (!exportAgentId) {
|
|
1948
|
+
return badCommand('Missing Agent', 'Cannot export session: no agent/chatbot is configured for the target session.');
|
|
1949
|
+
}
|
|
1950
|
+
const messages = memoryService.getRecentMessages(targetSessionId);
|
|
1951
|
+
const exported = exportSessionSnapshotJsonl({
|
|
1952
|
+
agentId: exportAgentId,
|
|
1953
|
+
sessionId: targetSessionId,
|
|
1954
|
+
channelId: targetSession.channel_id,
|
|
1955
|
+
summary: targetSession.session_summary,
|
|
1956
|
+
messages,
|
|
1957
|
+
reason: 'manual',
|
|
1958
|
+
});
|
|
1959
|
+
if (!exported) {
|
|
1960
|
+
return badCommand('Export Failed', 'Failed to write session export JSONL file. Check gateway logs for details.');
|
|
1961
|
+
}
|
|
1962
|
+
return infoCommand('Session Exported', [
|
|
1963
|
+
`File: ${exported.path}`,
|
|
1964
|
+
`Messages: ${messages.length}`,
|
|
1965
|
+
`Summary: ${targetSession.session_summary ? 'yes' : 'no'}`,
|
|
1966
|
+
].join('\n'));
|
|
1967
|
+
}
|
|
1473
1968
|
case 'schedule': {
|
|
1474
1969
|
const sub = req.args[1]?.toLowerCase();
|
|
1475
1970
|
if (sub === 'add') {
|
|
1476
1971
|
const rest = req.args.slice(2).join(' ');
|
|
1972
|
+
const atMatch = rest.match(/^at\s+"([^"]+)"\s+(.+)$/i);
|
|
1973
|
+
if (atMatch) {
|
|
1974
|
+
const [, runAtRaw, prompt] = atMatch;
|
|
1975
|
+
const parsedDate = new Date(runAtRaw);
|
|
1976
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
1977
|
+
return badCommand('Invalid Time', `\`${runAtRaw}\` is not a valid ISO timestamp.`);
|
|
1978
|
+
}
|
|
1979
|
+
const taskId = createTask(session.id, req.channelId, '', prompt, parsedDate.toISOString());
|
|
1980
|
+
rearmScheduler();
|
|
1981
|
+
return plainCommand(`Task #${taskId} created: one-shot at \`${parsedDate.toISOString()}\` — ${prompt}`);
|
|
1982
|
+
}
|
|
1983
|
+
const everyMatch = rest.match(/^every\s+(\d+)\s+(.+)$/i);
|
|
1984
|
+
if (everyMatch) {
|
|
1985
|
+
const [, everyRaw, prompt] = everyMatch;
|
|
1986
|
+
const everyMs = Number.parseInt(everyRaw, 10);
|
|
1987
|
+
if (!Number.isFinite(everyMs) || everyMs < 10_000) {
|
|
1988
|
+
return badCommand('Invalid Interval', 'Interval must be at least 10000ms.');
|
|
1989
|
+
}
|
|
1990
|
+
const taskId = createTask(session.id, req.channelId, '', prompt, undefined, everyMs);
|
|
1991
|
+
rearmScheduler();
|
|
1992
|
+
return plainCommand(`Task #${taskId} created: every \`${everyMs}ms\` — ${prompt}`);
|
|
1993
|
+
}
|
|
1477
1994
|
const cronMatch = rest.match(/^"([^"]+)"\s+(.+)$/);
|
|
1478
1995
|
if (!cronMatch) {
|
|
1479
|
-
return badCommand('Usage', 'Usage: `schedule add "<cron>" <prompt>`');
|
|
1996
|
+
return badCommand('Usage', 'Usage: `schedule add "<cron>" <prompt>` or `schedule add at "<ISO time>" <prompt>` or `schedule add every <ms> <prompt>`');
|
|
1480
1997
|
}
|
|
1481
1998
|
const [, cronExpr, prompt] = cronMatch;
|
|
1482
1999
|
try {
|
|
@@ -1487,13 +2004,28 @@ export async function handleGatewayCommand(req) {
|
|
|
1487
2004
|
}
|
|
1488
2005
|
const taskId = createTask(session.id, req.channelId, cronExpr, prompt);
|
|
1489
2006
|
rearmScheduler();
|
|
1490
|
-
return plainCommand(`Task #${taskId} created: \`${cronExpr}\` — ${prompt}`);
|
|
2007
|
+
return plainCommand(`Task #${taskId} created: cron \`${cronExpr}\` — ${prompt}`);
|
|
1491
2008
|
}
|
|
1492
2009
|
if (sub === 'list') {
|
|
1493
2010
|
const tasks = getTasksForSession(session.id);
|
|
1494
2011
|
if (tasks.length === 0)
|
|
1495
2012
|
return plainCommand('No scheduled tasks.');
|
|
1496
|
-
const list = tasks
|
|
2013
|
+
const list = tasks
|
|
2014
|
+
.map((task) => {
|
|
2015
|
+
const scheduleLabel = task.run_at
|
|
2016
|
+
? `at ${task.run_at}`
|
|
2017
|
+
: task.every_ms
|
|
2018
|
+
? `every ${task.every_ms}ms`
|
|
2019
|
+
: task.cron_expr
|
|
2020
|
+
? `cron ${task.cron_expr}`
|
|
2021
|
+
: 'unspecified';
|
|
2022
|
+
const statusLabel = task.last_status || 'n/a';
|
|
2023
|
+
const errorSuffix = task.consecutive_errors > 0
|
|
2024
|
+
? ` · errors ${task.consecutive_errors}`
|
|
2025
|
+
: '';
|
|
2026
|
+
return `#${task.id} ${task.enabled ? 'enabled' : 'disabled'} (${scheduleLabel}) [${statusLabel}${errorSuffix}] — ${task.prompt.slice(0, 60)}`;
|
|
2027
|
+
})
|
|
2028
|
+
.join('\n');
|
|
1497
2029
|
return infoCommand('Scheduled Tasks', list);
|
|
1498
2030
|
}
|
|
1499
2031
|
if (sub === 'remove') {
|
|
@@ -1512,7 +2044,12 @@ export async function handleGatewayCommand(req) {
|
|
|
1512
2044
|
const task = tasks.find((t) => t.id === taskId);
|
|
1513
2045
|
if (!task)
|
|
1514
2046
|
return badCommand('Not Found', `Task #${taskId} was not found in this session.`);
|
|
1515
|
-
|
|
2047
|
+
if (task.enabled) {
|
|
2048
|
+
pauseTask(taskId);
|
|
2049
|
+
}
|
|
2050
|
+
else {
|
|
2051
|
+
resumeTask(taskId);
|
|
2052
|
+
}
|
|
1516
2053
|
rearmScheduler();
|
|
1517
2054
|
return plainCommand(`Task #${taskId} ${task.enabled ? 'disabled' : 'enabled'}.`);
|
|
1518
2055
|
}
|