@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/src/gateway-service.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
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,
|
|
@@ -20,61 +25,73 @@ import {
|
|
|
20
25
|
PROACTIVE_DELEGATION_MAX_PER_TURN,
|
|
21
26
|
PROACTIVE_RALPH_MAX_ITERATIONS,
|
|
22
27
|
} from './config.js';
|
|
23
|
-
import { runAgent } from './agent.js';
|
|
24
|
-
import { getActiveContainerCount, stopSessionContainer } from './container-runner.js';
|
|
25
28
|
import {
|
|
26
|
-
|
|
29
|
+
getActiveContainerCount,
|
|
30
|
+
stopSessionContainer,
|
|
31
|
+
} from './container-runner.js';
|
|
32
|
+
import { buildConversationContext } from './conversation.js';
|
|
33
|
+
import {
|
|
27
34
|
createTask,
|
|
28
35
|
deleteTask,
|
|
29
36
|
getAllSessions,
|
|
30
|
-
getConversationHistory,
|
|
31
|
-
getOrCreateSession,
|
|
32
37
|
getQueuedProactiveMessageCount,
|
|
33
38
|
getRecentStructuredAuditForSession,
|
|
34
39
|
getSessionCount,
|
|
35
40
|
getTasksForSession,
|
|
41
|
+
getUsageTotals,
|
|
42
|
+
listUsageByAgent,
|
|
43
|
+
listUsageByModel,
|
|
36
44
|
logAudit,
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
pauseTask,
|
|
46
|
+
recordUsageEvent,
|
|
47
|
+
resumeTask,
|
|
39
48
|
updateSessionChatbot,
|
|
40
49
|
updateSessionModel,
|
|
41
50
|
updateSessionRag,
|
|
42
51
|
} from './db.js';
|
|
43
|
-
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';
|
|
44
64
|
import { fetchHybridAIBots } from './hybridai-bots.js';
|
|
65
|
+
import { resolveModelContextWindowFallback } from './hybridai-models.js';
|
|
45
66
|
import { logger } from './logger.js';
|
|
67
|
+
import { memoryService } from './memory-service.js';
|
|
46
68
|
import { getObservabilityIngestState } from './observability-ingest.js';
|
|
47
|
-
import {
|
|
69
|
+
import { updateRuntimeConfig } from './runtime-config.js';
|
|
70
|
+
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
71
|
+
import { getSchedulerStatus, rearmScheduler } from './scheduler.js';
|
|
72
|
+
import { exportSessionSnapshotJsonl } from './session-export.js';
|
|
48
73
|
import { maybeCompactSession } from './session-maintenance.js';
|
|
49
74
|
import { appendSessionTranscript } from './session-transcripts.js';
|
|
50
75
|
import { processSideEffects } from './side-effects.js';
|
|
51
76
|
import { expandSkillInvocation } from './skills.js';
|
|
52
77
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type GatewayCommandRequest,
|
|
57
|
-
type GatewayCommandResult,
|
|
58
|
-
type GatewayStatus,
|
|
59
|
-
} from './gateway-types.js';
|
|
78
|
+
estimateTokenCountFromMessages,
|
|
79
|
+
estimateTokenCountFromText,
|
|
80
|
+
} from './token-efficiency.js';
|
|
60
81
|
import type {
|
|
61
82
|
ArtifactMetadata,
|
|
83
|
+
CanonicalSessionContext,
|
|
62
84
|
ChatMessage,
|
|
63
85
|
DelegationSideEffect,
|
|
64
86
|
DelegationTaskSpec,
|
|
65
87
|
MediaContextItem,
|
|
66
88
|
ScheduledTask,
|
|
67
|
-
StructuredAuditEntry,
|
|
68
89
|
StoredMessage,
|
|
90
|
+
StructuredAuditEntry,
|
|
69
91
|
TokenUsageStats,
|
|
70
92
|
ToolProgressEvent,
|
|
71
93
|
} from './types.js';
|
|
72
94
|
import { ensureBootstrapFiles } from './workspace.js';
|
|
73
|
-
import { buildConversationContext } from './conversation.js';
|
|
74
|
-
import { runIsolatedScheduledTask } from './scheduled-task-runner.js';
|
|
75
|
-
import { delegationQueueStatus, enqueueDelegation } from './delegation-manager.js';
|
|
76
|
-
import { estimateTokenCountFromMessages, estimateTokenCountFromText } from './token-efficiency.js';
|
|
77
|
-
import { updateRuntimeConfig } from './runtime-config.js';
|
|
78
95
|
|
|
79
96
|
const BOT_CACHE_TTL = 300_000; // 5 minutes
|
|
80
97
|
const MAX_HISTORY_MESSAGES = 40;
|
|
@@ -93,6 +110,7 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
|
93
110
|
'browser_snapshot',
|
|
94
111
|
'browser_click',
|
|
95
112
|
'browser_type',
|
|
113
|
+
'browser_upload',
|
|
96
114
|
'browser_press',
|
|
97
115
|
'browser_scroll',
|
|
98
116
|
'browser_back',
|
|
@@ -106,7 +124,10 @@ const BASE_SUBAGENT_ALLOWED_TOOLS = [
|
|
|
106
124
|
'browser_network',
|
|
107
125
|
'browser_close',
|
|
108
126
|
];
|
|
109
|
-
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
127
|
+
const ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS = [
|
|
128
|
+
...BASE_SUBAGENT_ALLOWED_TOOLS,
|
|
129
|
+
'delegate',
|
|
130
|
+
];
|
|
110
131
|
const MAX_DELEGATION_TASKS = 6;
|
|
111
132
|
const MAX_DELEGATION_USER_CHARS = 500;
|
|
112
133
|
const MAX_RALPH_ITERATIONS = 64;
|
|
@@ -195,7 +216,9 @@ export interface GatewayChatRequest {
|
|
|
195
216
|
enableRag?: GatewayChatRequestBody['enableRag'];
|
|
196
217
|
onTextDelta?: (delta: string) => void;
|
|
197
218
|
onToolProgress?: (event: ToolProgressEvent) => void;
|
|
198
|
-
onProactiveMessage?: (
|
|
219
|
+
onProactiveMessage?: (
|
|
220
|
+
message: ProactiveMessagePayload,
|
|
221
|
+
) => void | Promise<void>;
|
|
199
222
|
abortSignal?: AbortSignal;
|
|
200
223
|
}
|
|
201
224
|
|
|
@@ -204,7 +227,12 @@ export interface ProactiveMessagePayload {
|
|
|
204
227
|
artifacts?: ArtifactMetadata[];
|
|
205
228
|
}
|
|
206
229
|
|
|
207
|
-
export type {
|
|
230
|
+
export type {
|
|
231
|
+
GatewayChatResult,
|
|
232
|
+
GatewayCommandRequest,
|
|
233
|
+
GatewayCommandResult,
|
|
234
|
+
GatewayStatus,
|
|
235
|
+
};
|
|
208
236
|
export { renderGatewayCommand };
|
|
209
237
|
|
|
210
238
|
function formatUptime(seconds: number): string {
|
|
@@ -226,22 +254,31 @@ function parseIntOrNull(raw: string | undefined): number | null {
|
|
|
226
254
|
return Number.isNaN(parsed) ? null : parsed;
|
|
227
255
|
}
|
|
228
256
|
|
|
229
|
-
function normalizeMediaContextItems(
|
|
257
|
+
function normalizeMediaContextItems(
|
|
258
|
+
raw: GatewayChatRequestBody['media'],
|
|
259
|
+
): MediaContextItem[] {
|
|
230
260
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
231
261
|
const normalized: MediaContextItem[] = [];
|
|
232
262
|
for (const item of raw) {
|
|
233
263
|
if (!item || typeof item !== 'object') continue;
|
|
234
|
-
const path =
|
|
264
|
+
const path =
|
|
265
|
+
typeof item.path === 'string' && item.path.trim()
|
|
266
|
+
? item.path.trim()
|
|
267
|
+
: null;
|
|
235
268
|
const url = typeof item.url === 'string' ? item.url.trim() : '';
|
|
236
|
-
const originalUrl =
|
|
237
|
-
|
|
269
|
+
const originalUrl =
|
|
270
|
+
typeof item.originalUrl === 'string' ? item.originalUrl.trim() : '';
|
|
271
|
+
const filename =
|
|
272
|
+
typeof item.filename === 'string' ? item.filename.trim() : '';
|
|
238
273
|
if (!url || !originalUrl || !filename) continue;
|
|
239
|
-
const sizeBytes =
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
274
|
+
const sizeBytes =
|
|
275
|
+
typeof item.sizeBytes === 'number' && Number.isFinite(item.sizeBytes)
|
|
276
|
+
? Math.max(0, Math.floor(item.sizeBytes))
|
|
277
|
+
: 0;
|
|
278
|
+
const mimeType =
|
|
279
|
+
typeof item.mimeType === 'string' && item.mimeType.trim()
|
|
280
|
+
? item.mimeType.trim().toLowerCase()
|
|
281
|
+
: null;
|
|
245
282
|
normalized.push({
|
|
246
283
|
path,
|
|
247
284
|
url,
|
|
@@ -256,7 +293,9 @@ function normalizeMediaContextItems(raw: GatewayChatRequestBody['media']): Media
|
|
|
256
293
|
|
|
257
294
|
function buildMediaPromptContext(media: MediaContextItem[]): string {
|
|
258
295
|
if (media.length === 0) return '';
|
|
259
|
-
const mediaPaths = media
|
|
296
|
+
const mediaPaths = media
|
|
297
|
+
.map((item) => item.path)
|
|
298
|
+
.filter((path): path is string => Boolean(path));
|
|
260
299
|
const mediaUrls = media.map((item) => item.url);
|
|
261
300
|
const mediaTypes = media.map((item) => item.mimeType || 'unknown');
|
|
262
301
|
const payload = media.map((item, index) => ({
|
|
@@ -299,7 +338,10 @@ export interface MediaToolPolicy {
|
|
|
299
338
|
prioritizeVisionTool: boolean;
|
|
300
339
|
}
|
|
301
340
|
|
|
302
|
-
export function resolveMediaToolPolicy(
|
|
341
|
+
export function resolveMediaToolPolicy(
|
|
342
|
+
content: string,
|
|
343
|
+
media: MediaContextItem[],
|
|
344
|
+
): MediaToolPolicy {
|
|
303
345
|
if (media.length === 0) {
|
|
304
346
|
return {
|
|
305
347
|
blockedTools: undefined,
|
|
@@ -358,14 +400,22 @@ function formatRelativeTime(raw: string | null | undefined): string {
|
|
|
358
400
|
if (!at) return 'unknown';
|
|
359
401
|
const deltaMs = Date.now() - at.getTime();
|
|
360
402
|
if (deltaMs < 15_000) return 'just now';
|
|
361
|
-
if (deltaMs < 60_000)
|
|
362
|
-
|
|
363
|
-
if (deltaMs <
|
|
403
|
+
if (deltaMs < 60_000)
|
|
404
|
+
return `${Math.max(1, Math.floor(deltaMs / 1_000))}s ago`;
|
|
405
|
+
if (deltaMs < 3_600_000)
|
|
406
|
+
return `${Math.max(1, Math.floor(deltaMs / 60_000))}m ago`;
|
|
407
|
+
if (deltaMs < 86_400_000)
|
|
408
|
+
return `${Math.max(1, Math.floor(deltaMs / 3_600_000))}h ago`;
|
|
364
409
|
return `${Math.max(1, Math.floor(deltaMs / 86_400_000))}d ago`;
|
|
365
410
|
}
|
|
366
411
|
|
|
367
412
|
function numberFromUnknown(value: unknown): number | null {
|
|
368
|
-
if (
|
|
413
|
+
if (
|
|
414
|
+
typeof value !== 'number' ||
|
|
415
|
+
Number.isNaN(value) ||
|
|
416
|
+
!Number.isFinite(value)
|
|
417
|
+
)
|
|
418
|
+
return null;
|
|
369
419
|
return value;
|
|
370
420
|
}
|
|
371
421
|
|
|
@@ -377,7 +427,9 @@ function firstNumber(values: unknown[]): number | null {
|
|
|
377
427
|
return null;
|
|
378
428
|
}
|
|
379
429
|
|
|
380
|
-
function parseAuditPayload(
|
|
430
|
+
function parseAuditPayload(
|
|
431
|
+
entry: StructuredAuditEntry,
|
|
432
|
+
): Record<string, unknown> | null {
|
|
381
433
|
try {
|
|
382
434
|
const parsed = JSON.parse(entry.payload) as unknown;
|
|
383
435
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
@@ -391,31 +443,115 @@ function formatCompactNumber(value: number | null): string {
|
|
|
391
443
|
if (value == null) return 'n/a';
|
|
392
444
|
const abs = Math.abs(value);
|
|
393
445
|
if (abs >= 1_000_000) {
|
|
394
|
-
const scaled =
|
|
446
|
+
const scaled =
|
|
447
|
+
abs >= 10_000_000
|
|
448
|
+
? (value / 1_000_000).toFixed(0)
|
|
449
|
+
: (value / 1_000_000).toFixed(1);
|
|
395
450
|
return `${scaled.replace(/\.0$/, '')}M`;
|
|
396
451
|
}
|
|
397
452
|
if (abs >= 1_000) {
|
|
398
|
-
const scaled =
|
|
453
|
+
const scaled =
|
|
454
|
+
abs >= 10_000 ? (value / 1_000).toFixed(0) : (value / 1_000).toFixed(1);
|
|
399
455
|
return `${scaled.replace(/\.0$/, '')}k`;
|
|
400
456
|
}
|
|
401
457
|
return String(Math.round(value));
|
|
402
458
|
}
|
|
403
459
|
|
|
404
460
|
function formatPercent(value: number | null): string {
|
|
405
|
-
if (value == null || Number.isNaN(value) || !Number.isFinite(value))
|
|
461
|
+
if (value == null || Number.isNaN(value) || !Number.isFinite(value))
|
|
462
|
+
return 'n/a';
|
|
406
463
|
return `${Math.max(0, Math.min(100, Math.round(value)))}%`;
|
|
407
464
|
}
|
|
408
465
|
|
|
466
|
+
function formatUsd(value: number | null): string {
|
|
467
|
+
if (value == null || Number.isNaN(value) || !Number.isFinite(value)) {
|
|
468
|
+
return 'n/a';
|
|
469
|
+
}
|
|
470
|
+
if (value <= 0) return '$0.0000';
|
|
471
|
+
if (value >= 1) return `$${value.toFixed(2)}`;
|
|
472
|
+
if (value >= 0.01) return `$${value.toFixed(4)}`;
|
|
473
|
+
return `$${value.toFixed(6)}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function resolveSessionAgentId(session: {
|
|
477
|
+
chatbot_id: string | null;
|
|
478
|
+
}): string | null {
|
|
479
|
+
const sessionAgent = session.chatbot_id?.trim();
|
|
480
|
+
if (sessionAgent) return sessionAgent;
|
|
481
|
+
const defaultAgent = HYBRIDAI_CHATBOT_ID?.trim();
|
|
482
|
+
if (defaultAgent) return defaultAgent;
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function extractUsageCostUsd(tokenUsage?: TokenUsageStats): number {
|
|
487
|
+
if (!tokenUsage) return 0;
|
|
488
|
+
const costCarrier = tokenUsage as unknown as Record<string, unknown>;
|
|
489
|
+
const value = firstNumber([
|
|
490
|
+
costCarrier.costUsd,
|
|
491
|
+
costCarrier.costUSD,
|
|
492
|
+
costCarrier.cost_usd,
|
|
493
|
+
costCarrier.estimatedCostUsd,
|
|
494
|
+
costCarrier.estimated_cost_usd,
|
|
495
|
+
]);
|
|
496
|
+
if (value == null) return 0;
|
|
497
|
+
return Math.max(0, value);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function formatCanonicalContextPrompt(params: {
|
|
501
|
+
summary: string | null;
|
|
502
|
+
recentMessages: Array<{
|
|
503
|
+
role: string;
|
|
504
|
+
content: string;
|
|
505
|
+
session_id: string;
|
|
506
|
+
channel_id: string | null;
|
|
507
|
+
}>;
|
|
508
|
+
}): string | null {
|
|
509
|
+
const sections: string[] = [];
|
|
510
|
+
const summary = (params.summary || '').trim();
|
|
511
|
+
if (summary) {
|
|
512
|
+
sections.push(['### Canonical Session Summary', summary].join('\n'));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (params.recentMessages.length > 0) {
|
|
516
|
+
const lines = params.recentMessages.slice(-6).map((entry) => {
|
|
517
|
+
const role = (entry.role || 'user').trim().toLowerCase();
|
|
518
|
+
const who = role === 'assistant' ? 'Assistant' : 'User';
|
|
519
|
+
const from =
|
|
520
|
+
entry.channel_id && entry.channel_id.trim()
|
|
521
|
+
? `${entry.channel_id.trim()} (${entry.session_id})`
|
|
522
|
+
: entry.session_id;
|
|
523
|
+
const compact = entry.content.replace(/\s+/g, ' ').trim();
|
|
524
|
+
const short =
|
|
525
|
+
compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
|
|
526
|
+
return `- ${who} [${from}]: ${short}`;
|
|
527
|
+
});
|
|
528
|
+
sections.push(
|
|
529
|
+
[
|
|
530
|
+
'### Cross-Channel Recall',
|
|
531
|
+
'Recent context from other sessions/channels for this user:',
|
|
532
|
+
...lines,
|
|
533
|
+
].join('\n'),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const merged = sections.join('\n\n').trim();
|
|
538
|
+
return merged || null;
|
|
539
|
+
}
|
|
540
|
+
|
|
409
541
|
function resolveActivationModeLabel(): string {
|
|
410
542
|
if (DISCORD_COMMANDS_ONLY) return 'commands-only';
|
|
411
543
|
if (DISCORD_GROUP_POLICY === 'disabled') return 'disabled';
|
|
412
544
|
if (DISCORD_GROUP_POLICY === 'allowlist') return 'allowlist';
|
|
413
|
-
if (DISCORD_FREE_RESPONSE_CHANNELS.length > 0)
|
|
545
|
+
if (DISCORD_FREE_RESPONSE_CHANNELS.length > 0)
|
|
546
|
+
return `mention + ${DISCORD_FREE_RESPONSE_CHANNELS.length} free channel(s)`;
|
|
414
547
|
if (DISCORD_RESPOND_TO_ALL_MESSAGES) return 'all messages';
|
|
415
548
|
return 'mention';
|
|
416
549
|
}
|
|
417
550
|
|
|
418
|
-
function resolveGuildChannelMode(
|
|
551
|
+
function resolveGuildChannelMode(
|
|
552
|
+
guildId: string | null,
|
|
553
|
+
channelId: string,
|
|
554
|
+
): 'off' | 'mention' | 'free' {
|
|
419
555
|
if (!guildId) return 'free';
|
|
420
556
|
if (DISCORD_GROUP_POLICY === 'disabled') return 'off';
|
|
421
557
|
const guild = DISCORD_GUILDS[guildId];
|
|
@@ -429,7 +565,11 @@ function resolveGuildChannelMode(guildId: string | null, channelId: string): 'of
|
|
|
429
565
|
if (DISCORD_FREE_RESPONSE_CHANNELS.includes(channelId)) return 'free';
|
|
430
566
|
if (guild) {
|
|
431
567
|
const defaultMode = guild.defaultMode;
|
|
432
|
-
if (
|
|
568
|
+
if (
|
|
569
|
+
defaultMode === 'off' ||
|
|
570
|
+
defaultMode === 'mention' ||
|
|
571
|
+
defaultMode === 'free'
|
|
572
|
+
) {
|
|
433
573
|
return defaultMode;
|
|
434
574
|
}
|
|
435
575
|
}
|
|
@@ -448,21 +588,22 @@ interface SessionStatusSnapshot {
|
|
|
448
588
|
contextUsagePercent: number | null;
|
|
449
589
|
}
|
|
450
590
|
|
|
451
|
-
function readSessionStatusSnapshot(
|
|
591
|
+
function readSessionStatusSnapshot(
|
|
592
|
+
sessionId: string,
|
|
593
|
+
options?: { modelContextWindowTokens?: number | null },
|
|
594
|
+
): SessionStatusSnapshot {
|
|
452
595
|
const entries = getRecentStructuredAuditForSession(sessionId, 160);
|
|
453
596
|
let usagePayload: Record<string, unknown> | null = null;
|
|
454
|
-
let contextPayload: Record<string, unknown> | null = null;
|
|
455
597
|
|
|
456
598
|
for (const entry of entries) {
|
|
457
599
|
const payload = parseAuditPayload(entry);
|
|
458
600
|
if (!payload) continue;
|
|
459
|
-
const payloadType =
|
|
601
|
+
const payloadType =
|
|
602
|
+
typeof payload.type === 'string' ? payload.type : entry.event_type;
|
|
460
603
|
if (!usagePayload && payloadType === 'model.usage') {
|
|
461
604
|
usagePayload = payload;
|
|
462
|
-
} else if (!contextPayload && payloadType === 'context.optimization') {
|
|
463
|
-
contextPayload = payload;
|
|
464
605
|
}
|
|
465
|
-
if (usagePayload
|
|
606
|
+
if (usagePayload) break;
|
|
466
607
|
}
|
|
467
608
|
|
|
468
609
|
const promptTokens = firstNumber([
|
|
@@ -479,29 +620,63 @@ function readSessionStatusSnapshot(sessionId: string): SessionStatusSnapshot {
|
|
|
479
620
|
const cacheReadTokens = firstNumber([
|
|
480
621
|
usagePayload?.cacheReadTokens,
|
|
481
622
|
usagePayload?.cacheReadInputTokens,
|
|
623
|
+
usagePayload?.apiCacheReadTokens,
|
|
624
|
+
usagePayload?.cacheRead,
|
|
625
|
+
usagePayload?.cache_read,
|
|
626
|
+
usagePayload?.cache_read_tokens,
|
|
627
|
+
usagePayload?.cache_read_input_tokens,
|
|
628
|
+
usagePayload?.cached_tokens,
|
|
629
|
+
(usagePayload?.prompt_tokens_details as Record<string, unknown> | undefined)
|
|
630
|
+
?.cached_tokens,
|
|
482
631
|
]);
|
|
483
632
|
const cacheWriteTokens = firstNumber([
|
|
484
633
|
usagePayload?.cacheWriteTokens,
|
|
485
634
|
usagePayload?.cacheWriteInputTokens,
|
|
635
|
+
usagePayload?.apiCacheWriteTokens,
|
|
636
|
+
usagePayload?.cacheWrite,
|
|
637
|
+
usagePayload?.cache_write,
|
|
638
|
+
usagePayload?.cache_write_tokens,
|
|
639
|
+
usagePayload?.cache_write_input_tokens,
|
|
640
|
+
usagePayload?.cache_creation_input_tokens,
|
|
486
641
|
]);
|
|
487
642
|
const cacheRead = Math.max(0, cacheReadTokens || 0);
|
|
488
643
|
const cacheWrite = Math.max(0, cacheWriteTokens || 0);
|
|
489
644
|
const cacheTotal = cacheRead + cacheWrite;
|
|
490
|
-
const cacheHitPercent =
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
645
|
+
const cacheHitPercent =
|
|
646
|
+
cacheTotal > 0 ? (cacheRead / cacheTotal) * 100 : null;
|
|
647
|
+
|
|
648
|
+
const contextUsedTokens = firstNumber([
|
|
649
|
+
usagePayload?.contextTokens,
|
|
650
|
+
usagePayload?.context_tokens,
|
|
651
|
+
usagePayload?.tokensInContext,
|
|
652
|
+
usagePayload?.tokens_in_context,
|
|
653
|
+
usagePayload?.promptTokens,
|
|
654
|
+
usagePayload?.apiPromptTokens,
|
|
655
|
+
usagePayload?.estimatedPromptTokens,
|
|
656
|
+
]);
|
|
657
|
+
const contextBudgetTokens = firstNumber([
|
|
658
|
+
usagePayload?.contextWindowTokens,
|
|
659
|
+
usagePayload?.context_window_tokens,
|
|
660
|
+
usagePayload?.modelContextWindowTokens,
|
|
661
|
+
usagePayload?.model_context_window_tokens,
|
|
662
|
+
usagePayload?.modelContextWindow,
|
|
663
|
+
usagePayload?.model_context_window,
|
|
664
|
+
usagePayload?.maxContextTokens,
|
|
665
|
+
usagePayload?.max_context_tokens,
|
|
666
|
+
usagePayload?.contextWindow,
|
|
667
|
+
usagePayload?.context_window,
|
|
668
|
+
usagePayload?.contextLength,
|
|
669
|
+
usagePayload?.context_length,
|
|
670
|
+
usagePayload?.maxContextSize,
|
|
671
|
+
usagePayload?.max_context_size,
|
|
672
|
+
options?.modelContextWindowTokens,
|
|
673
|
+
]);
|
|
674
|
+
const contextUsagePercent =
|
|
675
|
+
contextUsedTokens != null &&
|
|
676
|
+
contextBudgetTokens != null &&
|
|
677
|
+
contextBudgetTokens > 0
|
|
678
|
+
? (contextUsedTokens / contextBudgetTokens) * 100
|
|
679
|
+
: null;
|
|
505
680
|
|
|
506
681
|
return {
|
|
507
682
|
promptTokens,
|
|
@@ -552,10 +727,12 @@ function isVersionOnlyQuestion(raw: string): boolean {
|
|
|
552
727
|
if (detailedRuntimeTokens.some((token) => text.includes(token))) return false;
|
|
553
728
|
|
|
554
729
|
const words = text.split(' ').filter(Boolean);
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
730
|
+
if (
|
|
731
|
+
words.length > 8 &&
|
|
732
|
+
!text.includes('welche version') &&
|
|
733
|
+
!text.includes('what version') &&
|
|
734
|
+
!text.includes('which version')
|
|
735
|
+
) {
|
|
559
736
|
return false;
|
|
560
737
|
}
|
|
561
738
|
|
|
@@ -578,8 +755,46 @@ function recordSuccessfulTurn(opts: {
|
|
|
578
755
|
toolCallCount: number;
|
|
579
756
|
startedAt: number;
|
|
580
757
|
}): void {
|
|
581
|
-
|
|
582
|
-
|
|
758
|
+
memoryService.storeTurn({
|
|
759
|
+
sessionId: opts.sessionId,
|
|
760
|
+
user: {
|
|
761
|
+
userId: opts.userId,
|
|
762
|
+
username: opts.username,
|
|
763
|
+
content: opts.userContent,
|
|
764
|
+
},
|
|
765
|
+
assistant: {
|
|
766
|
+
userId: 'assistant',
|
|
767
|
+
username: null,
|
|
768
|
+
content: opts.resultText,
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
try {
|
|
772
|
+
if (opts.userId.trim()) {
|
|
773
|
+
memoryService.appendCanonicalMessages({
|
|
774
|
+
agentId: opts.agentId,
|
|
775
|
+
userId: opts.userId,
|
|
776
|
+
newMessages: [
|
|
777
|
+
{
|
|
778
|
+
role: 'user',
|
|
779
|
+
content: opts.userContent,
|
|
780
|
+
sessionId: opts.sessionId,
|
|
781
|
+
channelId: opts.channelId,
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
role: 'assistant',
|
|
785
|
+
content: opts.resultText,
|
|
786
|
+
sessionId: opts.sessionId,
|
|
787
|
+
channelId: opts.channelId,
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
} catch (err) {
|
|
793
|
+
logger.debug(
|
|
794
|
+
{ sessionId: opts.sessionId, userId: opts.userId, err },
|
|
795
|
+
'Failed to append canonical session memory',
|
|
796
|
+
);
|
|
797
|
+
}
|
|
583
798
|
appendSessionTranscript(opts.agentId, {
|
|
584
799
|
sessionId: opts.sessionId,
|
|
585
800
|
channelId: opts.channelId,
|
|
@@ -605,7 +820,10 @@ function recordSuccessfulTurn(opts: {
|
|
|
605
820
|
model: opts.model,
|
|
606
821
|
channelId: opts.channelId,
|
|
607
822
|
}).catch((err) => {
|
|
608
|
-
logger.warn(
|
|
823
|
+
logger.warn(
|
|
824
|
+
{ sessionId: opts.sessionId, err },
|
|
825
|
+
'Background session compaction failed',
|
|
826
|
+
);
|
|
609
827
|
});
|
|
610
828
|
|
|
611
829
|
recordAuditEvent({
|
|
@@ -670,19 +888,33 @@ function buildTokenUsageAuditPayload(
|
|
|
670
888
|
}, 0);
|
|
671
889
|
const completionChars = (resultText || '').length;
|
|
672
890
|
|
|
673
|
-
const fallbackEstimatedPromptTokens =
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
891
|
+
const fallbackEstimatedPromptTokens =
|
|
892
|
+
estimateTokenCountFromMessages(messages);
|
|
893
|
+
const fallbackEstimatedCompletionTokens = estimateTokenCountFromText(
|
|
894
|
+
resultText || '',
|
|
895
|
+
);
|
|
896
|
+
const estimatedPromptTokens =
|
|
897
|
+
tokenUsage?.estimatedPromptTokens || fallbackEstimatedPromptTokens;
|
|
898
|
+
const estimatedCompletionTokens =
|
|
899
|
+
tokenUsage?.estimatedCompletionTokens || fallbackEstimatedCompletionTokens;
|
|
677
900
|
const estimatedTotalTokens =
|
|
678
|
-
tokenUsage?.estimatedTotalTokens ||
|
|
901
|
+
tokenUsage?.estimatedTotalTokens ||
|
|
902
|
+
estimatedPromptTokens + estimatedCompletionTokens;
|
|
679
903
|
|
|
680
904
|
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
681
905
|
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
682
906
|
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
683
|
-
const apiTotalTokens =
|
|
684
|
-
|
|
685
|
-
const
|
|
907
|
+
const apiTotalTokens =
|
|
908
|
+
tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
|
|
909
|
+
const apiCacheUsageAvailable = tokenUsage?.apiCacheUsageAvailable === true;
|
|
910
|
+
const apiCacheReadTokens = tokenUsage?.apiCacheReadTokens || 0;
|
|
911
|
+
const apiCacheWriteTokens = tokenUsage?.apiCacheWriteTokens || 0;
|
|
912
|
+
const promptTokens = apiUsageAvailable
|
|
913
|
+
? apiPromptTokens
|
|
914
|
+
: estimatedPromptTokens;
|
|
915
|
+
const completionTokens = apiUsageAvailable
|
|
916
|
+
? apiCompletionTokens
|
|
917
|
+
: estimatedCompletionTokens;
|
|
686
918
|
const totalTokens = apiUsageAvailable ? apiTotalTokens : estimatedTotalTokens;
|
|
687
919
|
|
|
688
920
|
return {
|
|
@@ -699,6 +931,17 @@ function buildTokenUsageAuditPayload(
|
|
|
699
931
|
apiPromptTokens,
|
|
700
932
|
apiCompletionTokens,
|
|
701
933
|
apiTotalTokens,
|
|
934
|
+
...(apiCacheUsageAvailable
|
|
935
|
+
? {
|
|
936
|
+
apiCacheUsageAvailable,
|
|
937
|
+
apiCacheReadTokens,
|
|
938
|
+
apiCacheWriteTokens,
|
|
939
|
+
cacheReadTokens: apiCacheReadTokens,
|
|
940
|
+
cacheReadInputTokens: apiCacheReadTokens,
|
|
941
|
+
cacheWriteTokens: apiCacheWriteTokens,
|
|
942
|
+
cacheWriteInputTokens: apiCacheWriteTokens,
|
|
943
|
+
}
|
|
944
|
+
: {}),
|
|
702
945
|
};
|
|
703
946
|
}
|
|
704
947
|
|
|
@@ -714,11 +957,19 @@ export function getGatewayStatus(): GatewayStatus {
|
|
|
714
957
|
ragDefault: HYBRIDAI_ENABLE_RAG,
|
|
715
958
|
timestamp: new Date().toISOString(),
|
|
716
959
|
observability: getObservabilityIngestState(),
|
|
960
|
+
scheduler: {
|
|
961
|
+
jobs: getSchedulerStatus(),
|
|
962
|
+
},
|
|
717
963
|
};
|
|
718
964
|
}
|
|
719
965
|
|
|
720
|
-
export function getGatewayHistory(
|
|
721
|
-
|
|
966
|
+
export function getGatewayHistory(
|
|
967
|
+
sessionId: string,
|
|
968
|
+
limit = MAX_HISTORY_MESSAGES,
|
|
969
|
+
): StoredMessage[] {
|
|
970
|
+
return memoryService
|
|
971
|
+
.getConversationHistory(sessionId, Math.max(1, Math.min(limit, 200)))
|
|
972
|
+
.reverse();
|
|
722
973
|
}
|
|
723
974
|
|
|
724
975
|
function extractDelegationDepth(sessionId: string): number {
|
|
@@ -728,18 +979,28 @@ function extractDelegationDepth(sessionId: string): number {
|
|
|
728
979
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
729
980
|
}
|
|
730
981
|
|
|
731
|
-
function nextDelegationSessionId(
|
|
732
|
-
|
|
982
|
+
function nextDelegationSessionId(
|
|
983
|
+
parentSessionId: string,
|
|
984
|
+
nextDepth: number,
|
|
985
|
+
): string {
|
|
986
|
+
const safeParent = parentSessionId
|
|
987
|
+
.replace(/[^a-zA-Z0-9:_-]/g, '-')
|
|
988
|
+
.slice(0, 48);
|
|
733
989
|
const nonce = Math.random().toString(36).slice(2, 8);
|
|
734
990
|
return `delegate:d${nextDepth}:${safeParent}:${Date.now()}:${nonce}`;
|
|
735
991
|
}
|
|
736
992
|
|
|
737
993
|
function resolveSubagentAllowedTools(depth: number): string[] {
|
|
738
|
-
if (depth < PROACTIVE_DELEGATION_MAX_DEPTH)
|
|
994
|
+
if (depth < PROACTIVE_DELEGATION_MAX_DEPTH)
|
|
995
|
+
return ORCHESTRATOR_SUBAGENT_ALLOWED_TOOLS;
|
|
739
996
|
return BASE_SUBAGENT_ALLOWED_TOOLS;
|
|
740
997
|
}
|
|
741
998
|
|
|
742
|
-
function buildSubagentSystemPrompt(params: {
|
|
999
|
+
function buildSubagentSystemPrompt(params: {
|
|
1000
|
+
depth: number;
|
|
1001
|
+
canDelegate: boolean;
|
|
1002
|
+
mode: DelegationMode;
|
|
1003
|
+
}): string {
|
|
743
1004
|
const { depth, canDelegate, mode } = params;
|
|
744
1005
|
const delegationLine = canDelegate
|
|
745
1006
|
? 'You may delegate further only if absolutely necessary and still within depth/turn limits.'
|
|
@@ -787,31 +1048,50 @@ function formatDurationMs(ms: number): string {
|
|
|
787
1048
|
return `${(ms / 1_000).toFixed(1)}s`;
|
|
788
1049
|
}
|
|
789
1050
|
|
|
790
|
-
function abbreviateForUser(
|
|
1051
|
+
function abbreviateForUser(
|
|
1052
|
+
text: string,
|
|
1053
|
+
maxChars = MAX_DELEGATION_USER_CHARS,
|
|
1054
|
+
): string {
|
|
791
1055
|
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
792
1056
|
if (normalized.length <= maxChars) return normalized;
|
|
793
1057
|
return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
794
1058
|
}
|
|
795
1059
|
|
|
796
1060
|
function classifyDelegationError(errorText: string): DelegationErrorClass {
|
|
797
|
-
if (
|
|
798
|
-
|
|
1061
|
+
if (
|
|
1062
|
+
PERMANENT_DELEGATION_ERROR_PATTERNS.some((pattern) =>
|
|
1063
|
+
pattern.test(errorText),
|
|
1064
|
+
)
|
|
1065
|
+
)
|
|
1066
|
+
return 'permanent';
|
|
1067
|
+
if (
|
|
1068
|
+
TRANSIENT_DELEGATION_ERROR_PATTERNS.some((pattern) =>
|
|
1069
|
+
pattern.test(errorText),
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1072
|
+
return 'transient';
|
|
799
1073
|
return 'unknown';
|
|
800
1074
|
}
|
|
801
1075
|
|
|
802
1076
|
function inferDelegationStatus(errorText: string): DelegationRunStatus {
|
|
803
|
-
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
1077
|
+
return /timeout|timed out|deadline exceeded/i.test(errorText)
|
|
1078
|
+
? 'timeout'
|
|
1079
|
+
: 'failed';
|
|
804
1080
|
}
|
|
805
1081
|
|
|
806
|
-
function normalizeDelegationTask(
|
|
1082
|
+
function normalizeDelegationTask(
|
|
1083
|
+
raw: unknown,
|
|
1084
|
+
fallbackModel: string,
|
|
1085
|
+
): NormalizedDelegationTask | null {
|
|
807
1086
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
808
1087
|
const task = raw as DelegationTaskSpec;
|
|
809
1088
|
const prompt = typeof task.prompt === 'string' ? task.prompt.trim() : '';
|
|
810
1089
|
if (!prompt) return null;
|
|
811
1090
|
const label = typeof task.label === 'string' ? task.label.trim() : '';
|
|
812
|
-
const model =
|
|
813
|
-
|
|
814
|
-
|
|
1091
|
+
const model =
|
|
1092
|
+
typeof task.model === 'string' && task.model.trim()
|
|
1093
|
+
? task.model.trim()
|
|
1094
|
+
: fallbackModel;
|
|
815
1095
|
return {
|
|
816
1096
|
prompt,
|
|
817
1097
|
label: label || undefined,
|
|
@@ -819,11 +1099,15 @@ function normalizeDelegationTask(raw: unknown, fallbackModel: string): Normalize
|
|
|
819
1099
|
};
|
|
820
1100
|
}
|
|
821
1101
|
|
|
822
|
-
function normalizeDelegationEffect(
|
|
1102
|
+
function normalizeDelegationEffect(
|
|
1103
|
+
effect: DelegationSideEffect,
|
|
1104
|
+
fallbackModel: string,
|
|
1105
|
+
): {
|
|
823
1106
|
plan?: NormalizedDelegationPlan;
|
|
824
1107
|
error?: string;
|
|
825
1108
|
} {
|
|
826
|
-
const rawMode =
|
|
1109
|
+
const rawMode =
|
|
1110
|
+
typeof effect.mode === 'string' ? effect.mode.trim().toLowerCase() : '';
|
|
827
1111
|
const modeRaw: DelegationMode | '' =
|
|
828
1112
|
rawMode === 'single' || rawMode === 'parallel' || rawMode === 'chain'
|
|
829
1113
|
? rawMode
|
|
@@ -833,9 +1117,10 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
833
1117
|
}
|
|
834
1118
|
|
|
835
1119
|
const label = typeof effect.label === 'string' ? effect.label.trim() : '';
|
|
836
|
-
const baseModel =
|
|
837
|
-
|
|
838
|
-
|
|
1120
|
+
const baseModel =
|
|
1121
|
+
typeof effect.model === 'string' && effect.model.trim()
|
|
1122
|
+
? effect.model.trim()
|
|
1123
|
+
: fallbackModel;
|
|
839
1124
|
const prompt = typeof effect.prompt === 'string' ? effect.prompt.trim() : '';
|
|
840
1125
|
const rawTasks = Array.isArray(effect.tasks) ? effect.tasks : [];
|
|
841
1126
|
const rawChain = Array.isArray(effect.chain) ? effect.chain : [];
|
|
@@ -862,12 +1147,15 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
862
1147
|
return { error: `${mode} delegation requires at least one task` };
|
|
863
1148
|
}
|
|
864
1149
|
if (sourceTasks.length > MAX_DELEGATION_TASKS) {
|
|
865
|
-
return {
|
|
1150
|
+
return {
|
|
1151
|
+
error: `${mode} delegation exceeds max tasks (${MAX_DELEGATION_TASKS})`,
|
|
1152
|
+
};
|
|
866
1153
|
}
|
|
867
1154
|
const tasks: NormalizedDelegationTask[] = [];
|
|
868
1155
|
for (let i = 0; i < sourceTasks.length; i++) {
|
|
869
1156
|
const normalized = normalizeDelegationTask(sourceTasks[i], baseModel);
|
|
870
|
-
if (!normalized)
|
|
1157
|
+
if (!normalized)
|
|
1158
|
+
return { error: `${mode} delegation task #${i + 1} is invalid` };
|
|
871
1159
|
tasks.push(normalized);
|
|
872
1160
|
}
|
|
873
1161
|
return {
|
|
@@ -879,14 +1167,22 @@ function normalizeDelegationEffect(effect: DelegationSideEffect, fallbackModel:
|
|
|
879
1167
|
};
|
|
880
1168
|
}
|
|
881
1169
|
|
|
882
|
-
function renderDelegationTaskTitle(
|
|
1170
|
+
function renderDelegationTaskTitle(
|
|
1171
|
+
mode: DelegationMode,
|
|
1172
|
+
task: NormalizedDelegationTask,
|
|
1173
|
+
index: number,
|
|
1174
|
+
total: number,
|
|
1175
|
+
): string {
|
|
883
1176
|
if (task.label) return task.label;
|
|
884
1177
|
if (mode === 'chain') return `step ${index + 1}/${total}`;
|
|
885
1178
|
if (mode === 'parallel') return `task ${index + 1}/${total}`;
|
|
886
1179
|
return 'task';
|
|
887
1180
|
}
|
|
888
1181
|
|
|
889
|
-
function interpolateChainPrompt(
|
|
1182
|
+
function interpolateChainPrompt(
|
|
1183
|
+
prompt: string,
|
|
1184
|
+
previousResult: string,
|
|
1185
|
+
): string {
|
|
890
1186
|
if (!prompt.includes('{previous}')) return prompt;
|
|
891
1187
|
const replacement = previousResult.trim() || '(no previous output)';
|
|
892
1188
|
return prompt.replace(/\{previous\}/g, replacement);
|
|
@@ -896,7 +1192,9 @@ function sleep(ms: number): Promise<void> {
|
|
|
896
1192
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
897
1193
|
}
|
|
898
1194
|
|
|
899
|
-
async function runDelegationTaskWithRetry(
|
|
1195
|
+
async function runDelegationTaskWithRetry(
|
|
1196
|
+
input: DelegationTaskRunInput,
|
|
1197
|
+
): Promise<DelegationRunResult> {
|
|
900
1198
|
const {
|
|
901
1199
|
parentSessionId,
|
|
902
1200
|
childDepth,
|
|
@@ -909,7 +1207,9 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
909
1207
|
} = input;
|
|
910
1208
|
const allowedTools = resolveSubagentAllowedTools(childDepth);
|
|
911
1209
|
const canDelegate = allowedTools.includes('delegate');
|
|
912
|
-
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
1210
|
+
const maxAttempts = PROACTIVE_AUTO_RETRY_ENABLED
|
|
1211
|
+
? PROACTIVE_AUTO_RETRY_MAX_ATTEMPTS
|
|
1212
|
+
: 1;
|
|
913
1213
|
let attempt = 0;
|
|
914
1214
|
let delayMs = PROACTIVE_AUTO_RETRY_BASE_DELAY_MS;
|
|
915
1215
|
let lastError = 'Delegation failed with unknown error';
|
|
@@ -928,7 +1228,14 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
928
1228
|
const output = await runAgent(
|
|
929
1229
|
sessionId,
|
|
930
1230
|
[
|
|
931
|
-
{
|
|
1231
|
+
{
|
|
1232
|
+
role: 'system',
|
|
1233
|
+
content: buildSubagentSystemPrompt({
|
|
1234
|
+
depth: childDepth,
|
|
1235
|
+
canDelegate,
|
|
1236
|
+
mode,
|
|
1237
|
+
}),
|
|
1238
|
+
},
|
|
932
1239
|
{ role: 'user', content: task.prompt },
|
|
933
1240
|
],
|
|
934
1241
|
chatbotId,
|
|
@@ -961,11 +1268,19 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
961
1268
|
lastError = errorText;
|
|
962
1269
|
lastStatus = inferDelegationStatus(errorText);
|
|
963
1270
|
const classification = classifyDelegationError(errorText);
|
|
964
|
-
const shouldRetry =
|
|
1271
|
+
const shouldRetry =
|
|
1272
|
+
classification === 'transient' && attempt < maxAttempts;
|
|
965
1273
|
if (!shouldRetry) break;
|
|
966
1274
|
|
|
967
1275
|
logger.warn(
|
|
968
|
-
{
|
|
1276
|
+
{
|
|
1277
|
+
parentSessionId,
|
|
1278
|
+
sessionId,
|
|
1279
|
+
attempt,
|
|
1280
|
+
maxAttempts,
|
|
1281
|
+
delayMs,
|
|
1282
|
+
errorText,
|
|
1283
|
+
},
|
|
969
1284
|
'Delegation retry scheduled after transient error',
|
|
970
1285
|
);
|
|
971
1286
|
await sleep(delayMs);
|
|
@@ -977,10 +1292,18 @@ async function runDelegationTaskWithRetry(input: DelegationTaskRunInput): Promis
|
|
|
977
1292
|
lastError = errorText;
|
|
978
1293
|
lastStatus = inferDelegationStatus(errorText);
|
|
979
1294
|
const classification = classifyDelegationError(errorText);
|
|
980
|
-
const shouldRetry =
|
|
1295
|
+
const shouldRetry =
|
|
1296
|
+
classification === 'transient' && attempt < maxAttempts;
|
|
981
1297
|
if (!shouldRetry) break;
|
|
982
1298
|
logger.warn(
|
|
983
|
-
{
|
|
1299
|
+
{
|
|
1300
|
+
parentSessionId,
|
|
1301
|
+
sessionId,
|
|
1302
|
+
attempt,
|
|
1303
|
+
maxAttempts,
|
|
1304
|
+
delayMs,
|
|
1305
|
+
errorText,
|
|
1306
|
+
},
|
|
984
1307
|
'Delegation retry scheduled after transient exception',
|
|
985
1308
|
);
|
|
986
1309
|
await sleep(delayMs);
|
|
@@ -1007,19 +1330,32 @@ function formatDelegationCompletion(params: {
|
|
|
1007
1330
|
totalDurationMs: number;
|
|
1008
1331
|
}): { forUser: string; forLLM: string; artifacts?: ArtifactMetadata[] } {
|
|
1009
1332
|
const { mode, label, entries, totalDurationMs } = params;
|
|
1010
|
-
const completedCount = entries.filter(
|
|
1333
|
+
const completedCount = entries.filter(
|
|
1334
|
+
(entry) => entry.run.status === 'completed',
|
|
1335
|
+
).length;
|
|
1011
1336
|
const failedCount = entries.length - completedCount;
|
|
1012
|
-
const overallStatus =
|
|
1013
|
-
|
|
1337
|
+
const overallStatus =
|
|
1338
|
+
failedCount === 0
|
|
1339
|
+
? 'completed'
|
|
1340
|
+
: completedCount === 0
|
|
1341
|
+
? 'failed'
|
|
1342
|
+
: 'partial';
|
|
1343
|
+
const heading = label?.trim()
|
|
1344
|
+
? `[Delegate: ${label.trim()}]`
|
|
1345
|
+
: `[Delegate ${mode}]`;
|
|
1014
1346
|
|
|
1015
1347
|
const userLines = [
|
|
1016
1348
|
`${heading} ${overallStatus} (${completedCount}/${entries.length} completed, ${formatDurationMs(totalDurationMs)}).`,
|
|
1017
1349
|
];
|
|
1018
1350
|
for (const entry of entries) {
|
|
1019
1351
|
if (entry.run.status === 'completed') {
|
|
1020
|
-
userLines.push(
|
|
1352
|
+
userLines.push(
|
|
1353
|
+
`- ${entry.title}: ${abbreviateForUser(entry.run.result || '')}`,
|
|
1354
|
+
);
|
|
1021
1355
|
} else {
|
|
1022
|
-
userLines.push(
|
|
1356
|
+
userLines.push(
|
|
1357
|
+
`- ${entry.title}: ${entry.run.status} (${abbreviateForUser(entry.run.error || 'Unknown error')})`,
|
|
1358
|
+
);
|
|
1023
1359
|
}
|
|
1024
1360
|
}
|
|
1025
1361
|
|
|
@@ -1075,7 +1411,9 @@ async function publishDelegationCompletion(params: {
|
|
|
1075
1411
|
forLLM: string;
|
|
1076
1412
|
forUser: string;
|
|
1077
1413
|
artifacts?: ArtifactMetadata[];
|
|
1078
|
-
onProactiveMessage?: (
|
|
1414
|
+
onProactiveMessage?: (
|
|
1415
|
+
message: ProactiveMessagePayload,
|
|
1416
|
+
) => void | Promise<void>;
|
|
1079
1417
|
}): Promise<void> {
|
|
1080
1418
|
const {
|
|
1081
1419
|
parentSessionId,
|
|
@@ -1087,7 +1425,13 @@ async function publishDelegationCompletion(params: {
|
|
|
1087
1425
|
onProactiveMessage,
|
|
1088
1426
|
} = params;
|
|
1089
1427
|
|
|
1090
|
-
storeMessage(
|
|
1428
|
+
memoryService.storeMessage({
|
|
1429
|
+
sessionId: parentSessionId,
|
|
1430
|
+
userId: 'assistant',
|
|
1431
|
+
username: null,
|
|
1432
|
+
role: 'assistant',
|
|
1433
|
+
content: forLLM,
|
|
1434
|
+
});
|
|
1091
1435
|
appendSessionTranscript(agentId, {
|
|
1092
1436
|
sessionId: parentSessionId,
|
|
1093
1437
|
channelId,
|
|
@@ -1102,7 +1446,11 @@ async function publishDelegationCompletion(params: {
|
|
|
1102
1446
|
return;
|
|
1103
1447
|
}
|
|
1104
1448
|
logger.info(
|
|
1105
|
-
{
|
|
1449
|
+
{
|
|
1450
|
+
parentSessionId,
|
|
1451
|
+
message: forUser,
|
|
1452
|
+
artifactCount: artifacts?.length || 0,
|
|
1453
|
+
},
|
|
1106
1454
|
'Delegation completion (no proactive channel callback)',
|
|
1107
1455
|
);
|
|
1108
1456
|
}
|
|
@@ -1114,7 +1462,9 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1114
1462
|
chatbotId: string;
|
|
1115
1463
|
enableRag: boolean;
|
|
1116
1464
|
agentId: string;
|
|
1117
|
-
onProactiveMessage?: (
|
|
1465
|
+
onProactiveMessage?: (
|
|
1466
|
+
message: ProactiveMessagePayload,
|
|
1467
|
+
) => void | Promise<void>;
|
|
1118
1468
|
parentDepth: number;
|
|
1119
1469
|
}): void {
|
|
1120
1470
|
const {
|
|
@@ -1129,7 +1479,10 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1129
1479
|
} = params;
|
|
1130
1480
|
const childDepth = parentDepth + 1;
|
|
1131
1481
|
if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
|
|
1132
|
-
logger.info(
|
|
1482
|
+
logger.info(
|
|
1483
|
+
{ parentSessionId, childDepth, maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH },
|
|
1484
|
+
'Delegation skipped — depth limit reached',
|
|
1485
|
+
);
|
|
1133
1486
|
return;
|
|
1134
1487
|
}
|
|
1135
1488
|
|
|
@@ -1141,22 +1494,29 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1141
1494
|
const entries: DelegationCompletionEntry[] = [];
|
|
1142
1495
|
|
|
1143
1496
|
if (plan.mode === 'parallel') {
|
|
1144
|
-
const runs = await Promise.all(
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1497
|
+
const runs = await Promise.all(
|
|
1498
|
+
plan.tasks.map(async (task, index) => {
|
|
1499
|
+
const run = await runDelegationTaskWithRetry({
|
|
1500
|
+
parentSessionId,
|
|
1501
|
+
childDepth,
|
|
1502
|
+
channelId,
|
|
1503
|
+
chatbotId,
|
|
1504
|
+
enableRag,
|
|
1505
|
+
agentId,
|
|
1506
|
+
mode: plan.mode,
|
|
1507
|
+
task,
|
|
1508
|
+
});
|
|
1509
|
+
return {
|
|
1510
|
+
title: renderDelegationTaskTitle(
|
|
1511
|
+
plan.mode,
|
|
1512
|
+
task,
|
|
1513
|
+
index,
|
|
1514
|
+
plan.tasks.length,
|
|
1515
|
+
),
|
|
1516
|
+
run,
|
|
1517
|
+
} as DelegationCompletionEntry;
|
|
1518
|
+
}),
|
|
1519
|
+
);
|
|
1160
1520
|
entries.push(...runs);
|
|
1161
1521
|
} else if (plan.mode === 'chain') {
|
|
1162
1522
|
let previousResult = '';
|
|
@@ -1176,7 +1536,12 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1176
1536
|
},
|
|
1177
1537
|
});
|
|
1178
1538
|
entries.push({
|
|
1179
|
-
title: renderDelegationTaskTitle(
|
|
1539
|
+
title: renderDelegationTaskTitle(
|
|
1540
|
+
plan.mode,
|
|
1541
|
+
task,
|
|
1542
|
+
i,
|
|
1543
|
+
plan.tasks.length,
|
|
1544
|
+
),
|
|
1180
1545
|
run,
|
|
1181
1546
|
});
|
|
1182
1547
|
if (run.status !== 'completed') break;
|
|
@@ -1201,7 +1566,10 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1201
1566
|
}
|
|
1202
1567
|
|
|
1203
1568
|
if (entries.length === 0) {
|
|
1204
|
-
logger.warn(
|
|
1569
|
+
logger.warn(
|
|
1570
|
+
{ parentSessionId, mode: plan.mode },
|
|
1571
|
+
'Delegation produced no entries',
|
|
1572
|
+
);
|
|
1205
1573
|
return;
|
|
1206
1574
|
}
|
|
1207
1575
|
|
|
@@ -1224,10 +1592,16 @@ function enqueueDelegationFromSideEffect(params: {
|
|
|
1224
1592
|
});
|
|
1225
1593
|
}
|
|
1226
1594
|
|
|
1227
|
-
export async function handleGatewayMessage(
|
|
1595
|
+
export async function handleGatewayMessage(
|
|
1596
|
+
req: GatewayChatRequest,
|
|
1597
|
+
): Promise<GatewayChatResult> {
|
|
1228
1598
|
const startedAt = Date.now();
|
|
1229
1599
|
const runId = makeAuditRunId('turn');
|
|
1230
|
-
const session = getOrCreateSession(
|
|
1600
|
+
const session = memoryService.getOrCreateSession(
|
|
1601
|
+
req.sessionId,
|
|
1602
|
+
req.guildId,
|
|
1603
|
+
req.channelId,
|
|
1604
|
+
);
|
|
1231
1605
|
const chatbotId = req.chatbotId ?? session.chatbot_id ?? HYBRIDAI_CHATBOT_ID;
|
|
1232
1606
|
const enableRag = req.enableRag ?? session.enable_rag === 1;
|
|
1233
1607
|
const model = req.model ?? session.model ?? HYBRIDAI_MODEL;
|
|
@@ -1259,7 +1633,8 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1259
1633
|
});
|
|
1260
1634
|
|
|
1261
1635
|
if (!chatbotId) {
|
|
1262
|
-
const error =
|
|
1636
|
+
const error =
|
|
1637
|
+
'No chatbot configured. Set `hybridai.defaultChatbotId` in config.json or select a bot for this session.';
|
|
1263
1638
|
recordAuditEvent({
|
|
1264
1639
|
sessionId: req.sessionId,
|
|
1265
1640
|
runId,
|
|
@@ -1307,10 +1682,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1307
1682
|
if (isVersionOnlyQuestion(req.content)) {
|
|
1308
1683
|
const resultText = `HybridClaw v${APP_VERSION}`;
|
|
1309
1684
|
recordSuccessfulTurn({
|
|
1310
|
-
sessionId: req.sessionId,
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1685
|
+
sessionId: req.sessionId,
|
|
1686
|
+
agentId,
|
|
1687
|
+
chatbotId,
|
|
1688
|
+
enableRag,
|
|
1689
|
+
model,
|
|
1690
|
+
channelId: req.channelId,
|
|
1691
|
+
runId,
|
|
1692
|
+
turnIndex,
|
|
1693
|
+
userId: req.userId,
|
|
1694
|
+
username: req.username,
|
|
1695
|
+
userContent: req.content,
|
|
1696
|
+
resultText,
|
|
1697
|
+
toolCallCount: 0,
|
|
1698
|
+
startedAt,
|
|
1314
1699
|
});
|
|
1315
1700
|
return {
|
|
1316
1701
|
status: 'success',
|
|
@@ -1319,10 +1704,48 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1319
1704
|
};
|
|
1320
1705
|
}
|
|
1321
1706
|
|
|
1322
|
-
const history = getConversationHistory(
|
|
1707
|
+
const history = memoryService.getConversationHistory(
|
|
1708
|
+
req.sessionId,
|
|
1709
|
+
MAX_HISTORY_MESSAGES,
|
|
1710
|
+
);
|
|
1711
|
+
let canonicalContext: CanonicalSessionContext = {
|
|
1712
|
+
summary: null,
|
|
1713
|
+
recent_messages: [],
|
|
1714
|
+
};
|
|
1715
|
+
if (req.userId.trim()) {
|
|
1716
|
+
try {
|
|
1717
|
+
canonicalContext = memoryService.getCanonicalContext({
|
|
1718
|
+
agentId,
|
|
1719
|
+
userId: req.userId,
|
|
1720
|
+
windowSize: 12,
|
|
1721
|
+
excludeSessionId: req.sessionId,
|
|
1722
|
+
});
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
logger.debug(
|
|
1725
|
+
{ sessionId: req.sessionId, userId: req.userId, err },
|
|
1726
|
+
'Failed to load canonical session context',
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const canonicalPromptSummary = formatCanonicalContextPrompt({
|
|
1731
|
+
summary: canonicalContext.summary,
|
|
1732
|
+
recentMessages: canonicalContext.recent_messages,
|
|
1733
|
+
});
|
|
1734
|
+
const memoryContext = memoryService.buildPromptMemoryContext({
|
|
1735
|
+
session,
|
|
1736
|
+
query: req.content,
|
|
1737
|
+
});
|
|
1738
|
+
const mergedSessionSummary =
|
|
1739
|
+
[canonicalPromptSummary, memoryContext.promptSummary]
|
|
1740
|
+
.filter(
|
|
1741
|
+
(value): value is string =>
|
|
1742
|
+
typeof value === 'string' && value.trim().length > 0,
|
|
1743
|
+
)
|
|
1744
|
+
.join('\n\n')
|
|
1745
|
+
.trim() || null;
|
|
1323
1746
|
const { messages, skills, historyStats } = buildConversationContext({
|
|
1324
1747
|
agentId,
|
|
1325
|
-
sessionSummary:
|
|
1748
|
+
sessionSummary: mergedSessionSummary,
|
|
1326
1749
|
history,
|
|
1327
1750
|
runtimeInfo: {
|
|
1328
1751
|
chatbotId,
|
|
@@ -1332,7 +1755,8 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1332
1755
|
guildId: req.guildId,
|
|
1333
1756
|
},
|
|
1334
1757
|
});
|
|
1335
|
-
const historyStart =
|
|
1758
|
+
const historyStart =
|
|
1759
|
+
messages.length > 0 && messages[0].role === 'system' ? 1 : 0;
|
|
1336
1760
|
recordAuditEvent({
|
|
1337
1761
|
sessionId: req.sessionId,
|
|
1338
1762
|
runId,
|
|
@@ -1349,7 +1773,11 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1349
1773
|
historyMaxMessageChars: historyStats.maxMessageChars,
|
|
1350
1774
|
perMessageTruncatedCount: historyStats.perMessageTruncatedCount,
|
|
1351
1775
|
middleCompressionApplied: historyStats.middleCompressionApplied,
|
|
1352
|
-
historyEstimatedTokens: estimateTokenCountFromMessages(
|
|
1776
|
+
historyEstimatedTokens: estimateTokenCountFromMessages(
|
|
1777
|
+
messages.slice(historyStart),
|
|
1778
|
+
),
|
|
1779
|
+
canonicalSummaryIncluded: Boolean(canonicalPromptSummary),
|
|
1780
|
+
canonicalRecentMessagesIncluded: canonicalContext.recent_messages.length,
|
|
1353
1781
|
},
|
|
1354
1782
|
});
|
|
1355
1783
|
const mediaPolicy = resolveMediaToolPolicy(req.content, media);
|
|
@@ -1391,12 +1819,22 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1391
1819
|
req.abortSignal,
|
|
1392
1820
|
media,
|
|
1393
1821
|
);
|
|
1822
|
+
const effectiveUserContent =
|
|
1823
|
+
typeof output.effectiveUserPrompt === 'string' &&
|
|
1824
|
+
output.effectiveUserPrompt.trim()
|
|
1825
|
+
? output.effectiveUserPrompt.trim()
|
|
1826
|
+
: req.content;
|
|
1394
1827
|
const toolExecutions = output.toolExecutions || [];
|
|
1395
1828
|
emitToolExecutionAuditEvents({
|
|
1396
1829
|
sessionId: req.sessionId,
|
|
1397
1830
|
runId,
|
|
1398
1831
|
toolExecutions,
|
|
1399
1832
|
});
|
|
1833
|
+
const usagePayload = buildTokenUsageAuditPayload(
|
|
1834
|
+
messages,
|
|
1835
|
+
output.result,
|
|
1836
|
+
output.tokenUsage,
|
|
1837
|
+
);
|
|
1400
1838
|
recordAuditEvent({
|
|
1401
1839
|
sessionId: req.sessionId,
|
|
1402
1840
|
runId,
|
|
@@ -1406,9 +1844,19 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1406
1844
|
model,
|
|
1407
1845
|
durationMs: Date.now() - startedAt,
|
|
1408
1846
|
toolCallCount: toolExecutions.length,
|
|
1409
|
-
...
|
|
1847
|
+
...usagePayload,
|
|
1410
1848
|
},
|
|
1411
1849
|
});
|
|
1850
|
+
recordUsageEvent({
|
|
1851
|
+
sessionId: req.sessionId,
|
|
1852
|
+
agentId,
|
|
1853
|
+
model,
|
|
1854
|
+
inputTokens: firstNumber([usagePayload.promptTokens]) || 0,
|
|
1855
|
+
outputTokens: firstNumber([usagePayload.completionTokens]) || 0,
|
|
1856
|
+
totalTokens: firstNumber([usagePayload.totalTokens]) || 0,
|
|
1857
|
+
toolCalls: toolExecutions.length,
|
|
1858
|
+
costUsd: extractUsageCostUsd(output.tokenUsage),
|
|
1859
|
+
});
|
|
1412
1860
|
|
|
1413
1861
|
const parentDepth = extractDelegationDepth(req.sessionId);
|
|
1414
1862
|
let acceptedDelegations = 0;
|
|
@@ -1417,7 +1865,11 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1417
1865
|
const normalized = normalizeDelegationEffect(effect, model);
|
|
1418
1866
|
if (!normalized.plan) {
|
|
1419
1867
|
logger.warn(
|
|
1420
|
-
{
|
|
1868
|
+
{
|
|
1869
|
+
sessionId: req.sessionId,
|
|
1870
|
+
error: normalized.error || 'unknown',
|
|
1871
|
+
effect,
|
|
1872
|
+
},
|
|
1421
1873
|
'Delegation skipped — invalid payload',
|
|
1422
1874
|
);
|
|
1423
1875
|
return;
|
|
@@ -1426,14 +1878,21 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1426
1878
|
const childDepth = parentDepth + 1;
|
|
1427
1879
|
if (childDepth > PROACTIVE_DELEGATION_MAX_DEPTH) {
|
|
1428
1880
|
logger.info(
|
|
1429
|
-
{
|
|
1881
|
+
{
|
|
1882
|
+
sessionId: req.sessionId,
|
|
1883
|
+
childDepth,
|
|
1884
|
+
maxDepth: PROACTIVE_DELEGATION_MAX_DEPTH,
|
|
1885
|
+
},
|
|
1430
1886
|
'Delegation skipped — depth limit reached',
|
|
1431
1887
|
);
|
|
1432
1888
|
return;
|
|
1433
1889
|
}
|
|
1434
1890
|
|
|
1435
1891
|
const requestedRuns = normalized.plan.tasks.length;
|
|
1436
|
-
if (
|
|
1892
|
+
if (
|
|
1893
|
+
acceptedDelegations + requestedRuns >
|
|
1894
|
+
PROACTIVE_DELEGATION_MAX_PER_TURN
|
|
1895
|
+
) {
|
|
1437
1896
|
logger.info(
|
|
1438
1897
|
{
|
|
1439
1898
|
sessionId: req.sessionId,
|
|
@@ -1507,10 +1966,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1507
1966
|
|
|
1508
1967
|
const resultText = output.result || 'No response from agent.';
|
|
1509
1968
|
recordSuccessfulTurn({
|
|
1510
|
-
sessionId: req.sessionId,
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1969
|
+
sessionId: req.sessionId,
|
|
1970
|
+
agentId,
|
|
1971
|
+
chatbotId,
|
|
1972
|
+
enableRag,
|
|
1973
|
+
model,
|
|
1974
|
+
channelId: req.channelId,
|
|
1975
|
+
runId,
|
|
1976
|
+
turnIndex,
|
|
1977
|
+
userId: req.userId,
|
|
1978
|
+
username: req.username,
|
|
1979
|
+
userContent: effectiveUserContent,
|
|
1980
|
+
resultText,
|
|
1981
|
+
toolCallCount: toolExecutions.length,
|
|
1982
|
+
startedAt,
|
|
1514
1983
|
});
|
|
1515
1984
|
|
|
1516
1985
|
return {
|
|
@@ -1520,11 +1989,20 @@ export async function handleGatewayMessage(req: GatewayChatRequest): Promise<Gat
|
|
|
1520
1989
|
artifacts: output.artifacts,
|
|
1521
1990
|
toolExecutions,
|
|
1522
1991
|
tokenUsage: output.tokenUsage,
|
|
1992
|
+
effectiveUserPrompt: output.effectiveUserPrompt,
|
|
1523
1993
|
};
|
|
1524
1994
|
} catch (err) {
|
|
1525
1995
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1526
|
-
logAudit(
|
|
1527
|
-
|
|
1996
|
+
logAudit(
|
|
1997
|
+
'error',
|
|
1998
|
+
req.sessionId,
|
|
1999
|
+
{ error: errorMsg },
|
|
2000
|
+
Date.now() - startedAt,
|
|
2001
|
+
);
|
|
2002
|
+
logger.error(
|
|
2003
|
+
{ sessionId: req.sessionId, err },
|
|
2004
|
+
'Gateway message handling failed',
|
|
2005
|
+
);
|
|
1528
2006
|
recordAuditEvent({
|
|
1529
2007
|
sessionId: req.sessionId,
|
|
1530
2008
|
runId,
|
|
@@ -1575,8 +2053,13 @@ export async function runGatewayScheduledTask(
|
|
|
1575
2053
|
taskId: number,
|
|
1576
2054
|
onResult: (result: ProactiveMessagePayload) => Promise<void>,
|
|
1577
2055
|
onError: (error: unknown) => void,
|
|
2056
|
+
runKey?: string,
|
|
1578
2057
|
): Promise<void> {
|
|
1579
|
-
const session = getOrCreateSession(
|
|
2058
|
+
const session = memoryService.getOrCreateSession(
|
|
2059
|
+
origSessionId,
|
|
2060
|
+
null,
|
|
2061
|
+
channelId,
|
|
2062
|
+
);
|
|
1580
2063
|
const chatbotId = session.chatbot_id || HYBRIDAI_CHATBOT_ID;
|
|
1581
2064
|
if (!chatbotId) return;
|
|
1582
2065
|
const model = session.model || HYBRIDAI_MODEL;
|
|
@@ -1589,14 +2072,21 @@ export async function runGatewayScheduledTask(
|
|
|
1589
2072
|
chatbotId,
|
|
1590
2073
|
model,
|
|
1591
2074
|
agentId,
|
|
2075
|
+
sessionKey: runKey,
|
|
1592
2076
|
onResult,
|
|
1593
2077
|
onError,
|
|
1594
2078
|
});
|
|
1595
2079
|
}
|
|
1596
2080
|
|
|
1597
|
-
export async function handleGatewayCommand(
|
|
2081
|
+
export async function handleGatewayCommand(
|
|
2082
|
+
req: GatewayCommandRequest,
|
|
2083
|
+
): Promise<GatewayCommandResult> {
|
|
1598
2084
|
const cmd = (req.args[0] || '').toLowerCase();
|
|
1599
|
-
const session = getOrCreateSession(
|
|
2085
|
+
const session = memoryService.getOrCreateSession(
|
|
2086
|
+
req.sessionId,
|
|
2087
|
+
req.guildId,
|
|
2088
|
+
req.channelId,
|
|
2089
|
+
);
|
|
1600
2090
|
|
|
1601
2091
|
switch (cmd) {
|
|
1602
2092
|
case 'help': {
|
|
@@ -1613,9 +2103,15 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1613
2103
|
'`ralph [on|off|set <n>|info]` — Configure Ralph loop (0 off, -1 unlimited)',
|
|
1614
2104
|
'`clear` — Clear session history',
|
|
1615
2105
|
'`/status` — Show runtime status (Discord slash command, private to caller)',
|
|
2106
|
+
'`/approve [view|yes|session|agent|no] [approval_id]` — View/respond to pending approvals privately',
|
|
1616
2107
|
'`/channel-mode <off|mention|free>` — Set this Discord channel response mode',
|
|
2108
|
+
'`/channel-policy <open|allowlist|disabled>` — Set Discord guild channel policy',
|
|
1617
2109
|
'`sessions` — List active sessions',
|
|
1618
|
-
'`
|
|
2110
|
+
'`usage [summary|daily|monthly|model [daily|monthly] [agentId]]` — Usage/cost aggregates',
|
|
2111
|
+
'`export session [sessionId]` — Export session JSONL snapshot for debugging',
|
|
2112
|
+
'`schedule add "<cron>" <prompt>` — Add cron scheduled task',
|
|
2113
|
+
'`schedule add at "<ISO time>" <prompt>` — Add one-shot task',
|
|
2114
|
+
'`schedule add every <ms> <prompt>` — Add interval task',
|
|
1619
2115
|
'`schedule list` — List scheduled tasks',
|
|
1620
2116
|
'`schedule remove <id>` — Remove a task',
|
|
1621
2117
|
'`schedule toggle <id>` — Enable/disable a task',
|
|
@@ -1629,30 +2125,41 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1629
2125
|
try {
|
|
1630
2126
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1631
2127
|
if (bots.length === 0) return plainCommand('No bots available.');
|
|
1632
|
-
const list = bots
|
|
1633
|
-
|
|
1634
|
-
|
|
2128
|
+
const list = bots
|
|
2129
|
+
.map(
|
|
2130
|
+
(b) =>
|
|
2131
|
+
`• ${b.name} (${b.id})${b.description ? ` — ${b.description}` : ''}`,
|
|
2132
|
+
)
|
|
2133
|
+
.join('\n');
|
|
1635
2134
|
return infoCommand('Available Bots', list);
|
|
1636
2135
|
} catch (err) {
|
|
1637
|
-
return badCommand(
|
|
2136
|
+
return badCommand(
|
|
2137
|
+
'Error',
|
|
2138
|
+
`Failed to fetch bots: ${err instanceof Error ? err.message : String(err)}`,
|
|
2139
|
+
);
|
|
1638
2140
|
}
|
|
1639
2141
|
}
|
|
1640
2142
|
|
|
1641
2143
|
if (sub === 'set') {
|
|
1642
2144
|
const requested = req.args.slice(2).join(' ').trim();
|
|
1643
|
-
if (!requested)
|
|
2145
|
+
if (!requested)
|
|
2146
|
+
return badCommand('Usage', 'Usage: `bot set <id|name>`');
|
|
1644
2147
|
let resolvedBotId = requested;
|
|
1645
2148
|
try {
|
|
1646
2149
|
const bots = await fetchHybridAIBots({ cacheTtlMs: BOT_CACHE_TTL });
|
|
1647
|
-
const matched = bots.find(
|
|
1648
|
-
b
|
|
2150
|
+
const matched = bots.find(
|
|
2151
|
+
(b) =>
|
|
2152
|
+
b.id === requested ||
|
|
2153
|
+
b.name.toLowerCase() === requested.toLowerCase(),
|
|
1649
2154
|
);
|
|
1650
2155
|
if (matched) resolvedBotId = matched.id;
|
|
1651
2156
|
} catch {
|
|
1652
2157
|
// keep user-supplied value when lookup fails
|
|
1653
2158
|
}
|
|
1654
2159
|
updateSessionChatbot(session.id, resolvedBotId);
|
|
1655
|
-
return plainCommand(
|
|
2160
|
+
return plainCommand(
|
|
2161
|
+
`Chatbot set to \`${resolvedBotId}\` for this session.`,
|
|
2162
|
+
);
|
|
1656
2163
|
}
|
|
1657
2164
|
|
|
1658
2165
|
if (sub === 'info') {
|
|
@@ -1667,7 +2174,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1667
2174
|
}
|
|
1668
2175
|
const model = session.model || HYBRIDAI_MODEL;
|
|
1669
2176
|
const ragStatus = session.enable_rag ? 'Enabled' : 'Disabled';
|
|
1670
|
-
return infoCommand(
|
|
2177
|
+
return infoCommand(
|
|
2178
|
+
'Bot Info',
|
|
2179
|
+
`Chatbot: ${botLabel}\nModel: ${model}\nRAG: ${ragStatus}`,
|
|
2180
|
+
);
|
|
1671
2181
|
}
|
|
1672
2182
|
|
|
1673
2183
|
return badCommand('Usage', 'Usage: `bot list|set <id|name>|info`');
|
|
@@ -1678,7 +2188,7 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1678
2188
|
if (sub === 'list') {
|
|
1679
2189
|
const current = session.model || HYBRIDAI_MODEL;
|
|
1680
2190
|
const list = HYBRIDAI_MODELS.map((m) =>
|
|
1681
|
-
m === current ? `${m} (current)` : m
|
|
2191
|
+
m === current ? `${m} (current)` : m,
|
|
1682
2192
|
).join('\n');
|
|
1683
2193
|
return infoCommand('Available Models', list);
|
|
1684
2194
|
}
|
|
@@ -1686,8 +2196,14 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1686
2196
|
if (sub === 'set') {
|
|
1687
2197
|
const modelName = req.args[2];
|
|
1688
2198
|
if (!modelName) return badCommand('Usage', 'Usage: `model set <name>`');
|
|
1689
|
-
if (
|
|
1690
|
-
|
|
2199
|
+
if (
|
|
2200
|
+
HYBRIDAI_MODELS.length > 0 &&
|
|
2201
|
+
!HYBRIDAI_MODELS.includes(modelName)
|
|
2202
|
+
) {
|
|
2203
|
+
return badCommand(
|
|
2204
|
+
'Unknown Model',
|
|
2205
|
+
`\`${modelName}\` is not in the available models list.`,
|
|
2206
|
+
);
|
|
1691
2207
|
}
|
|
1692
2208
|
updateSessionModel(session.id, modelName);
|
|
1693
2209
|
return plainCommand(`Model set to \`${modelName}\` for this session.`);
|
|
@@ -1695,7 +2211,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1695
2211
|
|
|
1696
2212
|
if (sub === 'info') {
|
|
1697
2213
|
const current = session.model || HYBRIDAI_MODEL;
|
|
1698
|
-
return infoCommand(
|
|
2214
|
+
return infoCommand(
|
|
2215
|
+
'Model Info',
|
|
2216
|
+
`Current model: ${current}\nDefault model: ${HYBRIDAI_MODEL}`,
|
|
2217
|
+
);
|
|
1699
2218
|
}
|
|
1700
2219
|
|
|
1701
2220
|
return badCommand('Usage', 'Usage: `model list|set <name>|info`');
|
|
@@ -1705,12 +2224,16 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1705
2224
|
const sub = req.args[1]?.toLowerCase();
|
|
1706
2225
|
if (sub === 'on' || sub === 'off') {
|
|
1707
2226
|
updateSessionRag(session.id, sub === 'on');
|
|
1708
|
-
return plainCommand(
|
|
2227
|
+
return plainCommand(
|
|
2228
|
+
`RAG ${sub === 'on' ? 'enabled' : 'disabled'} for this session.`,
|
|
2229
|
+
);
|
|
1709
2230
|
}
|
|
1710
2231
|
if (!sub) {
|
|
1711
2232
|
const nextEnabled = session.enable_rag === 0;
|
|
1712
2233
|
updateSessionRag(session.id, nextEnabled);
|
|
1713
|
-
return plainCommand(
|
|
2234
|
+
return plainCommand(
|
|
2235
|
+
`RAG ${nextEnabled ? 'enabled' : 'disabled'} for this session.`,
|
|
2236
|
+
);
|
|
1714
2237
|
}
|
|
1715
2238
|
return badCommand('Usage', 'Usage: `rag [on|off]`');
|
|
1716
2239
|
}
|
|
@@ -1720,7 +2243,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1720
2243
|
if (sub === 'mode' || !sub) {
|
|
1721
2244
|
const guildId = req.guildId;
|
|
1722
2245
|
if (!guildId) {
|
|
1723
|
-
return badCommand(
|
|
2246
|
+
return badCommand(
|
|
2247
|
+
'Guild Only',
|
|
2248
|
+
'`channel mode` is only available in Discord guild channels.',
|
|
2249
|
+
);
|
|
1724
2250
|
}
|
|
1725
2251
|
const requestedMode = (req.args[sub ? 2 : 1] || '').toLowerCase();
|
|
1726
2252
|
if (!requestedMode) {
|
|
@@ -1740,7 +2266,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1740
2266
|
}
|
|
1741
2267
|
const mode = requestedMode as 'off' | 'mention' | 'free';
|
|
1742
2268
|
updateRuntimeConfig((draft) => {
|
|
1743
|
-
const guild = draft.discord.guilds[guildId] ?? {
|
|
2269
|
+
const guild = draft.discord.guilds[guildId] ?? {
|
|
2270
|
+
defaultMode: 'mention',
|
|
2271
|
+
channels: {},
|
|
2272
|
+
};
|
|
1744
2273
|
guild.channels[req.channelId] = { mode };
|
|
1745
2274
|
draft.discord.guilds[guildId] = guild;
|
|
1746
2275
|
});
|
|
@@ -1765,7 +2294,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1765
2294
|
);
|
|
1766
2295
|
}
|
|
1767
2296
|
if (!DISCORD_GROUP_POLICY_VALUES.has(requestedPolicy)) {
|
|
1768
|
-
return badCommand(
|
|
2297
|
+
return badCommand(
|
|
2298
|
+
'Usage',
|
|
2299
|
+
'Usage: `channel policy open|allowlist|disabled`',
|
|
2300
|
+
);
|
|
1769
2301
|
}
|
|
1770
2302
|
const policy = requestedPolicy as 'open' | 'allowlist' | 'disabled';
|
|
1771
2303
|
updateRuntimeConfig((draft) => {
|
|
@@ -1774,13 +2306,18 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1774
2306
|
return plainCommand(`Discord group policy set to \`${policy}\`.`);
|
|
1775
2307
|
}
|
|
1776
2308
|
|
|
1777
|
-
return badCommand(
|
|
2309
|
+
return badCommand(
|
|
2310
|
+
'Usage',
|
|
2311
|
+
'Usage: `channel mode [off|mention|free]` or `channel policy [open|allowlist|disabled]`',
|
|
2312
|
+
);
|
|
1778
2313
|
}
|
|
1779
2314
|
|
|
1780
2315
|
case 'ralph': {
|
|
1781
2316
|
const sub = (req.args[1] || '').toLowerCase();
|
|
1782
2317
|
if (!sub || sub === 'info' || sub === 'status') {
|
|
1783
|
-
const current = normalizeRalphIterations(
|
|
2318
|
+
const current = normalizeRalphIterations(
|
|
2319
|
+
PROACTIVE_RALPH_MAX_ITERATIONS,
|
|
2320
|
+
);
|
|
1784
2321
|
return infoCommand(
|
|
1785
2322
|
'Ralph Loop',
|
|
1786
2323
|
[
|
|
@@ -1793,19 +2330,31 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1793
2330
|
|
|
1794
2331
|
let nextValue: number | null = null;
|
|
1795
2332
|
if (sub === 'on') {
|
|
1796
|
-
nextValue =
|
|
2333
|
+
nextValue =
|
|
2334
|
+
PROACTIVE_RALPH_MAX_ITERATIONS === 0
|
|
2335
|
+
? 3
|
|
2336
|
+
: PROACTIVE_RALPH_MAX_ITERATIONS;
|
|
1797
2337
|
} else if (sub === 'off') {
|
|
1798
2338
|
nextValue = 0;
|
|
1799
2339
|
} else if (sub === 'set') {
|
|
1800
2340
|
if (req.args[2] == null) {
|
|
1801
|
-
return badCommand(
|
|
2341
|
+
return badCommand(
|
|
2342
|
+
'Usage',
|
|
2343
|
+
'Usage: `ralph set <n>` (0=off, -1=unlimited, 1-64=extra iterations)',
|
|
2344
|
+
);
|
|
1802
2345
|
}
|
|
1803
2346
|
const parsed = Number.parseInt(req.args[2], 10);
|
|
1804
2347
|
if (Number.isNaN(parsed)) {
|
|
1805
|
-
return badCommand(
|
|
2348
|
+
return badCommand(
|
|
2349
|
+
'Usage',
|
|
2350
|
+
'Usage: `ralph set <n>` where n is an integer',
|
|
2351
|
+
);
|
|
1806
2352
|
}
|
|
1807
2353
|
if (parsed < -1 || parsed > MAX_RALPH_ITERATIONS) {
|
|
1808
|
-
return badCommand(
|
|
2354
|
+
return badCommand(
|
|
2355
|
+
'Range',
|
|
2356
|
+
`Ralph iterations must be between -1 and ${MAX_RALPH_ITERATIONS}.`,
|
|
2357
|
+
);
|
|
1809
2358
|
}
|
|
1810
2359
|
nextValue = parsed;
|
|
1811
2360
|
} else {
|
|
@@ -1814,7 +2363,10 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1814
2363
|
return badCommand('Usage', 'Usage: `ralph on|off|set <n>|info`');
|
|
1815
2364
|
}
|
|
1816
2365
|
if (parsed < -1 || parsed > MAX_RALPH_ITERATIONS) {
|
|
1817
|
-
return badCommand(
|
|
2366
|
+
return badCommand(
|
|
2367
|
+
'Range',
|
|
2368
|
+
`Ralph iterations must be between -1 and ${MAX_RALPH_ITERATIONS}.`,
|
|
2369
|
+
);
|
|
1818
2370
|
}
|
|
1819
2371
|
nextValue = parsed;
|
|
1820
2372
|
}
|
|
@@ -1827,30 +2379,39 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1827
2379
|
const restartNote = restarted
|
|
1828
2380
|
? ' Current session container restarted to apply immediately.'
|
|
1829
2381
|
: '';
|
|
1830
|
-
return plainCommand(
|
|
2382
|
+
return plainCommand(
|
|
2383
|
+
`Ralph loop set to ${formatRalphIterations(normalized)}.${restartNote}`,
|
|
2384
|
+
);
|
|
1831
2385
|
}
|
|
1832
2386
|
|
|
1833
2387
|
case 'clear': {
|
|
1834
|
-
const deleted = clearSessionHistory(session.id);
|
|
1835
|
-
return infoCommand(
|
|
2388
|
+
const deleted = memoryService.clearSessionHistory(session.id);
|
|
2389
|
+
return infoCommand(
|
|
2390
|
+
'Session Cleared',
|
|
2391
|
+
`Deleted ${deleted} messages. Workspace files preserved.`,
|
|
2392
|
+
);
|
|
1836
2393
|
}
|
|
1837
2394
|
|
|
1838
2395
|
case 'status': {
|
|
1839
2396
|
const status = getGatewayStatus();
|
|
1840
2397
|
const delegationStatus = delegationQueueStatus();
|
|
1841
|
-
const metrics = readSessionStatusSnapshot(session.id);
|
|
1842
2398
|
const commitShort = resolveGitCommitShort();
|
|
1843
2399
|
const sessionModel = session.model || HYBRIDAI_MODEL;
|
|
2400
|
+
const modelContextWindowTokens =
|
|
2401
|
+
resolveModelContextWindowFallback(sessionModel);
|
|
2402
|
+
const metrics = readSessionStatusSnapshot(session.id, {
|
|
2403
|
+
modelContextWindowTokens,
|
|
2404
|
+
});
|
|
1844
2405
|
const queueLabel = `${delegationStatus.active} active / ${delegationStatus.queued} queued`;
|
|
1845
2406
|
const proactiveQueued = getQueuedProactiveMessageCount();
|
|
1846
|
-
const cacheKnown =
|
|
1847
|
-
|
|
2407
|
+
const cacheKnown =
|
|
2408
|
+
metrics.cacheReadTokens != null || metrics.cacheWriteTokens != null;
|
|
2409
|
+
const contextLabel =
|
|
1848
2410
|
metrics.contextUsedTokens != null && metrics.contextBudgetTokens != null
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
: 'n/a';
|
|
2411
|
+
? `${formatCompactNumber(metrics.contextUsedTokens)}/${formatCompactNumber(metrics.contextBudgetTokens)} (${formatPercent(metrics.contextUsagePercent)})`
|
|
2412
|
+
: metrics.contextUsedTokens != null
|
|
2413
|
+
? `${formatCompactNumber(metrics.contextUsedTokens)}/? (window unknown)`
|
|
2414
|
+
: 'n/a';
|
|
1854
2415
|
const lines = [
|
|
1855
2416
|
`🦞 HybridClaw v${status.version}${commitShort ? ` (${commitShort})` : ''}`,
|
|
1856
2417
|
`🧠 Model: ${sessionModel}`,
|
|
@@ -1870,43 +2431,244 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1870
2431
|
case 'sessions': {
|
|
1871
2432
|
const sessions = getAllSessions();
|
|
1872
2433
|
if (sessions.length === 0) return plainCommand('No active sessions.');
|
|
1873
|
-
const list = sessions
|
|
1874
|
-
|
|
1875
|
-
|
|
2434
|
+
const list = sessions
|
|
2435
|
+
.slice(0, 20)
|
|
2436
|
+
.map(
|
|
2437
|
+
(s) =>
|
|
2438
|
+
`${s.id} — ${s.message_count} msgs, last active ${s.last_active}`,
|
|
2439
|
+
)
|
|
2440
|
+
.join('\n');
|
|
1876
2441
|
return infoCommand('Sessions', list);
|
|
1877
2442
|
}
|
|
1878
2443
|
|
|
2444
|
+
case 'usage': {
|
|
2445
|
+
const sub = (req.args[1] || 'summary').toLowerCase();
|
|
2446
|
+
if (sub === 'daily' || sub === 'monthly') {
|
|
2447
|
+
const rows = listUsageByAgent({ window: sub });
|
|
2448
|
+
if (rows.length === 0) {
|
|
2449
|
+
return plainCommand(`No usage events recorded for ${sub} window.`);
|
|
2450
|
+
}
|
|
2451
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
2452
|
+
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)}`;
|
|
2453
|
+
});
|
|
2454
|
+
return infoCommand(`Usage (${sub} · by agent)`, lines.join('\n'));
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
if (sub === 'model') {
|
|
2458
|
+
const maybeWindow = (req.args[2] || '').toLowerCase();
|
|
2459
|
+
const window =
|
|
2460
|
+
maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
2461
|
+
? maybeWindow
|
|
2462
|
+
: 'monthly';
|
|
2463
|
+
const modelAgentId =
|
|
2464
|
+
maybeWindow === 'daily' || maybeWindow === 'monthly'
|
|
2465
|
+
? (req.args[3] || '').trim()
|
|
2466
|
+
: (req.args[2] || '').trim();
|
|
2467
|
+
const rows = listUsageByModel({
|
|
2468
|
+
window,
|
|
2469
|
+
agentId: modelAgentId || undefined,
|
|
2470
|
+
});
|
|
2471
|
+
if (rows.length === 0) {
|
|
2472
|
+
return plainCommand('No usage events recorded for model breakdown.');
|
|
2473
|
+
}
|
|
2474
|
+
const lines = rows.slice(0, 20).map((row) => {
|
|
2475
|
+
return `${row.model} — ${formatCompactNumber(row.total_tokens)} tokens · ${row.call_count} calls · ${formatUsd(row.total_cost_usd)}`;
|
|
2476
|
+
});
|
|
2477
|
+
const scope = modelAgentId ? `agent ${modelAgentId}` : 'all agents';
|
|
2478
|
+
return infoCommand(
|
|
2479
|
+
`Usage (${window} · by model · ${scope})`,
|
|
2480
|
+
lines.join('\n'),
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
if (sub !== 'summary') {
|
|
2485
|
+
return badCommand(
|
|
2486
|
+
'Usage',
|
|
2487
|
+
'Usage: `usage [summary|daily|monthly|model [daily|monthly] [agentId]]`',
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
const currentAgentId = resolveSessionAgentId(session);
|
|
2492
|
+
const daily = getUsageTotals({
|
|
2493
|
+
agentId: currentAgentId || undefined,
|
|
2494
|
+
window: 'daily',
|
|
2495
|
+
});
|
|
2496
|
+
const monthly = getUsageTotals({
|
|
2497
|
+
agentId: currentAgentId || undefined,
|
|
2498
|
+
window: 'monthly',
|
|
2499
|
+
});
|
|
2500
|
+
const topModels = listUsageByModel({
|
|
2501
|
+
agentId: currentAgentId || undefined,
|
|
2502
|
+
window: 'monthly',
|
|
2503
|
+
}).slice(0, 5);
|
|
2504
|
+
const scopeLabel = currentAgentId || 'all agents';
|
|
2505
|
+
const lines = [
|
|
2506
|
+
`Scope: ${scopeLabel}`,
|
|
2507
|
+
`Today: ${formatCompactNumber(daily.total_tokens)} tokens · ${daily.call_count} calls · ${formatUsd(daily.total_cost_usd)}`,
|
|
2508
|
+
`Month: ${formatCompactNumber(monthly.total_tokens)} tokens · ${monthly.call_count} calls · ${formatUsd(monthly.total_cost_usd)}`,
|
|
2509
|
+
];
|
|
2510
|
+
if (topModels.length > 0) {
|
|
2511
|
+
lines.push('Top models (monthly):');
|
|
2512
|
+
lines.push(
|
|
2513
|
+
...topModels.map(
|
|
2514
|
+
(row) =>
|
|
2515
|
+
`- ${row.model}: ${formatCompactNumber(row.total_tokens)} tokens · ${formatUsd(row.total_cost_usd)}`,
|
|
2516
|
+
),
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
return infoCommand('Usage Summary', lines.join('\n'));
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
case 'export': {
|
|
2523
|
+
const sub = (req.args[1] || 'session').toLowerCase();
|
|
2524
|
+
if (sub !== 'session') {
|
|
2525
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
2526
|
+
}
|
|
2527
|
+
const targetSessionId = (req.args[2] || session.id || '').trim();
|
|
2528
|
+
if (!targetSessionId) {
|
|
2529
|
+
return badCommand('Usage', 'Usage: `export session [sessionId]`');
|
|
2530
|
+
}
|
|
2531
|
+
const targetSession = memoryService.getSessionById(targetSessionId);
|
|
2532
|
+
if (!targetSession) {
|
|
2533
|
+
return badCommand(
|
|
2534
|
+
'Not Found',
|
|
2535
|
+
`Session \`${targetSessionId}\` was not found.`,
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
const exportAgentId =
|
|
2539
|
+
resolveSessionAgentId(targetSession) || resolveSessionAgentId(session);
|
|
2540
|
+
if (!exportAgentId) {
|
|
2541
|
+
return badCommand(
|
|
2542
|
+
'Missing Agent',
|
|
2543
|
+
'Cannot export session: no agent/chatbot is configured for the target session.',
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
const messages = memoryService.getRecentMessages(targetSessionId);
|
|
2547
|
+
const exported = exportSessionSnapshotJsonl({
|
|
2548
|
+
agentId: exportAgentId,
|
|
2549
|
+
sessionId: targetSessionId,
|
|
2550
|
+
channelId: targetSession.channel_id,
|
|
2551
|
+
summary: targetSession.session_summary,
|
|
2552
|
+
messages,
|
|
2553
|
+
reason: 'manual',
|
|
2554
|
+
});
|
|
2555
|
+
if (!exported) {
|
|
2556
|
+
return badCommand(
|
|
2557
|
+
'Export Failed',
|
|
2558
|
+
'Failed to write session export JSONL file. Check gateway logs for details.',
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
return infoCommand(
|
|
2562
|
+
'Session Exported',
|
|
2563
|
+
[
|
|
2564
|
+
`File: ${exported.path}`,
|
|
2565
|
+
`Messages: ${messages.length}`,
|
|
2566
|
+
`Summary: ${targetSession.session_summary ? 'yes' : 'no'}`,
|
|
2567
|
+
].join('\n'),
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
|
|
1879
2571
|
case 'schedule': {
|
|
1880
2572
|
const sub = req.args[1]?.toLowerCase();
|
|
1881
2573
|
if (sub === 'add') {
|
|
1882
2574
|
const rest = req.args.slice(2).join(' ');
|
|
2575
|
+
const atMatch = rest.match(/^at\s+"([^"]+)"\s+(.+)$/i);
|
|
2576
|
+
if (atMatch) {
|
|
2577
|
+
const [, runAtRaw, prompt] = atMatch;
|
|
2578
|
+
const parsedDate = new Date(runAtRaw);
|
|
2579
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
2580
|
+
return badCommand(
|
|
2581
|
+
'Invalid Time',
|
|
2582
|
+
`\`${runAtRaw}\` is not a valid ISO timestamp.`,
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const taskId = createTask(
|
|
2586
|
+
session.id,
|
|
2587
|
+
req.channelId,
|
|
2588
|
+
'',
|
|
2589
|
+
prompt,
|
|
2590
|
+
parsedDate.toISOString(),
|
|
2591
|
+
);
|
|
2592
|
+
rearmScheduler();
|
|
2593
|
+
return plainCommand(
|
|
2594
|
+
`Task #${taskId} created: one-shot at \`${parsedDate.toISOString()}\` — ${prompt}`,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
const everyMatch = rest.match(/^every\s+(\d+)\s+(.+)$/i);
|
|
2599
|
+
if (everyMatch) {
|
|
2600
|
+
const [, everyRaw, prompt] = everyMatch;
|
|
2601
|
+
const everyMs = Number.parseInt(everyRaw, 10);
|
|
2602
|
+
if (!Number.isFinite(everyMs) || everyMs < 10_000) {
|
|
2603
|
+
return badCommand(
|
|
2604
|
+
'Invalid Interval',
|
|
2605
|
+
'Interval must be at least 10000ms.',
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
const taskId = createTask(
|
|
2609
|
+
session.id,
|
|
2610
|
+
req.channelId,
|
|
2611
|
+
'',
|
|
2612
|
+
prompt,
|
|
2613
|
+
undefined,
|
|
2614
|
+
everyMs,
|
|
2615
|
+
);
|
|
2616
|
+
rearmScheduler();
|
|
2617
|
+
return plainCommand(
|
|
2618
|
+
`Task #${taskId} created: every \`${everyMs}ms\` — ${prompt}`,
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
1883
2622
|
const cronMatch = rest.match(/^"([^"]+)"\s+(.+)$/);
|
|
1884
2623
|
if (!cronMatch) {
|
|
1885
|
-
return badCommand(
|
|
2624
|
+
return badCommand(
|
|
2625
|
+
'Usage',
|
|
2626
|
+
'Usage: `schedule add "<cron>" <prompt>` or `schedule add at "<ISO time>" <prompt>` or `schedule add every <ms> <prompt>`',
|
|
2627
|
+
);
|
|
1886
2628
|
}
|
|
1887
2629
|
const [, cronExpr, prompt] = cronMatch;
|
|
1888
2630
|
try {
|
|
1889
2631
|
CronExpressionParser.parse(cronExpr);
|
|
1890
2632
|
} catch {
|
|
1891
|
-
return badCommand(
|
|
2633
|
+
return badCommand(
|
|
2634
|
+
'Invalid Cron',
|
|
2635
|
+
`\`${cronExpr}\` is not a valid cron expression.`,
|
|
2636
|
+
);
|
|
1892
2637
|
}
|
|
1893
2638
|
const taskId = createTask(session.id, req.channelId, cronExpr, prompt);
|
|
1894
2639
|
rearmScheduler();
|
|
1895
|
-
return plainCommand(
|
|
2640
|
+
return plainCommand(
|
|
2641
|
+
`Task #${taskId} created: cron \`${cronExpr}\` — ${prompt}`,
|
|
2642
|
+
);
|
|
1896
2643
|
}
|
|
1897
2644
|
|
|
1898
2645
|
if (sub === 'list') {
|
|
1899
2646
|
const tasks = getTasksForSession(session.id);
|
|
1900
2647
|
if (tasks.length === 0) return plainCommand('No scheduled tasks.');
|
|
1901
|
-
const list = tasks
|
|
1902
|
-
|
|
1903
|
-
|
|
2648
|
+
const list = tasks
|
|
2649
|
+
.map((task) => {
|
|
2650
|
+
const scheduleLabel = task.run_at
|
|
2651
|
+
? `at ${task.run_at}`
|
|
2652
|
+
: task.every_ms
|
|
2653
|
+
? `every ${task.every_ms}ms`
|
|
2654
|
+
: task.cron_expr
|
|
2655
|
+
? `cron ${task.cron_expr}`
|
|
2656
|
+
: 'unspecified';
|
|
2657
|
+
const statusLabel = task.last_status || 'n/a';
|
|
2658
|
+
const errorSuffix =
|
|
2659
|
+
task.consecutive_errors > 0
|
|
2660
|
+
? ` · errors ${task.consecutive_errors}`
|
|
2661
|
+
: '';
|
|
2662
|
+
return `#${task.id} ${task.enabled ? 'enabled' : 'disabled'} (${scheduleLabel}) [${statusLabel}${errorSuffix}] — ${task.prompt.slice(0, 60)}`;
|
|
2663
|
+
})
|
|
2664
|
+
.join('\n');
|
|
1904
2665
|
return infoCommand('Scheduled Tasks', list);
|
|
1905
2666
|
}
|
|
1906
2667
|
|
|
1907
2668
|
if (sub === 'remove') {
|
|
1908
2669
|
const taskId = parseIntOrNull(req.args[2]);
|
|
1909
|
-
if (!taskId)
|
|
2670
|
+
if (!taskId)
|
|
2671
|
+
return badCommand('Usage', 'Usage: `schedule remove <id>`');
|
|
1910
2672
|
deleteTask(taskId);
|
|
1911
2673
|
rearmScheduler();
|
|
1912
2674
|
return plainCommand(`Task #${taskId} removed.`);
|
|
@@ -1914,19 +2676,33 @@ export async function handleGatewayCommand(req: GatewayCommandRequest): Promise<
|
|
|
1914
2676
|
|
|
1915
2677
|
if (sub === 'toggle') {
|
|
1916
2678
|
const taskId = parseIntOrNull(req.args[2]);
|
|
1917
|
-
if (!taskId)
|
|
2679
|
+
if (!taskId)
|
|
2680
|
+
return badCommand('Usage', 'Usage: `schedule toggle <id>`');
|
|
1918
2681
|
const tasks = getTasksForSession(session.id);
|
|
1919
2682
|
const task = tasks.find((t) => t.id === taskId);
|
|
1920
|
-
if (!task)
|
|
1921
|
-
|
|
2683
|
+
if (!task)
|
|
2684
|
+
return badCommand(
|
|
2685
|
+
'Not Found',
|
|
2686
|
+
`Task #${taskId} was not found in this session.`,
|
|
2687
|
+
);
|
|
2688
|
+
if (task.enabled) {
|
|
2689
|
+
pauseTask(taskId);
|
|
2690
|
+
} else {
|
|
2691
|
+
resumeTask(taskId);
|
|
2692
|
+
}
|
|
1922
2693
|
rearmScheduler();
|
|
1923
|
-
return plainCommand(
|
|
2694
|
+
return plainCommand(
|
|
2695
|
+
`Task #${taskId} ${task.enabled ? 'disabled' : 'enabled'}.`,
|
|
2696
|
+
);
|
|
1924
2697
|
}
|
|
1925
2698
|
|
|
1926
2699
|
return badCommand('Usage', 'Usage: `schedule add|list|remove|toggle`');
|
|
1927
2700
|
}
|
|
1928
2701
|
|
|
1929
2702
|
default:
|
|
1930
|
-
return badCommand(
|
|
2703
|
+
return badCommand(
|
|
2704
|
+
'Unknown Command',
|
|
2705
|
+
`Unknown command: \`${cmd || '(empty)'}\`.`,
|
|
2706
|
+
);
|
|
1931
2707
|
}
|
|
1932
2708
|
}
|