@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/src/gateway-service.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { CronExpressionParser } from 'cron-parser';
|
|
2
|
-
import {
|
|
3
|
-
|
|
3
|
+
import { runAgent } from './agent.js';
|
|
4
|
+
import {
|
|
5
|
+
emitToolExecutionAuditEvents,
|
|
6
|
+
makeAuditRunId,
|
|
7
|
+
recordAuditEvent,
|
|
8
|
+
} from './audit-events.js';
|
|
4
9
|
import {
|
|
5
10
|
APP_VERSION,
|
|
6
11
|
DISCORD_COMMANDS_ONLY,
|
|
12
|
+
DISCORD_FREE_RESPONSE_CHANNELS,
|
|
13
|
+
DISCORD_GROUP_POLICY,
|
|
14
|
+
DISCORD_GUILDS,
|
|
7
15
|
DISCORD_RESPOND_TO_ALL_MESSAGES,
|
|
8
16
|
HYBRIDAI_CHATBOT_ID,
|
|
9
17
|
HYBRIDAI_ENABLE_RAG,
|
|
@@ -17,61 +25,72 @@ import {
|
|
|
17
25
|
PROACTIVE_DELEGATION_MAX_PER_TURN,
|
|
18
26
|
PROACTIVE_RALPH_MAX_ITERATIONS,
|
|
19
27
|
} from './config.js';
|
|
20
|
-
import { runAgent } from './agent.js';
|
|
21
|
-
import { getActiveContainerCount, stopSessionContainer } from './container-runner.js';
|
|
22
28
|
import {
|
|
23
|
-
|
|
29
|
+
getActiveContainerCount,
|
|
30
|
+
stopSessionContainer,
|
|
31
|
+
} from './container-runner.js';
|
|
32
|
+
import { buildConversationContext } from './conversation.js';
|
|
33
|
+
import {
|
|
24
34
|
createTask,
|
|
25
35
|
deleteTask,
|
|
26
36
|
getAllSessions,
|
|
27
|
-
getConversationHistory,
|
|
28
|
-
getOrCreateSession,
|
|
29
37
|
getQueuedProactiveMessageCount,
|
|
30
38
|
getRecentStructuredAuditForSession,
|
|
31
39
|
getSessionCount,
|
|
32
40
|
getTasksForSession,
|
|
41
|
+
getUsageTotals,
|
|
42
|
+
listUsageByAgent,
|
|
43
|
+
listUsageByModel,
|
|
33
44
|
logAudit,
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
pauseTask,
|
|
46
|
+
recordUsageEvent,
|
|
47
|
+
resumeTask,
|
|
36
48
|
updateSessionChatbot,
|
|
37
49
|
updateSessionModel,
|
|
38
50
|
updateSessionRag,
|
|
39
51
|
} from './db.js';
|
|
40
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
delegationQueueStatus,
|
|
54
|
+
enqueueDelegation,
|
|
55
|
+
} from './delegation-manager.js';
|
|
56
|
+
import {
|
|
57
|
+
type GatewayChatRequestBody,
|
|
58
|
+
type GatewayChatResult,
|
|
59
|
+
type GatewayCommandRequest,
|
|
60
|
+
type GatewayCommandResult,
|
|
61
|
+
type GatewayStatus,
|
|
62
|
+
renderGatewayCommand,
|
|
63
|
+
} from './gateway-types.js';
|
|
41
64
|
import { fetchHybridAIBots } from './hybridai-bots.js';
|
|
42
65
|
import { logger } from './logger.js';
|
|
66
|
+
import { memoryService } from './memory-service.js';
|
|
43
67
|
import { getObservabilityIngestState } from './observability-ingest.js';
|
|
44
|
-
import {
|
|
68
|
+
import { updateRuntimeConfig } from './runtime-config.js';
|
|
69
|
+
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
70
|
+
import { getSchedulerStatus, rearmScheduler } from './scheduler.js';
|
|
71
|
+
import { exportSessionSnapshotJsonl } from './session-export.js';
|
|
45
72
|
import { maybeCompactSession } from './session-maintenance.js';
|
|
46
73
|
import { appendSessionTranscript } from './session-transcripts.js';
|
|
47
74
|
import { processSideEffects } from './side-effects.js';
|
|
48
75
|
import { expandSkillInvocation } from './skills.js';
|
|
49
76
|
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type GatewayCommandRequest,
|
|
54
|
-
type GatewayCommandResult,
|
|
55
|
-
type GatewayStatus,
|
|
56
|
-
} from './gateway-types.js';
|
|
77
|
+
estimateTokenCountFromMessages,
|
|
78
|
+
estimateTokenCountFromText,
|
|
79
|
+
} from './token-efficiency.js';
|
|
57
80
|
import type {
|
|
58
81
|
ArtifactMetadata,
|
|
82
|
+
CanonicalSessionContext,
|
|
59
83
|
ChatMessage,
|
|
60
84
|
DelegationSideEffect,
|
|
61
85
|
DelegationTaskSpec,
|
|
62
86
|
MediaContextItem,
|
|
63
87
|
ScheduledTask,
|
|
64
|
-
StructuredAuditEntry,
|
|
65
88
|
StoredMessage,
|
|
89
|
+
StructuredAuditEntry,
|
|
66
90
|
TokenUsageStats,
|
|
67
91
|
ToolProgressEvent,
|
|
68
92
|
} from './types.js';
|
|
69
93
|
import { ensureBootstrapFiles } from './workspace.js';
|
|
70
|
-
import { buildConversationContext } from './conversation.js';
|
|
71
|
-
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
72
|
-
import { delegationQueueStatus, enqueueDelegation } from './delegation-manager.js';
|
|
73
|
-
import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
|
|
74
|
-
import { updateRuntimeConfig } from './runtime-config.js';
|
|
75
94
|
|
|
76
95
|
const BOT_CACHE_TTL = 300_000; // 5 minutes
|
|
77
96
|
const MAX_HISTORY_MESSAGES = 40;
|
|
@@ -103,10 +122,15 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
|
103
122
|
'browser_network',
|
|
104
123
|
'browser_close',
|
|
105
124
|
];
|
|
106
|
-
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
125
|
+
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
126
|
+
...BASE_SUBAGENT_ALLOWED_TOOLS,
|
|
127
|
+
'delegate',
|
|
128
|
+
];
|
|
107
129
|
const MAX_DELEGATION_TASKS = 6;
|
|
108
130
|
const MAX_DELEGATION_USER_CHARS = 500;
|
|
109
131
|
const MAX_RALPH_ITERATIONS = 64;
|
|
132
|
+
const DISCORD_CHANNEL_MODE_VALUES = new Set(['off', 'mention', 'free']);
|
|
133
|
+
const DISCORD_GROUP_POLICY_VALUES = new Set(['open', 'allowlist', 'disabled']);
|
|
110
134
|
const IMAGE_QUESTION_RE =
|
|
111
135
|
/(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;
|
|
112
136
|
const BROWSER_TAB_RE =
|
|
@@ -190,7 +214,9 @@ export interface GatewayChatRequest {
|
|
|
190
214
|
enableRag?: GatewayChatRequestBody['enableRag'];
|
|
191
215
|
onTextDelta?: (delta: string) => void;
|
|
192
216
|
onToolProgress?: (event: ToolProgressEvent) => void;
|
|
193
|
-
onProactiveMessage?: (
|
|
217
|
+
onProactiveMessage?: (
|
|
218
|
+
message: ProactiveMessagePayload,
|
|
219
|
+
) => void | Promise<void>;
|
|
194
220
|
abortSignal?: AbortSignal;
|
|
195
221
|
}
|
|
196
222
|
|
|
@@ -199,7 +225,12 @@ export interface ProactiveMessagePayload {
|
|
|
199
225
|
artifacts?: ArtifactMetadata[];
|
|
200
226
|
}
|
|
201
227
|
|
|
202
|
-
export type {
|
|
228
|
+
export type {
|
|
229
|
+
GatewayChatResult,
|
|
230
|
+
GatewayCommandRequest,
|
|
231
|
+
GatewayCommandResult,
|
|
232
|
+
GatewayStatus,
|
|
233
|
+
};
|
|
203
234
|
export { renderGatewayCommand };
|
|
204
235
|
|
|
205
236
|
function formatUptime(seconds: number): string {
|
|
@@ -221,22 +252,31 @@ function parseIntOrNull(raw: string | undefined): number | null {
|
|
|
221
252
|
return Number.isNaN(parsed) ? null : parsed;
|
|
222
253
|
}
|
|
223
254
|
|
|
224
|
-
function normalizeMediaContextItems(
|
|
255
|
+
function normalizeMediaContextItems(
|
|
256
|
+
raw: GatewayChatRequestBody['media'],
|
|
257
|
+
): MediaContextItem[] {
|
|
225
258
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
226
259
|
const normalized: MediaContextItem[] = [];
|
|
227
260
|
for (const item of raw) {
|
|
228
261
|
if (!item || typeof item !== 'object') continue;
|
|
229
|
-
const path =
|
|
262
|
+
const path =
|
|
263
|
+
typeof item.path === 'string' && item.path.trim()
|
|
264
|
+
? item.path.trim()
|
|
265
|
+
: null;
|
|
230
266
|
const url = typeof item.url === 'string' ? item.url.trim() : '';
|
|
231
|
-
const originalUrl =
|
|
232
|
-
|
|
267
|
+
const originalUrl =
|
|
268
|
+
typeof item.originalUrl === 'string' ? item.originalUrl.trim() : '';
|
|
269
|
+
const filename =
|
|
270
|
+
typeof item.filename === 'string' ? item.filename.trim() : '';
|
|
233
271
|
if (!url || !originalUrl || !filename) continue;
|
|
234
|
-
const sizeBytes =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
const sizeBytes =
|
|
273
|
+
typeof item.sizeBytes === 'number' && Number.isFinite(item.sizeBytes)
|
|
274
|
+
? Math.max(0, Math.floor(item.sizeBytes))
|
|
275
|
+
: 0;
|
|
276
|
+
const mimeType =
|
|
277
|
+
typeof item.mimeType === 'string' && item.mimeType.trim()
|
|
278
|
+
? item.mimeType.trim().toLowerCase()
|
|
279
|
+
: null;
|
|
240
280
|
normalized.push({
|
|
241
281
|
path,
|
|
242
282
|
url,
|
|
@@ -251,7 +291,9 @@ function normalizeMediaContextItems(raw: GatewayChatRequestBody['media']): Media
|
|
|
251
291
|
|
|
252
292
|
function buildMediaPromptContext(media: MediaContextItem[]): string {
|
|
253
293
|
if (media.length === 0) return '';
|
|
254
|
-
const mediaPaths = media
|
|
294
|
+
const mediaPaths = media
|
|
295
|
+
.map((item) => item.path)
|
|
296
|
+
.filter((path): path is string => Boolean(path));
|
|
255
297
|
const mediaUrls = media.map((item) => item.url);
|
|
256
298
|
const mediaTypes = media.map((item) => item.mimeType || 'unknown');
|
|
257
299
|
const payload = media.map((item, index) => ({
|
|
@@ -294,7 +336,10 @@ export interface MediaToolPolicy {
|
|
|
294
336
|
prioritizeVisionTool: boolean;
|
|
295
337
|
}
|
|
296
338
|
|
|
297
|
-
export function resolveMediaToolPolicy(
|
|
339
|
+
export function resolveMediaToolPolicy(
|
|
340
|
+
content: string,
|
|
341
|
+
media: MediaContextItem[],
|
|
342
|
+
): MediaToolPolicy {
|
|
298
343
|
if (media.length === 0) {
|
|
299
344
|
return {
|
|
300
345
|
blockedTools: undefined,
|
|
@@ -353,14 +398,22 @@ function formatRelativeTime(raw: string | null | undefined): string {
|
|
|
353
398
|
if (!at) return 'unknown';
|
|
354
399
|
const deltaMs = Date.now() - at.getTime();
|
|
355
400
|
if (deltaMs < 15_000) return 'just now';
|
|
356
|
-
if (deltaMs < 60_000)
|
|
357
|
-
|
|
358
|
-
if (deltaMs <
|
|
401
|
+
if (deltaMs < 60_000)
|
|
402
|
+
return `${Math.max(1, Math.floor(deltaMs / 1_000))}s ago`;
|
|
403
|
+
if (deltaMs < 3_600_000)
|
|
404
|
+
return `${Math.max(1, Math.floor(deltaMs / 60_000))}m ago`;
|
|
405
|
+
if (deltaMs < 86_400_000)
|
|
406
|
+
return `${Math.max(1, Math.floor(deltaMs / 3_600_000))}h ago`;
|
|
359
407
|
return `${Math.max(1, Math.floor(deltaMs / 86_400_000))}d ago`;
|
|
360
408
|
}
|
|
361
409
|
|
|
362
410
|
function numberFromUnknown(value: unknown): number | null {
|
|
363
|
-
if (
|
|
411
|
+
if (
|
|
412
|
+
typeof value !== 'number' ||
|
|
413
|
+
Number.isNaN(value) ||
|
|
414
|
+
!Number.isFinite(value)
|
|
415
|
+
)
|
|
416
|
+
return null;
|
|
364
417
|
return value;
|
|
365
418
|
}
|
|
366
419
|
|
|
@@ -372,7 +425,9 @@ function firstNumber(values: unknown[]): number | null {
|
|
|
372
425
|
return null;
|
|
373
426
|
}
|
|
374
427
|
|
|
375
|
-
function parseAuditPayload(
|
|
428
|
+
function parseAuditPayload(
|
|
429
|
+
entry: StructuredAuditEntry,
|
|
430
|
+
): Record<string, unknown> | null {
|
|
376
431
|
try {
|
|
377
432
|
const parsed = JSON.parse(entry.payload) as unknown;
|
|
378
433
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
@@ -386,27 +441,140 @@ function formatCompactNumber(value: number | null): string {
|
|
|
386
441
|
if (value == null) return 'n/a';
|
|
387
442
|
const abs = Math.abs(value);
|
|
388
443
|
if (abs >= 1_000_000) {
|
|
389
|
-
const scaled =
|
|
444
|
+
const scaled =
|
|
445
|
+
abs >= 10_000_000
|
|
446
|
+
? (value / 1_000_000).toFixed(0)
|
|
447
|
+
: (value / 1_000_000).toFixed(1);
|
|
390
448
|
return `${scaled.replace(/\.0$/, '')}M`;
|
|
391
449
|
}
|
|
392
450
|
if (abs >= 1_000) {
|
|
393
|
-
const scaled =
|
|
451
|
+
const scaled =
|
|
452
|
+
abs >= 10_000 ? (value / 1_000).toFixed(0) : (value / 1_000).toFixed(1);
|
|
394
453
|
return `${scaled.replace(/\.0$/, '')}k`;
|
|
395
454
|
}
|
|
396
455
|
return String(Math.round(value));
|
|
397
456
|
}
|
|
398
457
|
|
|
399
458
|
function formatPercent(value: number | null): string {
|
|
400
|
-
if (value == null || Number.isNaN(value) || !Number.isFinite(value))
|
|
459
|
+
if (value == null || Number.isNaN(value) || !Number.isFinite(value))
|
|
460
|
+
return 'n/a';
|
|
401
461
|
return `${Math.max(0, Math.min(100, Math.round(value)))}%`;
|
|
402
462
|
}
|
|
403
463
|
|
|
464
|
+
function formatUsd(value: number | null): string {
|
|
465
|
+
if (value == null || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
466
|
+
return 'n/a';
|
|
467
|
+
}
|
|
468
|
+
if (value <= 0) return '$0.0000';
|
|
469
|
+
if (value >= 1) return `$${value.toFixed(2)}`;
|
|
470
|
+
if (value >= 0.01) return `$${value.toFixed(4)}`;
|
|
471
|
+
return `$${value.toFixed(6)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function resolveSessionAgentId(session: {
|
|
475
|
+
chatbot_id: string | null;
|
|
476
|
+
}): string | null {
|
|
477
|
+
const sessionAgent = session.chatbot_id?.trim();
|
|
478
|
+
if (sessionAgent) return sessionAgent;
|
|
479
|
+
const defaultAgent = HYBRIDAI_CHATBOT_ID?.trim();
|
|
480
|
+
if (defaultAgent) return defaultAgent;
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function extractUsageCostUsd(tokenUsage?: TokenUsageStats): number {
|
|
485
|
+
if (!tokenUsage) return 0;
|
|
486
|
+
const costCarrier = tokenUsage as unknown as Record<string, unknown>;
|
|
487
|
+
const value = firstNumber([
|
|
488
|
+
costCarrier.costUsd,
|
|
489
|
+
costCarrier.costUSD,
|
|
490
|
+
costCarrier.cost_usd,
|
|
491
|
+
costCarrier.estimatedCostUsd,
|
|
492
|
+
costCarrier.estimated_cost_usd,
|
|
493
|
+
]);
|
|
494
|
+
if (value == null) return 0;
|
|
495
|
+
return Math.max(0, value);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function formatCanonicalContextPrompt(params: {
|
|
499
|
+
summary: string | null;
|
|
500
|
+
recentMessages: Array<{
|
|
501
|
+
role: string;
|
|
502
|
+
content: string;
|
|
503
|
+
session_id: string;
|
|
504
|
+
channel_id: string | null;
|
|
505
|
+
}>;
|
|
506
|
+
}): string | null {
|
|
507
|
+
const sections: string[] = [];
|
|
508
|
+
const summary = (params.summary || '').trim();
|
|
509
|
+
if (summary) {
|
|
510
|
+
sections.push(['### Canonical Session Summary', summary].join('\n'));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (params.recentMessages.length > 0) {
|
|
514
|
+
const lines = params.recentMessages.slice(-6).map((entry) => {
|
|
515
|
+
const role = (entry.role || 'user').trim().toLowerCase();
|
|
516
|
+
const who = role === 'assistant' ? 'Assistant' : 'User';
|
|
517
|
+
const from =
|
|
518
|
+
entry.channel_id && entry.channel_id.trim()
|
|
519
|
+
? `${entry.channel_id.trim()} (${entry.session_id})`
|
|
520
|
+
: entry.session_id;
|
|
521
|
+
const compact = entry.content.replace(/\s+/g, ' ').trim();
|
|
522
|
+
const short =
|
|
523
|
+
compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
|
|
524
|
+
return `- ${who} [${from}]: ${short}`;
|
|
525
|
+
});
|
|
526
|
+
sections.push(
|
|
527
|
+
[
|
|
528
|
+
'### Cross-Channel Recall',
|
|
529
|
+
'Recent context from other sessions/channels for this user:',
|
|
530
|
+
...lines,
|
|
531
|
+
].join('\n'),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const merged = sections.join('\n\n').trim();
|
|
536
|
+
return merged || null;
|
|
537
|
+
}
|
|
538
|
+
|
|
404
539
|
function resolveActivationModeLabel(): string {
|
|
405
540
|
if (DISCORD_COMMANDS_ONLY) return 'commands-only';
|
|
541
|
+
if (DISCORD_GROUP_POLICY === 'disabled') return 'disabled';
|
|
542
|
+
if (DISCORD_GROUP_POLICY === 'allowlist') return 'allowlist';
|
|
543
|
+
if (DISCORD_FREE_RESPONSE_CHANNELS.length > 0)
|
|
544
|
+
return `mention + ${DISCORD_FREE_RESPONSE_CHANNELS.length} free channel(s)`;
|
|
406
545
|
if (DISCORD_RESPOND_TO_ALL_MESSAGES) return 'all messages';
|
|
407
546
|
return 'mention';
|
|
408
547
|
}
|
|
409
548
|
|
|
549
|
+
function resolveGuildChannelMode(
|
|
550
|
+
guildId: string | null,
|
|
551
|
+
channelId: string,
|
|
552
|
+
): 'off' | 'mention' | 'free' {
|
|
553
|
+
if (!guildId) return 'free';
|
|
554
|
+
if (DISCORD_GROUP_POLICY === 'disabled') return 'off';
|
|
555
|
+
const guild = DISCORD_GUILDS[guildId];
|
|
556
|
+
const explicit = guild?.channels[channelId]?.mode;
|
|
557
|
+
if (DISCORD_GROUP_POLICY === 'allowlist') {
|
|
558
|
+
return explicit ?? 'off';
|
|
559
|
+
}
|
|
560
|
+
if (explicit === 'off' || explicit === 'mention' || explicit === 'free') {
|
|
561
|
+
return explicit;
|
|
562
|
+
}
|
|
563
|
+
if (DISCORD_FREE_RESPONSE_CHANNELS.includes(channelId)) return 'free';
|
|
564
|
+
if (guild) {
|
|
565
|
+
const defaultMode = guild.defaultMode;
|
|
566
|
+
if (
|
|
567
|
+
defaultMode === 'off' ||
|
|
568
|
+
defaultMode === 'mention' ||
|
|
569
|
+
defaultMode === 'free'
|
|
570
|
+
) {
|
|
571
|
+
return defaultMode;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (DISCORD_RESPOND_TO_ALL_MESSAGES) return 'free';
|
|
575
|
+
return 'mention';
|
|
576
|
+
}
|
|
577
|
+
|
|
410
578
|
interface SessionStatusSnapshot {
|
|
411
579
|
promptTokens: number | null;
|
|
412
580
|
completionTokens: number | null;
|
|
@@ -426,7 +594,8 @@ function readSessionStatusSnapshot(sessionId: string): SessionStatusSnapshot {
|
|
|
426
594
|
for (const entry of entries) {
|
|
427
595
|
const payload = parseAuditPayload(entry);
|
|
428
596
|
if (!payload) continue;
|
|
429
|
-
const payloadType =
|
|
597
|
+
const payloadType =
|
|
598
|
+
typeof payload.type === 'string' ? payload.type : entry.event_type;
|
|
430
599
|
if (!usagePayload && payloadType === 'model.usage') {
|
|
431
600
|
usagePayload = payload;
|
|
432
601
|
} else if (!contextPayload && payloadType === 'context.optimization') {
|
|
@@ -457,21 +626,23 @@ function readSessionStatusSnapshot(sessionId: string): SessionStatusSnapshot {
|
|
|
457
626
|
const cacheRead = Math.max(0, cacheReadTokens || 0);
|
|
458
627
|
const cacheWrite = Math.max(0, cacheWriteTokens || 0);
|
|
459
628
|
const cacheTotal = cacheRead + cacheWrite;
|
|
460
|
-
const cacheHitPercent =
|
|
629
|
+
const cacheHitPercent =
|
|
630
|
+
cacheTotal > 0 ? (cacheRead / cacheTotal) * 100 : null;
|
|
461
631
|
|
|
462
|
-
const contextUsedTokens = firstNumber([
|
|
632
|
+
const contextUsedTokens = firstNumber([
|
|
633
|
+
contextPayload?.historyEstimatedTokens,
|
|
634
|
+
]);
|
|
463
635
|
const contextBudgetTokens = (() => {
|
|
464
636
|
const maxChars = firstNumber([contextPayload?.historyMaxChars]);
|
|
465
637
|
if (maxChars == null || maxChars <= 0) return null;
|
|
466
638
|
return Math.max(1, Math.round(maxChars / 4));
|
|
467
639
|
})();
|
|
468
|
-
const contextUsagePercent =
|
|
469
|
-
contextUsedTokens != null
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
: null;
|
|
640
|
+
const contextUsagePercent =
|
|
641
|
+
contextUsedTokens != null &&
|
|
642
|
+
contextBudgetTokens != null &&
|
|
643
|
+
contextBudgetTokens > 0
|
|
644
|
+
? (contextUsedTokens / contextBudgetTokens) * 100
|
|
645
|
+
: null;
|
|
475
646
|
|
|
476
647
|
return {
|
|
477
648
|
promptTokens,
|
|
@@ -522,10 +693,12 @@ function isVersionOnlyQuestion(raw: string): boolean {
|
|
|
522
693
|
if (detailedRuntimeTokens.some((token) => text.includes(token))) return false;
|
|
523
694
|
|
|
524
695
|
const words = text.split(' ').filter(Boolean);
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
696
|
+
if (
|
|
697
|
+
words.length > 8 &&
|
|
698
|
+
!text.includes('welche version') &&
|
|
699
|
+
!text.includes('what version') &&
|
|
700
|
+
!text.includes('which version')
|
|
701
|
+
) {
|
|
529
702
|
return false;
|
|
530
703
|
}
|
|
531
704
|
|
|
@@ -548,8 +721,46 @@ function recordSuccessfulTurn(opts: {
|
|
|
548
721
|
toolCallCount: number;
|
|
549
722
|
startedAt: number;
|
|
550
723
|
}): void {
|
|
551
|
-
|
|
552
|
-
|
|
724
|
+
memoryService.storeTurn({
|
|
725
|
+
sessionId: opts.sessionId,
|
|
726
|
+
user: {
|
|
727
|
+
userId: opts.userId,
|
|
728
|
+
username: opts.username,
|
|
729
|
+
content: opts.userContent,
|
|
730
|
+
},
|
|
731
|
+
assistant: {
|
|
732
|
+
userId: 'assistant',
|
|
733
|
+
username: null,
|
|
734
|
+
content: opts.resultText,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
try {
|
|
738
|
+
if (opts.userId.trim()) {
|
|
739
|
+
memoryService.appendCanonicalMessages({
|
|
740
|
+
agentId: opts.agentId,
|
|
741
|
+
userId: opts.userId,
|
|
742
|
+
newMessages: [
|
|
743
|
+
{
|
|
744
|
+
role: 'user',
|
|
745
|
+
content: opts.userContent,
|
|
746
|
+
sessionId: opts.sessionId,
|
|
747
|
+
channelId: opts.channelId,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
role: 'assistant',
|
|
751
|
+
content: opts.resultText,
|
|
752
|
+
sessionId: opts.sessionId,
|
|
753
|
+
channelId: opts.channelId,
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
} catch (err) {
|
|
759
|
+
logger.debug(
|
|
760
|
+
{ sessionId: opts.sessionId, userId: opts.userId, err },
|
|
761
|
+
'Failed to append canonical session memory',
|
|
762
|
+
);
|
|
763
|
+
}
|
|
553
764
|
appendSessionTranscript(opts.agentId, {
|
|
554
765
|
sessionId: opts.sessionId,
|
|
555
766
|
channelId: opts.channelId,
|
|
@@ -575,7 +786,10 @@ function recordSuccessfulTurn(opts: {
|
|
|
575
786
|
model: opts.model,
|
|
576
787
|
channelId: opts.channelId,
|
|
577
788
|
}).catch((err) => {
|
|
578
|
-
logger.warn(
|
|
789
|
+
logger.warn(
|
|
790
|
+
{ sessionId: opts.sessionId, err },
|
|
791
|
+
'Background session compaction failed',
|
|
792
|
+
);
|
|
579
793
|
});
|
|
580
794
|
|
|
581
795
|
recordAuditEvent({
|
|
@@ -640,19 +854,30 @@ function buildTokenUsageAuditPayload(
|
|
|
640
854
|
}, 0);
|
|
641
855
|
const completionChars = (resultText || '').length;
|
|
642
856
|
|
|
643
|
-
const fallbackEstimatedPromptTokens =
|
|
644
|
-
|
|
645
|
-
const
|
|
646
|
-
|
|
857
|
+
const fallbackEstimatedPromptTokens =
|
|
858
|
+
estimateTokenCountFromMessages(messages);
|
|
859
|
+
const fallbackEstimatedCompletionTokens = estimateTokenCountFromText(
|
|
860
|
+
resultText || '',
|
|
861
|
+
);
|
|
862
|
+
const estimatedPromptTokens =
|
|
863
|
+
tokenUsage?.estimatedPromptTokens || fallbackEstimatedPromptTokens;
|
|
864
|
+
const estimatedCompletionTokens =
|
|
865
|
+
tokenUsage?.estimatedCompletionTokens || fallbackEstimatedCompletionTokens;
|
|
647
866
|
const estimatedTotalTokens =
|
|
648
|
-
tokenUsage?.estimatedTotalTokens ||
|
|
867
|
+
tokenUsage?.estimatedTotalTokens ||
|
|
868
|
+
estimatedPromptTokens + estimatedCompletionTokens;
|
|
649
869
|
|
|
650
870
|
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
651
871
|
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
652
872
|
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
653
|
-
const apiTotalTokens =
|
|
654
|
-
|
|
655
|
-
const
|
|
873
|
+
const apiTotalTokens =
|
|
874
|
+
tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
|
|
875
|
+
const promptTokens = apiUsageAvailable
|
|
876
|
+
? apiPromptTokens
|
|
877
|
+
: estimatedPromptTokens;
|
|
878
|
+
const completionTokens = apiUsageAvailable
|
|
879
|
+
? apiCompletionTokens
|
|
880
|
+
: estimatedCompletionTokens;
|
|
656
881
|
const totalTokens = apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens;
|
|
657
882
|
|
|
658
883
|
return {
|
|
@@ -684,11 +909,19 @@ export function getGatewayStatus(): GatewayStatus {
|
|
|
684
909
|
ragDefault: HYBRIDAI_ENABLE_RAG,
|
|
685
910
|
timestamp: new Date().toISOString(),
|
|
686
911
|
observability: getObservabilityIngestState(),
|
|
912
|
+
scheduler: {
|
|
913
|
+
jobs: getSchedulerStatus(),
|
|
914
|
+
},
|
|
687
915
|
};
|
|
688
916
|
}
|
|
689
917
|
|
|
690
|
-
export function getGatewayHistory(
|
|
691
|
-
|
|
918
|
+
export function getGatewayHistory(
|
|
919
|
+
sessionId: string,
|
|
920
|
+
limit = MAX_HISTORY_MESSAGES,
|
|
921
|
+
): StoredMessage[] {
|
|
922
|
+
return memoryService
|
|
923
|
+
.getConversationHistory(sessionId, Math.max(1, Math.min(limit, 200)))
|
|
924
|
+
.reverse();
|
|
692
925
|
}
|
|
693
926
|
|
|
694
927
|
function extractDelegationDepth(sessionId: string): number {
|
|
@@ -698,18 +931,28 @@ function extractDelegationDepth(sessionId: string): number {
|
|
|
698
931
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
699
932
|
}
|
|
700
933
|
|
|
701
|
-
function nextDelegationSessionId(
|
|
702
|
-
|
|
934
|
+
function nextDelegationSessionId(
|
|
935
|
+
parentSessionId: string,
|
|
936
|
+
nextDepth: number,
|
|
937
|
+
): string {
|
|
938
|
+
const safeParent = parentSessionId
|
|
939
|
+
.replace(/[^a-zA-Z0-9:_-]/g, '-')
|
|
940
|
+
.slice(0, 48);
|
|
703
941
|
const nonce = Math.random().toString(36).slice(2, 8);
|
|
704
942
|
return `delegate:d${nextDepth}:${safeParent}:${Date.now()}:${nonce}`;
|
|
705
943
|
}
|
|
706
944
|
|
|
707
945
|
function resolveSubagentAllowedTools(depth: number): string[] {
|
|
708
|
-
if (depth < PROACTIVE_DELEGATION_MAX_DEPTH)
|
|
946
|
+
if (depth < PROACTIVE_DELEGATION_MAX_DEPTH)
|
|
947
|
+
return ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS;
|
|
709
948
|
return BASE_SUBAGENT_ALLOWED_TOOLS;
|
|
710
949
|
}
|
|
711
950
|
|
|
712
|
-
function buildSubagentSystemPrompt(params: {
|
|
951
|
+
function buildSubagentSystemPrompt(params: {
|
|
952
|
+
depth: number;
|
|
953
|
+
canDelegate: boolean;
|
|
954
|
+
mode: DelegationMode;
|
|
955
|
+
}): string {
|
|
713
956
|
const { depth, canDelegate, mode } = params;
|
|
714
957
|
const delegationLine = canDelegate
|
|
715
958
|
? 'You may delegate further only if absolutely necessary and still within depth/turn limits.'
|
|
@@ -757,31 +1000,50 @@ function formatDurationMs(ms: number): string {
|
|
|
757
1000
|
return `${(ms / 1_000).toFixed(1)}s`;
|
|
758
1001
|
}
|
|
759
1002
|
|
|
760
|
-
function abbreviateForUser(
|
|
1003
|
+
function abbreviateForUser(
|
|
1004
|
+
text: string,
|
|
1005
|
+
maxChars = MAX_DELEGATION_USER_CHARS,
|
|
1006
|
+
): string {
|
|
761
1007
|
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
762
1008
|
if (normalized.length <= maxChars) return normalized;
|
|
763
1009
|
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
764
1010
|
}
|
|
765
1011
|
|
|
766
1012
|
function classifyDelegationError(errorText: string): DelegationErrorClass {
|
|
767
|
-
if (
|
|
768
|
-
|
|
1013
|
+
if (
|
|
1014
|
+
PERMANENT_DELEGATION_ERROR_PATTERNS.some((pattern) =>
|
|
1015
|
+
pattern.test(errorText),
|
|
1016
|
+
)
|
|
1017
|
+
)
|
|
1018
|
+
return 'permanent';
|
|
1019
|
+
if (
|
|
1020
|
+
TRANSIENT_DELEGATION_ERROR_PATTERNS.some((pattern) =>
|
|
1021
|
+
pattern.test(errorText),
|
|
1022
|
+
)
|
|
1023
|
+
)
|
|
1024
|
+
return 'transient';
|
|
769
1025
|
return 'unknown';
|
|
770
1026
|
}
|
|
771
1027
|
|
|
772
1028
|
function inferDelegationStatus(errorText: string): DelegationRunStatus {
|
|
773
|
-
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
1029
|
+
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
1030
|
+
? 'timeout'
|
|
1031
|
+
: 'failed';
|
|
774
1032
|
}
|
|
775
1033
|
|
|
776
|
-
function normalizeDelegationTask(
|
|
1034
|
+
function normalizeDelegationTask(
|
|
1035
|
+
raw: unknown,
|
|
1036
|
+
fallbackModel: string,
|
|
1037
|
+
): NormalizedDelegationTask | null {
|
|
777
1038
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
778
1039
|
const task = raw as DelegationTaskSpec;
|
|
779
1040
|
const prompt = typeof task.prompt === 'string' ? task.prompt.trim() : '';
|
|
780
1041
|
if (!prompt) return null;
|
|
781
1042
|
const label = typeof task.label === 'string' ? task.label.trim() : '';
|
|
782
|
-
const model =
|
|
783
|
-
|
|
784
|
-
|
|
1043
|
+
const model =
|
|
1044
|
+
typeof task.model === 'string' && task.model.trim()
|
|
1045
|
+
? task.model.trim()
|
|
1046
|
+
: fallbackModel;
|
|
785
1047
|
return {
|
|
786
1048
|
prompt,
|
|
787
1049
|
label: label || undefined,
|
|
@@ -789,11 +1051,15 @@ function normalizeDelegationTask(raw: unknown, fallbackModel: string): Normalize
|
|
|
789
1051
|
};
|
|
790
1052
|
}
|
|
791
1053
|
|
|
792
|
-
function normalizeDelegationEffect(
|
|
1054
|
+
function normalizeDelegationEffect(
|
|
1055
|
+
effect: DelegationSideEffect,
|
|
1056
|
+
fallbackModel: string,
|
|
1057
|
+
): {
|
|
793
1058
|
plan?: NormalizedDelegationPlan;
|
|
794
1059
|
error?: string;
|
|
795
1060
|
} {
|
|
796
|
-
const rawMode =
|
|
1061
|
+
const rawMode =
|
|
1062
|
+
typeof effect.mode === 'string' ? effect.mode.trim().toLowerCase() : '';
|
|
797
1063
|
const modeRaw: DelegationMode | '' =
|
|
798
1064
|
rawMode === 'single' || rawMode === 'parallel' || rawMode === 'chain'
|
|
799
1065
|
? rawMode
|
|
@@ -803,9 +1069,10 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
803
1069
|
}
|
|
804
1070
|
|
|
805
1071
|
const label = typeof effect.label === 'string' ? effect.label.trim() : '';
|
|
806
|
-
const baseModel =
|
|
807
|
-
|
|
808
|
-
|
|
1072
|
+
const baseModel =
|
|
1073
|
+
typeof effect.model === 'string' && effect.model.trim()
|
|
1074
|
+
? effect.model.trim()
|
|
1075
|
+
: fallbackModel;
|
|
809
1076
|
const prompt = typeof effect.prompt === 'string' ? effect.prompt.trim() : '';
|
|
810
1077
|
const rawTasks = Array.isArray(effect.tasks) ? effect.tasks : [];
|
|
811
1078
|
const rawChain = Array.isArray(effect.chain) ? effect.chain : [];
|
|
@@ -832,12 +1099,15 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
832
1099
|
return { error: `${mode} delegation requires at least one task` };
|
|
833
1100
|
}
|
|
834
1101
|
if (sourceTasks.length > MAX_DELEGATION_TASKS) {
|
|
835
|
-
return {
|
|
1102
|
+
return {
|
|
1103
|
+
error: `${mode} delegation exceeds max tasks (${MAX_DELEGATION_TASKS})`,
|
|
1104
|
+
};
|
|
836
1105
|
}
|
|
837
1106
|
const tasks: NormalizedDelegationTask[] = [];
|
|
838
1107
|
for (let i = 0; i < sourceTasks.length; i++) {
|
|
839
1108
|
const normalized = normalizeDelegationTask(sourceTasks[i], baseModel);
|
|
840
|
-
if (!normalized)
|
|
1109
|
+
if (!normalized)
|
|
1110
|
+
return { error: `${mode} delegation task #${i + 1} is invalid` };
|
|
841
1111
|
tasks.push(normalized);
|
|
842
1112
|
}
|
|
843
1113
|
return {
|
|
@@ -849,14 +1119,22 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
849
1119
|
};
|
|
850
1120
|
}
|
|
851
1121
|
|
|
852
|
-
function renderDelegationTaskTitle(
|
|
1122
|
+
function renderDelegationTaskTitle(
|
|
1123
|
+
mode: DelegationMode,
|
|
1124
|
+
task: NormalizedDelegationTask,
|
|
1125
|
+
index: number,
|
|
1126
|
+
total: number,
|
|
1127
|
+
): string {
|
|
853
1128
|
if (task.label) return task.label;
|
|
854
1129
|
if (mode === 'chain') return `step ${index + 1}/${total}`;
|
|
855
1130
|
if (mode === 'parallel') return `task ${index + 1}/${total}`;
|
|
856
1131
|
return 'task';
|
|
857
1132
|
}
|
|
858
1133
|
|
|
859
|
-
function interpolateChainPrompt(
|
|
1134
|
+
function interpolateChainPrompt(
|
|
1135
|
+
prompt: string,
|
|
1136
|
+
previousResult: string,
|
|
1137
|
+
): string {
|
|
860
1138
|
if (!prompt.includes('{previous}')) return prompt;
|
|
861
1139
|
const replacement = previousResult.trim() || '(no previous output)';
|
|
862
1140
|
return prompt.replace(/\{previous\}/g, replacement);
|
|
@@ -866,7 +1144,9 @@ function sleep(ms: number): Promise<void> {
|
|
|
866
1144
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
867
1145
|
}
|
|
868
1146
|
|
|
869
|
-
async function runDelegationTaskWithRetry(
|
|
1147
|
+
async function runDelegationTaskWithRetry(
|
|
1148
|
+
input: DelegationTaskRunInput,
|
|
1149
|
+
): Promise<DelegationRunResult> {
|
|
870
1150
|
const {
|
|
871
1151
|
parentSessionId,
|
|
872
1152
|
childDepth,
|
|
@@ -879,7 +1159,9 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
879
1159
|
} = input;
|
|
880
1160
|
const allowedTools = resolveSubagentAllowedTools(childDepth);
|
|
881
1161
|
const canDelegate = allowedTools.includes('delegate');
|
|
882
|
-
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
1162
|
+
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
1163
|
+
? PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS
|
|
1164
|
+
: 1;
|
|
883
1165
|
let attempt = 0;
|
|
884
1166
|
let delayMs = PROACTIVE_AUTO_RETRY_BASE_DELAY_MS;
|
|
885
1167
|
let lastError = 'Delegation failed with unknown error';
|
|
@@ -898,7 +1180,14 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
898
1180
|
const output = await runAgent(
|
|
899
1181
|
sessionId,
|
|
900
1182
|
[
|
|
901
|
-
{
|
|
1183
|
+
{
|
|
1184
|
+
role: 'system',
|
|
1185
|
+
content: buildSubagentSystemPrompt({
|
|
1186
|
+
depth: childDepth,
|
|
1187
|
+
canDelegate,
|
|
1188
|
+
mode,
|
|
1189
|
+
}),
|
|
1190
|
+
},
|
|
902
1191
|
{ role: 'user', content: task.prompt },
|
|
903
1192
|
],
|
|
904
1193
|
chatbotId,
|
|
@@ -931,11 +1220,19 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
931
1220
|
lastError = errorText;
|
|
932
1221
|
lastStatus = inferDelegationStatus(errorText);
|
|
933
1222
|
const classification = classifyDelegationError(errorText);
|
|
934
|
-
const shouldRetry =
|
|
1223
|
+
const shouldRetry =
|
|
1224
|
+
classification === 'transient' && attempt < maxAttempts;
|
|
935
1225
|
if (!shouldRetry) break;
|
|
936
1226
|
|
|
937
1227
|
logger.warn(
|
|
938
|
-
{
|
|
1228
|
+
{
|
|
1229
|
+
parentSessionId,
|
|
1230
|
+
sessionId,
|
|
1231
|
+
attempt,
|
|
1232
|
+
maxAttempts,
|
|
1233
|
+
delayMs,
|
|
1234
|
+
errorText,
|
|
1235
|
+
},
|
|
939
1236
|
'Delegation retry scheduled after transient error',
|
|
940
1237
|
);
|
|
941
1238
|
await sleep(delayMs);
|
|
@@ -947,10 +1244,18 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
947
1244
|
lastError = errorText;
|
|
948
1245
|
lastStatus = inferDelegationStatus(errorText);
|
|
949
1246
|
const classification = classifyDelegationError(errorText);
|
|
950
|
-
const shouldRetry =
|
|
1247
|
+
const shouldRetry =
|
|
1248
|
+
classification === 'transient' && attempt < maxAttempts;
|
|
951
1249
|
if (!shouldRetry) break;
|
|
952
1250
|
logger.warn(
|
|
953
|
-
{
|
|
1251
|
+
{
|
|
1252
|
+
parentSessionId,
|
|
1253
|
+
sessionId,
|
|
1254
|
+
attempt,
|
|
1255
|
+
maxAttempts,
|
|
1256
|
+
delayMs,
|
|
1257
|
+
errorText,
|
|
1258
|
+
},
|
|
954
1259
|
'Delegation retry scheduled after transient exception',
|
|
955
1260
|
);
|
|
956
1261
|
await sleep(delayMs);
|
|
@@ -977,19 +1282,32 @@ function formatDelegationCompletion(params: {
|
|
|
977
1282
|
totalDurationMs: number;
|
|
978
1283
|
}): { forUser: string; forLLM: string; artifacts?: ArtifactMetadata[] } {
|
|
979
1284
|
const { mode, label, entries, totalDurationMs } = params;
|
|
980
|
-
const completedCount = entries.filter(
|
|
1285
|
+
const completedCount = entries.filter(
|
|
1286
|
+
(entry) => entry.run.status === 'completed',
|
|
1287
|
+
).length;
|
|
981
1288
|
const failedCount = entries.length - completedCount;
|
|
982
|
-
const overallStatus =
|
|
983
|
-
|
|
1289
|
+
const overallStatus =
|
|
1290
|
+
failedCount === 0
|
|
1291
|
+
? 'completed'
|
|
1292
|
+
: completedCount === 0
|
|
1293
|
+
? 'failed'
|
|
1294
|
+
: 'partial';
|
|
1295
|
+
const heading = label?.trim()
|
|
1296
|
+
? `[Delegate: ${label.trim()}]`
|
|
1297
|
+
: `[Delegate ${mode}]`;
|
|
984
1298
|
|
|
985
1299
|
const userLines = [
|
|
986
1300
|
`${heading} ${overallStatus} (${completedCount}/${entries.length} completed, ${formatDurationMs(totalDurationMs)}).`,
|
|
987
1301
|
];
|
|
988
1302
|
for (const entry of entries) {
|
|
989
1303
|
if (entry.run.status === 'completed') {
|
|
990
|
-
userLines.push(
|
|
1304
|
+
userLines.push(
|
|
1305
|
+
`- ${entry.title}: ${abbreviateForUser(entry.run.result || '')}`,
|
|
1306
|
+
);
|
|
991
1307
|
} else {
|
|
992
|
-
userLines.push(
|
|
1308
|
+
userLines.push(
|
|
1309
|
+
`- ${entry.title}: ${entry.run.status} (${abbreviateForUser(entry.run.error || 'Unknown error')})`,
|
|
1310
|
+
);
|
|
993
1311
|
}
|
|
994
1312
|
}
|
|
995
1313
|
|
|
@@ -1045,7 +1363,9 @@ async function publishDelegationCompletion(params: {
|
|
|
1045
1363
|
forLLM: string;
|
|
1046
1364
|
forUser: string;
|
|
1047
1365
|
artifacts?: ArtifactMetadata[];
|
|
1048
|
-
onProactiveMessage?: (
|
|
1366
|
+
onProactiveMessage?: (
|
|
1367
|
+
message: ProactiveMessagePayload,
|
|
1368
|
+
) => void | Promise<void>;
|
|
1049
1369
|
}): Promise<void> {
|
|
1050
1370
|
const {
|
|
1051
1371
|
parentSessionId,
|
|
@@ -1057,7 +1377,13 @@ async function publishDelegationCompletion(params: {
|
|
|
1057
1377
|
onProactiveMessage,
|
|
1058
1378
|
} = params;
|
|
1059
1379
|
|
|
1060
|
-
storeMessage(
|
|
1380
|
+
memoryService.storeMessage({
|
|
1381
|
+
sessionId: parentSessionId,
|
|
1382
|
+
userId: 'assistant',
|
|
1383
|
+
username: null,
|
|
1384
|
+
role: 'assistant',
|
|
1385
|
+
content: forLLM,
|
|
1386
|
+
});
|
|
1061
1387
|
appendSessionTranscript(agentId, {
|
|
1062
1388
|
sessionId: parentSessionId,
|
|
1063
1389
|
channelId,
|
|
@@ -1072,7 +1398,11 @@ async function publishDelegationCompletion(params: {
|
|
|
1072
1398
|
return;
|
|
1073
1399
|
}
|
|
1074
1400
|
logger.info(
|
|
1075
|
-
{
|
|
1401
|
+
{
|
|
1402
|
+
parentSessionId,
|
|
1403
|
+
message: forUser,
|
|
1404
|
+
artifactCount: artifacts?.length || 0,
|
|
1405
|
+
},
|
|
1076
1406
|
'Delegation completion (no proactive channel callback)',
|
|
1077
1407
|
);
|
|
1078
1408
|
}
|
|
@@ -1084,7 +1414,9 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1084
1414
|
chatbotId: string;
|
|
1085
1415
|
enableRag: boolean;
|
|
1086
1416
|
agentId: string;
|
|
1087
|
-
onProactiveMessage?: (
|
|
1417
|
+
onProactiveMessage?: (
|
|
1418
|
+
message: ProactiveMessagePayload,
|
|
1419
|
+
) => void | Promise<void>;
|
|
1088
1420
|
parentDepth: number;
|
|
1089
1421
|
}): void {
|
|
1090
1422
|
const {
|
|
@@ -1099,7 +1431,10 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1099
1431
|
} = params;
|
|
1100
1432
|
const childDepth = parentDepth + 1;
|
|
1101
1433
|
if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
|
|
1102
|
-
logger.info(
|
|
1434
|
+
logger.info(
|
|
1435
|
+
{ parentSessionId, childDepth, maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH },
|
|
1436
|
+
'Delegation skipped — depth limit reached',
|
|
1437
|
+
);
|
|
1103
1438
|
return;
|
|
1104
1439
|
}
|
|
1105
1440
|
|
|
@@ -1111,22 +1446,29 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1111
1446
|
const entries: DelegationCompletionEntry[] = [];
|
|
1112
1447
|
|
|
1113
1448
|
if (plan.mode === 'parallel') {
|
|
1114
|
-
const runs = await Promise.all(
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1449
|
+
const runs = await Promise.all(
|
|
1450
|
+
plan.tasks.map(async (task, index) => {
|
|
1451
|
+
const run = await runDelegationTaskWithRetry({
|
|
1452
|
+
parentSessionId,
|
|
1453
|
+
childDepth,
|
|
1454
|
+
channelId,
|
|
1455
|
+
chatbotId,
|
|
1456
|
+
enableRag,
|
|
1457
|
+
agentId,
|
|
1458
|
+
mode: plan.mode,
|
|
1459
|
+
task,
|
|
1460
|
+
});
|
|
1461
|
+
return {
|
|
1462
|
+
title: renderDelegationTaskTitle(
|
|
1463
|
+
plan.mode,
|
|
1464
|
+
task,
|
|
1465
|
+
index,
|
|
1466
|
+
plan.tasks.length,
|
|
1467
|
+
),
|
|
1468
|
+
run,
|
|
1469
|
+
} as DelegationCompletionEntry;
|
|
1470
|
+
}),
|
|
1471
|
+
);
|
|
1130
1472
|
entries.push(...runs);
|
|
1131
1473
|
} else if (plan.mode === 'chain') {
|
|
1132
1474
|
let previousResult = '';
|
|
@@ -1146,7 +1488,12 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1146
1488
|
},
|
|
1147
1489
|
});
|
|
1148
1490
|
entries.push({
|
|
1149
|
-
title: renderDelegationTaskTitle(
|
|
1491
|
+
title: renderDelegationTaskTitle(
|
|
1492
|
+
plan.mode,
|
|
1493
|
+
task,
|
|
1494
|
+
i,
|
|
1495
|
+
plan.tasks.length,
|
|
1496
|
+
),
|
|
1150
1497
|
run,
|
|
1151
1498
|
});
|
|
1152
1499
|
if (run.status !== 'completed') break;
|
|
@@ -1171,7 +1518,10 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1171
1518
|
}
|
|
1172
1519
|
|
|
1173
1520
|
if (entries.length === 0) {
|
|
1174
|
-
logger.warn(
|
|
1521
|
+
logger.warn(
|
|
1522
|
+
{ parentSessionId, mode: plan.mode },
|
|
1523
|
+
'Delegation produced no entries',
|
|
1524
|
+
);
|
|
1175
1525
|
return;
|
|
1176
1526
|
}
|
|
1177
1527
|
|
|
@@ -1194,10 +1544,16 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1194
1544
|
});
|
|
1195
1545
|
}
|
|
1196
1546
|
|
|
1197
|
-
export async function handleGatewayMessage(
|
|
1547
|
+
export async function handleGatewayMessage(
|
|
1548
|
+
req: GatewayChatRequest,
|
|
1549
|
+
): Promise<GatewayChatResult> {
|
|
1198
1550
|
const startedAt = Date.now();
|
|
1199
1551
|
const runId = makeAuditRunId('turn');
|
|
1200
|
-
const session = getOrCreateSession(
|
|
1552
|
+
const session = memoryService.getOrCreateSession(
|
|
1553
|
+
req.sessionId,
|
|
1554
|
+
req.guildId,
|
|
1555
|
+
req.channelId,
|
|
1556
|
+
);
|
|
1201
1557
|
const chatbotId = req.chatbotId ?? session.chatbot_id ?? HYBRIDAI_CHATBOT_ID;
|
|
1202
1558
|
const enableRag = req.enableRag ?? session.enable_rag === 1;
|
|
1203
1559
|
const model = req.model ?? session.model ?? HYBRIDAI_MODEL;
|
|
@@ -1229,7 +1585,8 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1229
1585
|
});
|
|
1230
1586
|
|
|
1231
1587
|
if (!chatbotId) {
|
|
1232
|
-
const error =
|
|
1588
|
+
const error =
|
|
1589
|
+
'No chatbot configured. Set `hybridai.defaultChatbotId` in config.json or select a bot for this session.';
|
|
1233
1590
|
recordAuditEvent({
|
|
1234
1591
|
sessionId: req.sessionId,
|
|
1235
1592
|
runId,
|
|
@@ -1277,10 +1634,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1277
1634
|
if (isVersionOnlyQuestion(req.content)) {
|
|
1278
1635
|
const resultText = `HybridClaw v${APP_VERSION}`;
|
|
1279
1636
|
recordSuccessfulTurn({
|
|
1280
|
-
sessionId: req.sessionId,
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1637
|
+
sessionId: req.sessionId,
|
|
1638
|
+
agentId,
|
|
1639
|
+
chatbotId,
|
|
1640
|
+
enableRag,
|
|
1641
|
+
model,
|
|
1642
|
+
channelId: req.channelId,
|
|
1643
|
+
runId,
|
|
1644
|
+
turnIndex,
|
|
1645
|
+
userId: req.userId,
|
|
1646
|
+
username: req.username,
|
|
1647
|
+
userContent: req.content,
|
|
1648
|
+
resultText,
|
|
1649
|
+
toolCallCount: 0,
|
|
1650
|
+
startedAt,
|
|
1284
1651
|
});
|
|
1285
1652
|
return {
|
|
1286
1653
|
status: 'success',
|
|
@@ -1289,10 +1656,48 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1289
1656
|
};
|
|
1290
1657
|
}
|
|
1291
1658
|
|
|
1292
|
-
const history = getConversationHistory(
|
|
1659
|
+
const history = memoryService.getConversationHistory(
|
|
1660
|
+
req.sessionId,
|
|
1661
|
+
MAX_HISTORY_MESSAGES,
|
|
1662
|
+
);
|
|
1663
|
+
let canonicalContext: CanonicalSessionContext = {
|
|
1664
|
+
summary: null,
|
|
1665
|
+
recent_messages: [],
|
|
1666
|
+
};
|
|
1667
|
+
if (req.userId.trim()) {
|
|
1668
|
+
try {
|
|
1669
|
+
canonicalContext = memoryService.getCanonicalContext({
|
|
1670
|
+
agentId,
|
|
1671
|
+
userId: req.userId,
|
|
1672
|
+
windowSize: 12,
|
|
1673
|
+
excludeSessionId: req.sessionId,
|
|
1674
|
+
});
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
logger.debug(
|
|
1677
|
+
{ sessionId: req.sessionId, userId: req.userId, err },
|
|
1678
|
+
'Failed to load canonical session context',
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const canonicalPromptSummary = formatCanonicalContextPrompt({
|
|
1683
|
+
summary: canonicalContext.summary,
|
|
1684
|
+
recentMessages: canonicalContext.recent_messages,
|
|
1685
|
+
});
|
|
1686
|
+
const memoryContext = memoryService.buildPromptMemoryContext({
|
|
1687
|
+
session,
|
|
1688
|
+
query: req.content,
|
|
1689
|
+
});
|
|
1690
|
+
const mergedSessionSummary =
|
|
1691
|
+
[canonicalPromptSummary, memoryContext.promptSummary]
|
|
1692
|
+
.filter(
|
|
1693
|
+
(value): value is string =>
|
|
1694
|
+
typeof value === 'string' && value.trim().length > 0,
|
|
1695
|
+
)
|
|
1696
|
+
.join('\n\n')
|
|
1697
|
+
.trim() || null;
|
|
1293
1698
|
const { messages, skills, historyStats } = buildConversationContext({
|
|
1294
1699
|
agentId,
|
|
1295
|
-
sessionSummary:
|
|
1700
|
+
sessionSummary: mergedSessionSummary,
|
|
1296
1701
|
history,
|
|
1297
1702
|
runtimeInfo: {
|
|
1298
1703
|
chatbotId,
|
|
@@ -1302,7 +1707,8 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1302
1707
|
guildId: req.guildId,
|
|
1303
1708
|
},
|
|
1304
1709
|
});
|
|
1305
|
-
const historyStart =
|
|
1710
|
+
const historyStart =
|
|
1711
|
+
messages.length > 0 && messages[0].role === 'system' ? 1 : 0;
|
|
1306
1712
|
recordAuditEvent({
|
|
1307
1713
|
sessionId: req.sessionId,
|
|
1308
1714
|
runId,
|
|
@@ -1319,7 +1725,11 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1319
1725
|
historyMaxMessageChars: historyStats.maxMessageChars,
|
|
1320
1726
|
perMessageTruncatedCount: historyStats.perMessageTruncatedCount,
|
|
1321
1727
|
middleCompressionApplied: historyStats.middleCompressionApplied,
|
|
1322
|
-
historyEstimatedTokens: estimateTokenCountFromMessages(
|
|
1728
|
+
historyEstimatedTokens: estimateTokenCountFromMessages(
|
|
1729
|
+
messages.slice(historyStart),
|
|
1730
|
+
),
|
|
1731
|
+
canonicalSummaryIncluded: Boolean(canonicalPromptSummary),
|
|
1732
|
+
canonicalRecentMessagesIncluded: canonicalContext.recent_messages.length,
|
|
1323
1733
|
},
|
|
1324
1734
|
});
|
|
1325
1735
|
const mediaPolicy = resolveMediaToolPolicy(req.content, media);
|
|
@@ -1361,12 +1771,22 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1361
1771
|
req.abortSignal,
|
|
1362
1772
|
media,
|
|
1363
1773
|
);
|
|
1774
|
+
const effectiveUserContent =
|
|
1775
|
+
typeof output.effectiveUserPrompt === 'string' &&
|
|
1776
|
+
output.effectiveUserPrompt.trim()
|
|
1777
|
+
? output.effectiveUserPrompt.trim()
|
|
1778
|
+
: req.content;
|
|
1364
1779
|
const toolExecutions = output.toolExecutions || [];
|
|
1365
1780
|
emitToolExecutionAuditEvents({
|
|
1366
1781
|
sessionId: req.sessionId,
|
|
1367
1782
|
runId,
|
|
1368
1783
|
toolExecutions,
|
|
1369
1784
|
});
|
|
1785
|
+
const usagePayload = buildTokenUsageAuditPayload(
|
|
1786
|
+
messages,
|
|
1787
|
+
output.result,
|
|
1788
|
+
output.tokenUsage,
|
|
1789
|
+
);
|
|
1370
1790
|
recordAuditEvent({
|
|
1371
1791
|
sessionId: req.sessionId,
|
|
1372
1792
|
runId,
|
|
@@ -1376,9 +1796,19 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1376
1796
|
model,
|
|
1377
1797
|
durationMs: Date.now() - startedAt,
|
|
1378
1798
|
toolCallCount: toolExecutions.length,
|
|
1379
|
-
...
|
|
1799
|
+
...usagePayload,
|
|
1380
1800
|
},
|
|
1381
1801
|
});
|
|
1802
|
+
recordUsageEvent({
|
|
1803
|
+
sessionId: req.sessionId,
|
|
1804
|
+
agentId,
|
|
1805
|
+
model,
|
|
1806
|
+
inputTokens: firstNumber([usagePayload.promptTokens]) || 0,
|
|
1807
|
+
outputTokens: firstNumber([usagePayload.completionTokens]) || 0,
|
|
1808
|
+
totalTokens: firstNumber([usagePayload.totalTokens]) || 0,
|
|
1809
|
+
toolCalls: toolExecutions.length,
|
|
1810
|
+
costUsd: extractUsageCostUsd(output.tokenUsage),
|
|
1811
|
+
});
|
|
1382
1812
|
|
|
1383
1813
|
const parentDepth = extractDelegationDepth(req.sessionId);
|
|
1384
1814
|
let acceptedDelegations = 0;
|
|
@@ -1387,7 +1817,11 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1387
1817
|
const normalized = normalizeDelegationEffect(effect, model);
|
|
1388
1818
|
if (!normalized.plan) {
|
|
1389
1819
|
logger.warn(
|
|
1390
|
-
{
|
|
1820
|
+
{
|
|
1821
|
+
sessionId: req.sessionId,
|
|
1822
|
+
error: normalized.error || 'unknown',
|
|
1823
|
+
effect,
|
|
1824
|
+
},
|
|
1391
1825
|
'Delegation skipped — invalid payload',
|
|
1392
1826
|
);
|
|
1393
1827
|
return;
|
|
@@ -1396,14 +1830,21 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1396
1830
|
const childDepth = parentDepth + 1;
|
|
1397
1831
|
if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
|
|
1398
1832
|
logger.info(
|
|
1399
|
-
{
|
|
1833
|
+
{
|
|
1834
|
+
sessionId: req.sessionId,
|
|
1835
|
+
childDepth,
|
|
1836
|
+
maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH,
|
|
1837
|
+
},
|
|
1400
1838
|
'Delegation skipped — depth limit reached',
|
|
1401
1839
|
);
|
|
1402
1840
|
return;
|
|
1403
1841
|
}
|
|
1404
1842
|
|
|
1405
1843
|
const requestedRuns = normalized.plan.tasks.length;
|
|
1406
|
-
if (
|
|
1844
|
+
if (
|
|
1845
|
+
acceptedDelegations + requestedRuns >
|
|
1846
|
+
PROACTIVE_DELEGATION_MAX_PER_TURN
|
|
1847
|
+
) {
|
|
1407
1848
|
logger.info(
|
|
1408
1849
|
{
|
|
1409
1850
|
sessionId: req.sessionId,
|
|
@@ -1477,10 +1918,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1477
1918
|
|
|
1478
1919
|
const resultText = output.result || 'No response from agent.';
|
|
1479
1920
|
recordSuccessfulTurn({
|
|
1480
|
-
sessionId: req.sessionId,
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1921
|
+
sessionId: req.sessionId,
|
|
1922
|
+
agentId,
|
|
1923
|
+
chatbotId,
|
|
1924
|
+
enableRag,
|
|
1925
|
+
model,
|
|
1926
|
+
channelId: req.channelId,
|
|
1927
|
+
runId,
|
|
1928
|
+
turnIndex,
|
|
1929
|
+
userId: req.userId,
|
|
1930
|
+
username: req.username,
|
|
1931
|
+
userContent: effectiveUserContent,
|
|
1932
|
+
resultText,
|
|
1933
|
+
toolCallCount: toolExecutions.length,
|
|
1934
|
+
startedAt,
|
|
1484
1935
|
});
|
|
1485
1936
|
|
|
1486
1937
|
return {
|
|
@@ -1490,11 +1941,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1490
1941
|
artifacts: output.artifacts,
|
|
1491
1942
|
toolExecutions,
|
|
1492
1943
|
tokenUsage: output.tokenUsage,
|
|
1944
|
+
effectiveUserPrompt: output.effectiveUserPrompt,
|
|
1493
1945
|
};
|
|
1494
1946
|
} catch (err) {
|
|
1495
1947
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1496
|
-
logAudit(
|
|
1497
|
-
|
|
1948
|
+
logAudit(
|
|
1949
|
+
'error',
|
|
1950
|
+
req.sessionId,
|
|
1951
|
+
{ error: errorMsg },
|
|
1952
|
+
Date.now() - startedAt,
|
|
1953
|
+
);
|
|
1954
|
+
logger.error(
|
|
1955
|
+
{ sessionId: req.sessionId, err },
|
|
1956
|
+
'Gateway message handling failed',
|
|
1957
|
+
);
|
|
1498
1958
|
recordAuditEvent({
|
|
1499
1959
|
sessionId: req.sessionId,
|
|
1500
1960
|
runId,
|
|
@@ -1545,8 +2005,13 @@ export async function runGatewayScheduledTask(
|
|
|
1545
2005
|
taskId: number,
|
|
1546
2006
|
onResult: (result: ProactiveMessagePayload) => Promise<void>,
|
|
1547
2007
|
onError: (error: unknown) => void,
|
|
2008
|
+
runKey?: string,
|
|
1548
2009
|
): Promise<void> {
|
|
1549
|
-
const session = getOrCreateSession(
|
|
2010
|
+
const session = memoryService.getOrCreateSession(
|
|
2011
|
+
origSessionId,
|
|
2012
|
+
null,
|
|
2013
|
+
channelId,
|
|
2014
|
+
);
|
|
1550
2015
|
const chatbotId = session.chatbot_id || HYBRIDAI_CHATBOT_ID;
|
|
1551
2016
|
if (!chatbotId) return;
|
|
1552
2017
|
const model = session.model || HYBRIDAI_MODEL;
|
|
@@ -1559,14 +2024,21 @@ export async function runGatewayScheduledTask(
|
|
|
1559
2024
|
chatbotId,
|
|
1560
2025
|
model,
|
|
1561
2026
|
agentId,
|
|
2027
|
+
sessionKey: runKey,
|
|
1562
2028
|
onResult,
|
|
1563
2029
|
onError,
|
|
1564
2030
|
});
|
|
1565
2031
|
}
|
|
1566
2032
|
|
|
1567
|
-
export async function handleGatewayCommand(
|
|
2033
|
+
export async function handleGatewayCommand(
|
|
2034
|
+
req: GatewayCommandRequest,
|
|
2035
|
+
): Promise<GatewayCommandResult> {
|
|
1568
2036
|
const cmd = (req.args[0] || '').toLowerCase();
|
|
1569
|
-
const session = getOrCreateSession(
|
|
2037
|
+
const session = memoryService.getOrCreateSession(
|
|
2038
|
+
req.sessionId,
|
|
2039
|
+
req.guildId,
|
|
2040
|
+
req.channelId,
|
|
2041
|
+
);
|
|
1570
2042
|
|
|
1571
2043
|
switch (cmd) {
|
|
1572
2044
|
case 'help': {
|
|
@@ -1578,11 +2050,19 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1578
2050
|
'`model set <name>` — Set model for this session',
|
|
1579
2051
|
'`model info` — Show current model',
|
|
1580
2052
|
'`rag [on|off]` — Toggle or set RAG mode',
|
|
2053
|
+
'`channel mode [off|mention|free]` — Set or inspect this Discord channel response mode',
|
|
2054
|
+
'`channel policy [open|allowlist|disabled]` — Set or inspect guild channel policy',
|
|
1581
2055
|
'`ralph [on|off|set <n>|info]` — Configure Ralph loop (0 off, -1 unlimited)',
|
|
1582
2056
|
'`clear` — Clear session history',
|
|
1583
2057
|
'`/status` — Show runtime status (Discord slash command, private to caller)',
|
|
2058
|
+
'`/channel-mode <off|mention|free>` — Set this Discord channel response mode',
|
|
2059
|
+
'`/channel-policy <open|allowlist|disabled>` — Set Discord guild channel policy',
|
|
1584
2060
|
'`sessions` — List active sessions',
|
|
1585
|
-
'`
|
|
2061
|
+
'`usage [summary|daily|monthly|model [daily|monthly] [agentId]]` — Usage/cost aggregates',
|
|
2062
|
+
'`export session [sessionId]` — Export session JSONL snapshot for debugging',
|
|
2063
|
+
'`schedule add "<cron>" <prompt>` — Add cron scheduled task',
|
|
2064
|
+
'`schedule add at "<ISO time>" <prompt>` — Add one-shot task',
|
|
2065
|
+
'`schedule add every <ms> <prompt>` — Add interval task',
|
|
1586
2066
|
'`schedule list` — List scheduled tasks',
|
|
1587
2067
|
'`schedule remove <id>` — Remove a task',
|
|
1588
2068
|
'`schedule toggle <id>` — Enable/disable a task',
|
|
@@ -1596,30 +2076,41 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1596
2076
|
try {
|
|
1597
2077
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1598
2078
|
if (bots.length === 0) return plainCommand('No bots available.');
|
|
1599
|
-
const list = bots
|
|
1600
|
-
|
|
1601
|
-
|
|
2079
|
+
const list = bots
|
|
2080
|
+
.map(
|
|
2081
|
+
(b) =>
|
|
2082
|
+
`• ${b.name} (${b.id})${b.description ? ` — ${b.description}` : ''}`,
|
|
2083
|
+
)
|
|
2084
|
+
.join('\n');
|
|
1602
2085
|
return infoCommand('Available Bots', list);
|
|
1603
2086
|
} catch (err) {
|
|
1604
|
-
return badCommand(
|
|
2087
|
+
return badCommand(
|
|
2088
|
+
'Error',
|
|
2089
|
+
`Failed to fetch bots: ${err instanceof Error ? err.message : String(err)}`,
|
|
2090
|
+
);
|
|
1605
2091
|
}
|
|
1606
2092
|
}
|
|
1607
2093
|
|
|
1608
2094
|
if (sub === 'set') {
|
|
1609
2095
|
const requested = req.args.slice(2).join(' ').trim();
|
|
1610
|
-
if (!requested)
|
|
2096
|
+
if (!requested)
|
|
2097
|
+
return badCommand('Usage', 'Usage: `bot set <id|name>`');
|
|
1611
2098
|
let resolvedBotId = requested;
|
|
1612
2099
|
try {
|
|
1613
2100
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1614
|
-
const matched = bots.find(
|
|
1615
|
-
b
|
|
2101
|
+
const matched = bots.find(
|
|
2102
|
+
(b) =>
|
|
2103
|
+
b.id === requested ||
|
|
2104
|
+
b.name.toLowerCase() === requested.toLowerCase(),
|
|
1616
2105
|
);
|
|
1617
2106
|
if (matched) resolvedBotId = matched.id;
|
|
1618
2107
|
} catch {
|
|
1619
2108
|
// keep user-supplied value when lookup fails
|
|
1620
2109
|
}
|
|
1621
2110
|
updateSessionChatbot(session.id, resolvedBotId);
|
|
1622
|
-
return plainCommand(
|
|
2111
|
+
return plainCommand(
|
|
2112
|
+
`Chatbot set to \`${resolvedBotId}\` for this session.`,
|
|
2113
|
+
);
|
|
1623
2114
|
}
|
|
1624
2115
|
|
|
1625
2116
|
if (sub === 'info') {
|
|
@@ -1634,7 +2125,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1634
2125
|
}
|
|
1635
2126
|
const model = session.model || HYBRIDAI_MODEL;
|
|
1636
2127
|
const ragStatus = session.enable_rag ? 'Enabled' : 'Disabled';
|
|
1637
|
-
return infoCommand(
|
|
2128
|
+
return infoCommand(
|
|
2129
|
+
'Bot Info',
|
|
2130
|
+
`Chatbot: ${botLabel}\nModel: ${model}\nRAG: ${ragStatus}`,
|
|
2131
|
+
);
|
|
1638
2132
|
}
|
|
1639
2133
|
|
|
1640
2134
|
return badCommand('Usage', 'Usage: `bot list|set <id|name>|info`');
|
|
@@ -1645,7 +2139,7 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1645
2139
|
if (sub === 'list') {
|
|
1646
2140
|
const current = session.model || HYBRIDAI_MODEL;
|
|
1647
2141
|
const list = HYBRIDAI_MODELS.map((m) =>
|
|
1648
|
-
m === current ? `${m} (current)` : m
|
|
2142
|
+
m === current ? `${m} (current)` : m,
|
|
1649
2143
|
).join('\n');
|
|
1650
2144
|
return infoCommand('Available Models', list);
|
|
1651
2145
|
}
|
|
@@ -1653,8 +2147,14 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1653
2147
|
if (sub === 'set') {
|
|
1654
2148
|
const modelName = req.args[2];
|
|
1655
2149
|
if (!modelName) return badCommand('Usage', 'Usage: `model set <name>`');
|
|
1656
|
-
if (
|
|
1657
|
-
|
|
2150
|
+
if (
|
|
2151
|
+
HYBRIDAI_MODELS.length > 0 &&
|
|
2152
|
+
!HYBRIDAI_MODELS.includes(modelName)
|
|
2153
|
+
) {
|
|
2154
|
+
return badCommand(
|
|
2155
|
+
'Unknown Model',
|
|
2156
|
+
`\`${modelName}\` is not in the available models list.`,
|
|
2157
|
+
);
|
|
1658
2158
|
}
|
|
1659
2159
|
updateSessionModel(session.id, modelName);
|
|
1660
2160
|
return plainCommand(`Model set to \`${modelName}\` for this session.`);
|
|
@@ -1662,7 +2162,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1662
2162
|
|
|
1663
2163
|
if (sub === 'info') {
|
|
1664
2164
|
const current = session.model || HYBRIDAI_MODEL;
|
|
1665
|
-
return infoCommand(
|
|
2165
|
+
return infoCommand(
|
|
2166
|
+
'Model Info',
|
|
2167
|
+
`Current model: ${current}\nDefault model: ${HYBRIDAI_MODEL}`,
|
|
2168
|
+
);
|
|
1666
2169
|
}
|
|
1667
2170
|
|
|
1668
2171
|
return badCommand('Usage', 'Usage: `model list|set <name>|info`');
|
|
@@ -1672,20 +2175,100 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1672
2175
|
const sub = req.args[1]?.toLowerCase();
|
|
1673
2176
|
if (sub === 'on' || sub === 'off') {
|
|
1674
2177
|
updateSessionRag(session.id, sub === 'on');
|
|
1675
|
-
return plainCommand(
|
|
2178
|
+
return plainCommand(
|
|
2179
|
+
`RAG ${sub === 'on' ? 'enabled' : 'disabled'} for this session.`,
|
|
2180
|
+
);
|
|
1676
2181
|
}
|
|
1677
2182
|
if (!sub) {
|
|
1678
2183
|
const nextEnabled = session.enable_rag === 0;
|
|
1679
2184
|
updateSessionRag(session.id, nextEnabled);
|
|
1680
|
-
return plainCommand(
|
|
2185
|
+
return plainCommand(
|
|
2186
|
+
`RAG ${nextEnabled ? 'enabled' : 'disabled'} for this session.`,
|
|
2187
|
+
);
|
|
1681
2188
|
}
|
|
1682
2189
|
return badCommand('Usage', 'Usage: `rag [on|off]`');
|
|
1683
2190
|
}
|
|
1684
2191
|
|
|
2192
|
+
case 'channel': {
|
|
2193
|
+
const sub = (req.args[1] || '').toLowerCase();
|
|
2194
|
+
if (sub === 'mode' || !sub) {
|
|
2195
|
+
const guildId = req.guildId;
|
|
2196
|
+
if (!guildId) {
|
|
2197
|
+
return badCommand(
|
|
2198
|
+
'Guild Only',
|
|
2199
|
+
'`channel mode` is only available in Discord guild channels.',
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
const requestedMode = (req.args[sub ? 2 : 1] || '').toLowerCase();
|
|
2203
|
+
if (!requestedMode) {
|
|
2204
|
+
const currentMode = resolveGuildChannelMode(guildId, req.channelId);
|
|
2205
|
+
return infoCommand(
|
|
2206
|
+
'Channel Mode',
|
|
2207
|
+
[
|
|
2208
|
+
`Current mode: \`${currentMode}\``,
|
|
2209
|
+
`Group policy: \`${DISCORD_GROUP_POLICY}\``,
|
|
2210
|
+
`Config path: \`discord.guilds.${guildId}.channels.${req.channelId}.mode\``,
|
|
2211
|
+
'Usage: `channel mode off|mention|free`',
|
|
2212
|
+
].join('\n'),
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
if (!DISCORD_CHANNEL_MODE_VALUES.has(requestedMode)) {
|
|
2216
|
+
return badCommand('Usage', 'Usage: `channel mode off|mention|free`');
|
|
2217
|
+
}
|
|
2218
|
+
const mode = requestedMode as 'off' | 'mention' | 'free';
|
|
2219
|
+
updateRuntimeConfig((draft) => {
|
|
2220
|
+
const guild = draft.discord.guilds[guildId] ?? {
|
|
2221
|
+
defaultMode: 'mention',
|
|
2222
|
+
channels: {},
|
|
2223
|
+
};
|
|
2224
|
+
guild.channels[req.channelId] = { mode };
|
|
2225
|
+
draft.discord.guilds[guildId] = guild;
|
|
2226
|
+
});
|
|
2227
|
+
return plainCommand(
|
|
2228
|
+
`Set channel mode to \`${mode}\` for this channel. (Policy: \`${DISCORD_GROUP_POLICY}\`)`,
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (sub === 'policy') {
|
|
2233
|
+
const requestedPolicy = (req.args[2] || '').toLowerCase();
|
|
2234
|
+
if (!requestedPolicy) {
|
|
2235
|
+
return infoCommand(
|
|
2236
|
+
'Channel Policy',
|
|
2237
|
+
[
|
|
2238
|
+
`Current policy: \`${DISCORD_GROUP_POLICY}\``,
|
|
2239
|
+
'Policies:',
|
|
2240
|
+
'• `open` — all guild channels are active unless a per-channel mode overrides',
|
|
2241
|
+
'• `allowlist` — only channels listed under `discord.guilds.<guild>.channels` are active',
|
|
2242
|
+
'• `disabled` — all guild channels are disabled',
|
|
2243
|
+
'Usage: `channel policy open|allowlist|disabled`',
|
|
2244
|
+
].join('\n'),
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
if (!DISCORD_GROUP_POLICY_VALUES.has(requestedPolicy)) {
|
|
2248
|
+
return badCommand(
|
|
2249
|
+
'Usage',
|
|
2250
|
+
'Usage: `channel policy open|allowlist|disabled`',
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
const policy = requestedPolicy as 'open' | 'allowlist' | 'disabled';
|
|
2254
|
+
updateRuntimeConfig((draft) => {
|
|
2255
|
+
draft.discord.groupPolicy = policy;
|
|
2256
|
+
});
|
|
2257
|
+
return plainCommand(`Discord group policy set to \`${policy}\`.`);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
return badCommand(
|
|
2261
|
+
'Usage',
|
|
2262
|
+
'Usage: `channel mode [off|mention|free]` or `channel policy [open|allowlist|disabled]`',
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
1685
2266
|
case 'ralph': {
|
|
1686
2267
|
const sub = (req.args[1] || '').toLowerCase();
|
|
1687
2268
|
if (!sub || sub === 'info' || sub === 'status') {
|
|
1688
|
-
const current = normalizeRalphIterations(
|
|
2269
|
+
const current = normalizeRalphIterations(
|
|
2270
|
+
PROACTIVE_RALPH_MAX_ITERATIONS,
|
|
2271
|
+
);
|
|
1689
2272
|
return infoCommand(
|
|
1690
2273
|
'Ralph Loop',
|
|
1691
2274
|
[
|
|
@@ -1698,19 +2281,31 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1698
2281
|
|
|
1699
2282
|
let nextValue: number | null = null;
|
|
1700
2283
|
if (sub === 'on') {
|
|
1701
|
-
nextValue =
|
|
2284
|
+
nextValue =
|
|
2285
|
+
PROACTIVE_RALPH_MAX_ITERATIONS === 0
|
|
2286
|
+
? 3
|
|
2287
|
+
: PROACTIVE_RALPH_MAX_ITERATIONS;
|
|
1702
2288
|
} else if (sub === 'off') {
|
|
1703
2289
|
nextValue = 0;
|
|
1704
2290
|
} else if (sub === 'set') {
|
|
1705
2291
|
if (req.args[2] == null) {
|
|
1706
|
-
return badCommand(
|
|
2292
|
+
return badCommand(
|
|
2293
|
+
'Usage',
|
|
2294
|
+
'Usage: `ralph set <n>` (0=off, -1=unlimited, 1-64=extra iterations)',
|
|
2295
|
+
);
|
|
1707
2296
|
}
|
|
1708
2297
|
const parsed = Number.parseInt(req.args[2], 10);
|
|
1709
2298
|
if (Number.isNaN(parsed)) {
|
|
1710
|
-
return badCommand(
|
|
2299
|
+
return badCommand(
|
|
2300
|
+
'Usage',
|
|
2301
|
+
'Usage: `ralph set <n>` where n is an integer',
|
|
2302
|
+
);
|
|
1711
2303
|
}
|
|
1712
2304
|
if (parsed < -1 || parsed > MAX_RALPH_ITERATIONS) {
|
|
1713
|
-
return badCommand(
|
|
2305
|
+
return badCommand(
|
|
2306
|
+
'Range',
|
|
2307
|
+
`Ralph iterations must be between -1 and ${MAX_RALPH_ITERATIONS}.`,
|
|
2308
|
+
);
|
|
1714
2309
|
}
|
|
1715
2310
|
nextValue = parsed;
|
|
1716
2311
|
} else {
|
|
@@ -1719,7 +2314,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1719
2314
|
return badCommand('Usage', 'Usage: `ralph on|off|set <n>|info`');
|
|
1720
2315
|
}
|
|
1721
2316
|
if (parsed < -1 || parsed > MAX_RALPH_ITERATIONS) {
|
|
1722
|
-
return badCommand(
|
|
2317
|
+
return badCommand(
|
|
2318
|
+
'Range',
|
|
2319
|
+
`Ralph iterations must be between -1 and ${MAX_RALPH_ITERATIONS}.`,
|
|
2320
|
+
);
|
|
1723
2321
|
}
|
|
1724
2322
|
nextValue = parsed;
|
|
1725
2323
|
}
|
|
@@ -1732,12 +2330,17 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1732
2330
|
const restartNote = restarted
|
|
1733
2331
|
? ' Current session container restarted to apply immediately.'
|
|
1734
2332
|
: '';
|
|
1735
|
-
return plainCommand(
|
|
2333
|
+
return plainCommand(
|
|
2334
|
+
`Ralph loop set to ${formatRalphIterations(normalized)}.${restartNote}`,
|
|
2335
|
+
);
|
|
1736
2336
|
}
|
|
1737
2337
|
|
|
1738
2338
|
case 'clear': {
|
|
1739
|
-
const deleted = clearSessionHistory(session.id);
|
|
1740
|
-
return infoCommand(
|
|
2339
|
+
const deleted = memoryService.clearSessionHistory(session.id);
|
|
2340
|
+
return infoCommand(
|
|
2341
|
+
'Session Cleared',
|
|
2342
|
+
`Deleted ${deleted} messages. Workspace files preserved.`,
|
|
2343
|
+
);
|
|
1741
2344
|
}
|
|
1742
2345
|
|
|
1743
2346
|
case 'status': {
|
|
@@ -1748,14 +2351,14 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1748
2351
|
const sessionModel = session.model || HYBRIDAI_MODEL;
|
|
1749
2352
|
const queueLabel = `${delegationStatus.active} active / ${delegationStatus.queued} queued`;
|
|
1750
2353
|
const proactiveQueued = getQueuedProactiveMessageCount();
|
|
1751
|
-
const cacheKnown =
|
|
1752
|
-
|
|
2354
|
+
const cacheKnown =
|
|
2355
|
+
metrics.cacheReadTokens != null || metrics.cacheWriteTokens != null;
|
|
2356
|
+
const contextLabel =
|
|
1753
2357
|
metrics.contextUsedTokens != null && metrics.contextBudgetTokens != null
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
: 'n/a';
|
|
2358
|
+
? `${formatCompactNumber(metrics.contextUsedTokens)}/${formatCompactNumber(metrics.contextBudgetTokens)} (${formatPercent(metrics.contextUsagePercent)})`
|
|
2359
|
+
: metrics.contextUsedTokens != null
|
|
2360
|
+
? `${formatCompactNumber(metrics.contextUsedTokens)} est`
|
|
2361
|
+
: 'n/a';
|
|
1759
2362
|
const lines = [
|
|
1760
2363
|
`🦞 HybridClaw v${status.version}${commitShort ? ` (${commitShort})` : ''}`,
|
|
1761
2364
|
`🧠 Model: ${sessionModel}`,
|
|
@@ -1775,43 +2378,244 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1775
2378
|
case 'sessions': {
|
|
1776
2379
|
const sessions = getAllSessions();
|
|
1777
2380
|
if (sessions.length === 0) return plainCommand('No active sessions.');
|
|
1778
|
-
const list = sessions
|
|
1779
|
-
|
|
1780
|
-
|
|
2381
|
+
const list = sessions
|
|
2382
|
+
.slice(0, 20)
|
|
2383
|
+
.map(
|
|
2384
|
+
(s) =>
|
|
2385
|
+
`${s.id} — ${s.message_count} msgs, last active ${s.last_active}`,
|
|
2386
|
+
)
|
|
2387
|
+
.join('\n');
|
|
1781
2388
|
return infoCommand('Sessions', list);
|
|
1782
2389
|
}
|
|
1783
2390
|
|
|
2391
|
+
case 'usage': {
|
|
2392
|
+
const sub = (req.args[1] || 'summary').toLowerCase();
|
|
2393
|
+
if (sub === 'daily' || sub === 'monthly') {
|
|
2394
|
+
const rows = listUsageByAgent({ window: sub });
|
|
2395
|
+
if (rows.length === 0) {
|
|
2396
|
+
return plainCommand(`No usage events recorded for ${sub} window.`);
|
|
2397
|
+
}
|
|
2398
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
2399
|
+
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)}`;
|
|
2400
|
+
});
|
|
2401
|
+
return infoCommand(`Usage (${sub} · by agent)`, lines.join('\n'));
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
if (sub === 'model') {
|
|
2405
|
+
const maybeWindow = (req.args[2] || '').toLowerCase();
|
|
2406
|
+
const window =
|
|
2407
|
+
maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
2408
|
+
? maybeWindow
|
|
2409
|
+
: 'monthly';
|
|
2410
|
+
const modelAgentId =
|
|
2411
|
+
maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
2412
|
+
? (req.args[3] || '').trim()
|
|
2413
|
+
: (req.args[2] || '').trim();
|
|
2414
|
+
const rows = listUsageByModel({
|
|
2415
|
+
window,
|
|
2416
|
+
agentId: modelAgentId || undefined,
|
|
2417
|
+
});
|
|
2418
|
+
if (rows.length === 0) {
|
|
2419
|
+
return plainCommand('No usage events recorded for model breakdown.');
|
|
2420
|
+
}
|
|
2421
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
2422
|
+
return `${row.model} — ${formatCompactNumber(row.total_tokens)} tokens · ${row.call_count} calls · ${formatUsd(row.total_cost_usd)}`;
|
|
2423
|
+
});
|
|
2424
|
+
const scope = modelAgentId ? `agent ${modelAgentId}` : 'all agents';
|
|
2425
|
+
return infoCommand(
|
|
2426
|
+
`Usage (${window} · by model · ${scope})`,
|
|
2427
|
+
lines.join('\n'),
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
if (sub !== 'summary') {
|
|
2432
|
+
return badCommand(
|
|
2433
|
+
'Usage',
|
|
2434
|
+
'Usage: `usage [summary|daily|monthly|model [daily|monthly] [agentId]]`',
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const currentAgentId = resolveSessionAgentId(session);
|
|
2439
|
+
const daily = getUsageTotals({
|
|
2440
|
+
agentId: currentAgentId || undefined,
|
|
2441
|
+
window: 'daily',
|
|
2442
|
+
});
|
|
2443
|
+
const monthly = getUsageTotals({
|
|
2444
|
+
agentId: currentAgentId || undefined,
|
|
2445
|
+
window: 'monthly',
|
|
2446
|
+
});
|
|
2447
|
+
const topModels = listUsageByModel({
|
|
2448
|
+
agentId: currentAgentId || undefined,
|
|
2449
|
+
window: 'monthly',
|
|
2450
|
+
}).slice(0, 5);
|
|
2451
|
+
const scopeLabel = currentAgentId || 'all agents';
|
|
2452
|
+
const lines = [
|
|
2453
|
+
`Scope: ${scopeLabel}`,
|
|
2454
|
+
`Today: ${formatCompactNumber(daily.total_tokens)} tokens · ${daily.call_count} calls · ${formatUsd(daily.total_cost_usd)}`,
|
|
2455
|
+
`Month: ${formatCompactNumber(monthly.total_tokens)} tokens · ${monthly.call_count} calls · ${formatUsd(monthly.total_cost_usd)}`,
|
|
2456
|
+
];
|
|
2457
|
+
if (topModels.length > 0) {
|
|
2458
|
+
lines.push('Top models (monthly):');
|
|
2459
|
+
lines.push(
|
|
2460
|
+
...topModels.map(
|
|
2461
|
+
(row) =>
|
|
2462
|
+
`- ${row.model}: ${formatCompactNumber(row.total_tokens)} tokens · ${formatUsd(row.total_cost_usd)}`,
|
|
2463
|
+
),
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
return infoCommand('Usage Summary', lines.join('\n'));
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
case 'export': {
|
|
2470
|
+
const sub = (req.args[1] || 'session').toLowerCase();
|
|
2471
|
+
if (sub !== 'session') {
|
|
2472
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
2473
|
+
}
|
|
2474
|
+
const targetSessionId = (req.args[2] || session.id || '').trim();
|
|
2475
|
+
if (!targetSessionId) {
|
|
2476
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
2477
|
+
}
|
|
2478
|
+
const targetSession = memoryService.getSessionById(targetSessionId);
|
|
2479
|
+
if (!targetSession) {
|
|
2480
|
+
return badCommand(
|
|
2481
|
+
'Not Found',
|
|
2482
|
+
`Session \`${targetSessionId}\` was not found.`,
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
const exportAgentId =
|
|
2486
|
+
resolveSessionAgentId(targetSession) || resolveSessionAgentId(session);
|
|
2487
|
+
if (!exportAgentId) {
|
|
2488
|
+
return badCommand(
|
|
2489
|
+
'Missing Agent',
|
|
2490
|
+
'Cannot export session: no agent/chatbot is configured for the target session.',
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
const messages = memoryService.getRecentMessages(targetSessionId);
|
|
2494
|
+
const exported = exportSessionSnapshotJsonl({
|
|
2495
|
+
agentId: exportAgentId,
|
|
2496
|
+
sessionId: targetSessionId,
|
|
2497
|
+
channelId: targetSession.channel_id,
|
|
2498
|
+
summary: targetSession.session_summary,
|
|
2499
|
+
messages,
|
|
2500
|
+
reason: 'manual',
|
|
2501
|
+
});
|
|
2502
|
+
if (!exported) {
|
|
2503
|
+
return badCommand(
|
|
2504
|
+
'Export Failed',
|
|
2505
|
+
'Failed to write session export JSONL file. Check gateway logs for details.',
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
return infoCommand(
|
|
2509
|
+
'Session Exported',
|
|
2510
|
+
[
|
|
2511
|
+
`File: ${exported.path}`,
|
|
2512
|
+
`Messages: ${messages.length}`,
|
|
2513
|
+
`Summary: ${targetSession.session_summary ? 'yes' : 'no'}`,
|
|
2514
|
+
].join('\n'),
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
1784
2518
|
case 'schedule': {
|
|
1785
2519
|
const sub = req.args[1]?.toLowerCase();
|
|
1786
2520
|
if (sub === 'add') {
|
|
1787
2521
|
const rest = req.args.slice(2).join(' ');
|
|
2522
|
+
const atMatch = rest.match(/^at\s+"([^"]+)"\s+(.+)$/i);
|
|
2523
|
+
if (atMatch) {
|
|
2524
|
+
const [, runAtRaw, prompt] = atMatch;
|
|
2525
|
+
const parsedDate = new Date(runAtRaw);
|
|
2526
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
2527
|
+
return badCommand(
|
|
2528
|
+
'Invalid Time',
|
|
2529
|
+
`\`${runAtRaw}\` is not a valid ISO timestamp.`,
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
const taskId = createTask(
|
|
2533
|
+
session.id,
|
|
2534
|
+
req.channelId,
|
|
2535
|
+
'',
|
|
2536
|
+
prompt,
|
|
2537
|
+
parsedDate.toISOString(),
|
|
2538
|
+
);
|
|
2539
|
+
rearmScheduler();
|
|
2540
|
+
return plainCommand(
|
|
2541
|
+
`Task #${taskId} created: one-shot at \`${parsedDate.toISOString()}\` — ${prompt}`,
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
const everyMatch = rest.match(/^every\s+(\d+)\s+(.+)$/i);
|
|
2546
|
+
if (everyMatch) {
|
|
2547
|
+
const [, everyRaw, prompt] = everyMatch;
|
|
2548
|
+
const everyMs = Number.parseInt(everyRaw, 10);
|
|
2549
|
+
if (!Number.isFinite(everyMs) || everyMs < 10_000) {
|
|
2550
|
+
return badCommand(
|
|
2551
|
+
'Invalid Interval',
|
|
2552
|
+
'Interval must be at least 10000ms.',
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
const taskId = createTask(
|
|
2556
|
+
session.id,
|
|
2557
|
+
req.channelId,
|
|
2558
|
+
'',
|
|
2559
|
+
prompt,
|
|
2560
|
+
undefined,
|
|
2561
|
+
everyMs,
|
|
2562
|
+
);
|
|
2563
|
+
rearmScheduler();
|
|
2564
|
+
return plainCommand(
|
|
2565
|
+
`Task #${taskId} created: every \`${everyMs}ms\` — ${prompt}`,
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
|
|
1788
2569
|
const cronMatch = rest.match(/^"([^"]+)"\s+(.+)$/);
|
|
1789
2570
|
if (!cronMatch) {
|
|
1790
|
-
return badCommand(
|
|
2571
|
+
return badCommand(
|
|
2572
|
+
'Usage',
|
|
2573
|
+
'Usage: `schedule add "<cron>" <prompt>` or `schedule add at "<ISO time>" <prompt>` or `schedule add every <ms> <prompt>`',
|
|
2574
|
+
);
|
|
1791
2575
|
}
|
|
1792
2576
|
const [, cronExpr, prompt] = cronMatch;
|
|
1793
2577
|
try {
|
|
1794
2578
|
CronExpressionParser.parse(cronExpr);
|
|
1795
2579
|
} catch {
|
|
1796
|
-
return badCommand(
|
|
2580
|
+
return badCommand(
|
|
2581
|
+
'Invalid Cron',
|
|
2582
|
+
`\`${cronExpr}\` is not a valid cron expression.`,
|
|
2583
|
+
);
|
|
1797
2584
|
}
|
|
1798
2585
|
const taskId = createTask(session.id, req.channelId, cronExpr, prompt);
|
|
1799
2586
|
rearmScheduler();
|
|
1800
|
-
return plainCommand(
|
|
2587
|
+
return plainCommand(
|
|
2588
|
+
`Task #${taskId} created: cron \`${cronExpr}\` — ${prompt}`,
|
|
2589
|
+
);
|
|
1801
2590
|
}
|
|
1802
2591
|
|
|
1803
2592
|
if (sub === 'list') {
|
|
1804
2593
|
const tasks = getTasksForSession(session.id);
|
|
1805
2594
|
if (tasks.length === 0) return plainCommand('No scheduled tasks.');
|
|
1806
|
-
const list = tasks
|
|
1807
|
-
|
|
1808
|
-
|
|
2595
|
+
const list = tasks
|
|
2596
|
+
.map((task) => {
|
|
2597
|
+
const scheduleLabel = task.run_at
|
|
2598
|
+
? `at ${task.run_at}`
|
|
2599
|
+
: task.every_ms
|
|
2600
|
+
? `every ${task.every_ms}ms`
|
|
2601
|
+
: task.cron_expr
|
|
2602
|
+
? `cron ${task.cron_expr}`
|
|
2603
|
+
: 'unspecified';
|
|
2604
|
+
const statusLabel = task.last_status || 'n/a';
|
|
2605
|
+
const errorSuffix =
|
|
2606
|
+
task.consecutive_errors > 0
|
|
2607
|
+
? ` · errors ${task.consecutive_errors}`
|
|
2608
|
+
: '';
|
|
2609
|
+
return `#${task.id} ${task.enabled ? 'enabled' : 'disabled'} (${scheduleLabel}) [${statusLabel}${errorSuffix}] — ${task.prompt.slice(0, 60)}`;
|
|
2610
|
+
})
|
|
2611
|
+
.join('\n');
|
|
1809
2612
|
return infoCommand('Scheduled Tasks', list);
|
|
1810
2613
|
}
|
|
1811
2614
|
|
|
1812
2615
|
if (sub === 'remove') {
|
|
1813
2616
|
const taskId = parseIntOrNull(req.args[2]);
|
|
1814
|
-
if (!taskId)
|
|
2617
|
+
if (!taskId)
|
|
2618
|
+
return badCommand('Usage', 'Usage: `schedule remove <id>`');
|
|
1815
2619
|
deleteTask(taskId);
|
|
1816
2620
|
rearmScheduler();
|
|
1817
2621
|
return plainCommand(`Task #${taskId} removed.`);
|
|
@@ -1819,19 +2623,33 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1819
2623
|
|
|
1820
2624
|
if (sub === 'toggle') {
|
|
1821
2625
|
const taskId = parseIntOrNull(req.args[2]);
|
|
1822
|
-
if (!taskId)
|
|
2626
|
+
if (!taskId)
|
|
2627
|
+
return badCommand('Usage', 'Usage: `schedule toggle <id>`');
|
|
1823
2628
|
const tasks = getTasksForSession(session.id);
|
|
1824
2629
|
const task = tasks.find((t) => t.id === taskId);
|
|
1825
|
-
if (!task)
|
|
1826
|
-
|
|
2630
|
+
if (!task)
|
|
2631
|
+
return badCommand(
|
|
2632
|
+
'Not Found',
|
|
2633
|
+
`Task #${taskId} was not found in this session.`,
|
|
2634
|
+
);
|
|
2635
|
+
if (task.enabled) {
|
|
2636
|
+
pauseTask(taskId);
|
|
2637
|
+
} else {
|
|
2638
|
+
resumeTask(taskId);
|
|
2639
|
+
}
|
|
1827
2640
|
rearmScheduler();
|
|
1828
|
-
return plainCommand(
|
|
2641
|
+
return plainCommand(
|
|
2642
|
+
`Task #${taskId} ${task.enabled ? 'disabled' : 'enabled'}.`,
|
|
2643
|
+
);
|
|
1829
2644
|
}
|
|
1830
2645
|
|
|
1831
2646
|
return badCommand('Usage', 'Usage: `schedule add|list|remove|toggle`');
|
|
1832
2647
|
}
|
|
1833
2648
|
|
|
1834
2649
|
default:
|
|
1835
|
-
return badCommand(
|
|
2650
|
+
return badCommand(
|
|
2651
|
+
'Unknown Command',
|
|
2652
|
+
`Unknown command: \`${cmd || '(empty)'}\`.`,
|
|
2653
|
+
);
|
|
1836
2654
|
}
|
|
1837
2655
|
}
|