@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.ts
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
|
|
3
1
|
import { AttachmentBuilder } from 'discord.js';
|
|
4
|
-
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import {
|
|
4
|
+
buildResponseText,
|
|
5
|
+
formatError,
|
|
6
|
+
formatInfo,
|
|
7
|
+
} from './channels/discord/delivery.js';
|
|
8
|
+
import { rewriteUserMentionsForMessage } from './channels/discord/mentions.js';
|
|
9
|
+
import {
|
|
10
|
+
initDiscord,
|
|
11
|
+
type ReplyFn,
|
|
12
|
+
sendToChannel,
|
|
13
|
+
setDiscordMaintenancePresence,
|
|
14
|
+
} from './channels/discord/runtime.js';
|
|
5
15
|
import {
|
|
6
16
|
DISCORD_TOKEN,
|
|
17
|
+
getConfigSnapshot,
|
|
7
18
|
HEARTBEAT_CHANNEL,
|
|
8
19
|
HEARTBEAT_INTERVAL,
|
|
9
20
|
HYBRIDAI_CHATBOT_ID,
|
|
10
|
-
PROACTIVE_QUEUE_OUTSIDE_HOURS,
|
|
11
21
|
onConfigChange,
|
|
22
|
+
PROACTIVE_QUEUE_OUTSIDE_HOURS,
|
|
12
23
|
} from './config.js';
|
|
13
24
|
import { stopAllContainers } from './container-runner.js';
|
|
14
25
|
import {
|
|
15
26
|
deleteQueuedProactiveMessage,
|
|
16
27
|
enqueueProactiveMessage,
|
|
28
|
+
getMostRecentSessionChannelId,
|
|
17
29
|
getQueuedProactiveMessageCount,
|
|
18
30
|
initDatabase,
|
|
19
31
|
listQueuedProactiveMessages,
|
|
@@ -25,25 +37,41 @@ import {
|
|
|
25
37
|
renderGatewayCommand,
|
|
26
38
|
runGatewayScheduledTask,
|
|
27
39
|
} from './gateway-service.js';
|
|
28
|
-
import { buildResponseText, formatError, formatInfo } from './channels/discord/delivery.js';
|
|
29
|
-
import { rewriteUserMentionsForMessage } from './channels/discord/mentions.js';
|
|
30
40
|
import { startHealthServer } from './health.js';
|
|
31
41
|
import { startHeartbeat, stopHeartbeat } from './heartbeat.js';
|
|
32
42
|
import { logger } from './logger.js';
|
|
33
|
-
import {
|
|
34
|
-
import { startScheduler, stopScheduler } from './scheduler.js';
|
|
43
|
+
import { memoryService } from './memory-service.js';
|
|
35
44
|
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
startObservabilityIngest,
|
|
46
|
+
stopObservabilityIngest,
|
|
47
|
+
} from './observability-ingest.js';
|
|
48
|
+
import {
|
|
49
|
+
isWithinActiveHours,
|
|
50
|
+
proactiveWindowLabel,
|
|
51
|
+
} from './proactive-policy.js';
|
|
52
|
+
import {
|
|
53
|
+
rearmScheduler,
|
|
54
|
+
type SchedulerDispatchRequest,
|
|
55
|
+
startScheduler,
|
|
56
|
+
stopScheduler,
|
|
57
|
+
} from './scheduler.js';
|
|
41
58
|
import type { ArtifactMetadata } from './types.js';
|
|
42
59
|
|
|
43
60
|
let detachConfigListener: (() => void) | null = null;
|
|
44
61
|
let proactiveFlushTimer: ReturnType<typeof setInterval> | null = null;
|
|
62
|
+
let memoryConsolidationTimer: ReturnType<typeof setInterval> | null = null;
|
|
45
63
|
|
|
46
64
|
const MAX_QUEUED_PROACTIVE_MESSAGES = 100;
|
|
65
|
+
const APPROVAL_PROMPT_DEFAULT_TTL_MS = 120_000;
|
|
66
|
+
|
|
67
|
+
interface PendingApprovalPrompt {
|
|
68
|
+
prompt: string;
|
|
69
|
+
createdAt: number;
|
|
70
|
+
expiresAt: number;
|
|
71
|
+
userId: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pendingApprovalBySession = new Map<string, PendingApprovalPrompt>();
|
|
47
75
|
|
|
48
76
|
function isDiscordChannelId(channelId: string): boolean {
|
|
49
77
|
return /^\d{16,22}$/.test(channelId);
|
|
@@ -57,9 +85,14 @@ function buildArtifactAttachments(
|
|
|
57
85
|
for (const artifact of artifacts) {
|
|
58
86
|
try {
|
|
59
87
|
const content = fs.readFileSync(artifact.path);
|
|
60
|
-
attachments.push(
|
|
88
|
+
attachments.push(
|
|
89
|
+
new AttachmentBuilder(content, { name: artifact.filename }),
|
|
90
|
+
);
|
|
61
91
|
} catch (error) {
|
|
62
|
-
logger.warn(
|
|
92
|
+
logger.warn(
|
|
93
|
+
{ artifactPath: artifact.path, error },
|
|
94
|
+
'Failed to read artifact for Discord attachment',
|
|
95
|
+
);
|
|
63
96
|
}
|
|
64
97
|
}
|
|
65
98
|
return attachments;
|
|
@@ -75,7 +108,9 @@ function simplifyImageAttachmentNarration(
|
|
|
75
108
|
): string {
|
|
76
109
|
if (!text.trim() || !artifacts || artifacts.length === 0) return text;
|
|
77
110
|
|
|
78
|
-
const imageArtifacts = artifacts.filter((artifact) =>
|
|
111
|
+
const imageArtifacts = artifacts.filter((artifact) =>
|
|
112
|
+
artifact.mimeType.startsWith('image/'),
|
|
113
|
+
);
|
|
79
114
|
if (imageArtifacts.length === 0) return text;
|
|
80
115
|
|
|
81
116
|
const pathHints = new Set<string>();
|
|
@@ -88,14 +123,18 @@ function simplifyImageAttachmentNarration(
|
|
|
88
123
|
if (filename) pathHints.add(`.browser-artifacts/${filename}`);
|
|
89
124
|
}
|
|
90
125
|
|
|
91
|
-
const pathishLine =
|
|
92
|
-
|
|
126
|
+
const pathishLine =
|
|
127
|
+
/(^`?\s*(\.\/|\/|~\/|[a-zA-Z]:\\|\.browser-artifacts\/))|([\\/][^\\/\s]+\.[a-zA-Z0-9]{1,8})/;
|
|
128
|
+
const locationNarration =
|
|
129
|
+
/(workspace|saved to|find it at|located at|liegt unter|pfad|path)/i;
|
|
93
130
|
|
|
94
131
|
let removedPathNarration = false;
|
|
95
132
|
const keptLines: string[] = [];
|
|
96
133
|
for (const line of text.split('\n')) {
|
|
97
134
|
const normalizedLine = normalizePathForMatch(line);
|
|
98
|
-
const mentionsArtifact = Array.from(pathHints).some((hint) =>
|
|
135
|
+
const mentionsArtifact = Array.from(pathHints).some((hint) =>
|
|
136
|
+
normalizedLine.includes(hint),
|
|
137
|
+
);
|
|
99
138
|
const isPathLine = pathishLine.test(line.trim());
|
|
100
139
|
const isLocationNarration = locationNarration.test(line);
|
|
101
140
|
if (mentionsArtifact && (isPathLine || isLocationNarration)) {
|
|
@@ -107,11 +146,140 @@ function simplifyImageAttachmentNarration(
|
|
|
107
146
|
|
|
108
147
|
if (!removedPathNarration) return text;
|
|
109
148
|
|
|
110
|
-
const cleaned = keptLines
|
|
149
|
+
const cleaned = keptLines
|
|
150
|
+
.join('\n')
|
|
151
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
152
|
+
.trim();
|
|
111
153
|
if (cleaned) return cleaned;
|
|
112
154
|
return imageArtifacts.length === 1 ? 'Here it is.' : 'Here they are.';
|
|
113
155
|
}
|
|
114
156
|
|
|
157
|
+
function isApprovalPromptForPrivateDelivery(text: string): boolean {
|
|
158
|
+
const normalized = text.toLowerCase();
|
|
159
|
+
return (
|
|
160
|
+
normalized.includes('i need your approval before i') &&
|
|
161
|
+
normalized.includes('approval id:') &&
|
|
162
|
+
normalized.includes('reply `yes`')
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseApprovalExpiryMs(prompt: string): number {
|
|
167
|
+
const match = prompt.match(/Approval expires in\s+(\d+)s\./i);
|
|
168
|
+
const seconds = match ? Number.parseInt(match[1], 10) : NaN;
|
|
169
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
170
|
+
return APPROVAL_PROMPT_DEFAULT_TTL_MS;
|
|
171
|
+
}
|
|
172
|
+
return Math.max(15_000, seconds * 1_000);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractApprovalId(prompt: string): string {
|
|
176
|
+
const match = prompt.match(/Approval ID:\s*([a-f0-9-]{6,64})/i);
|
|
177
|
+
return match?.[1]?.trim() || '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function cleanupExpiredPendingApprovals(): void {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
for (const [sessionId, pending] of pendingApprovalBySession.entries()) {
|
|
183
|
+
if (pending.expiresAt <= now) {
|
|
184
|
+
pendingApprovalBySession.delete(sessionId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleApprovalCommand(params: {
|
|
190
|
+
sessionId: string;
|
|
191
|
+
guildId: string | null;
|
|
192
|
+
channelId: string;
|
|
193
|
+
userId: string;
|
|
194
|
+
username: string;
|
|
195
|
+
args: string[];
|
|
196
|
+
reply: ReplyFn;
|
|
197
|
+
}): Promise<boolean> {
|
|
198
|
+
const { sessionId, guildId, channelId, userId, username, args, reply } =
|
|
199
|
+
params;
|
|
200
|
+
if ((args[0] || '').toLowerCase() !== 'approve') return false;
|
|
201
|
+
|
|
202
|
+
cleanupExpiredPendingApprovals();
|
|
203
|
+
const pending = pendingApprovalBySession.get(sessionId) || null;
|
|
204
|
+
const action = (args[1] || 'view').trim().toLowerCase();
|
|
205
|
+
const providedApprovalId = (args[2] || '').trim();
|
|
206
|
+
const currentApprovalId = pending ? extractApprovalId(pending.prompt) : '';
|
|
207
|
+
const approvalId = providedApprovalId || currentApprovalId;
|
|
208
|
+
|
|
209
|
+
if (action === 'view' || action === 'status' || action === 'show') {
|
|
210
|
+
if (!pending || pending.userId !== userId) {
|
|
211
|
+
await reply('No pending approval request for you in this session.');
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
await reply(formatInfo('Pending Approval', pending.prompt));
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const directive = (() => {
|
|
219
|
+
if (action === 'yes' || action === '1') return 'yes';
|
|
220
|
+
if (action === 'session' || action === '2') return 'yes for session';
|
|
221
|
+
if (action === 'agent' || action === '3') return 'yes for agent';
|
|
222
|
+
if (
|
|
223
|
+
action === 'no' ||
|
|
224
|
+
action === 'deny' ||
|
|
225
|
+
action === 'skip' ||
|
|
226
|
+
action === '4'
|
|
227
|
+
) {
|
|
228
|
+
return 'no';
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
})();
|
|
232
|
+
|
|
233
|
+
if (!directive) {
|
|
234
|
+
await reply(
|
|
235
|
+
'Usage: `/approve action:view|yes|session|agent|no [approval_id]`',
|
|
236
|
+
);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!approvalId && !pending) {
|
|
241
|
+
await reply('No pending approval request for this session.');
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const approvalContent = approvalId ? `${directive} ${approvalId}` : directive;
|
|
246
|
+
const approvalResult = await handleGatewayMessage({
|
|
247
|
+
sessionId,
|
|
248
|
+
guildId,
|
|
249
|
+
channelId,
|
|
250
|
+
userId,
|
|
251
|
+
username,
|
|
252
|
+
content: approvalContent,
|
|
253
|
+
media: [],
|
|
254
|
+
});
|
|
255
|
+
if (approvalResult.status === 'error') {
|
|
256
|
+
await reply(
|
|
257
|
+
formatError('Approval Error', approvalResult.error || 'Unknown error'),
|
|
258
|
+
);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const resultText = buildResponseText(
|
|
263
|
+
approvalResult.result || 'No response from agent.',
|
|
264
|
+
approvalResult.toolsUsed,
|
|
265
|
+
);
|
|
266
|
+
if (isApprovalPromptForPrivateDelivery(resultText)) {
|
|
267
|
+
pendingApprovalBySession.set(sessionId, {
|
|
268
|
+
prompt: resultText,
|
|
269
|
+
createdAt: Date.now(),
|
|
270
|
+
expiresAt: Date.now() + parseApprovalExpiryMs(resultText),
|
|
271
|
+
userId,
|
|
272
|
+
});
|
|
273
|
+
await reply(formatInfo('Pending Approval', resultText));
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
pendingApprovalBySession.delete(sessionId);
|
|
278
|
+
const attachments = buildArtifactAttachments(approvalResult.artifacts);
|
|
279
|
+
await reply(resultText, attachments);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
115
283
|
async function deliverProactiveMessage(
|
|
116
284
|
channelId: string,
|
|
117
285
|
text: string,
|
|
@@ -120,7 +288,12 @@ async function deliverProactiveMessage(
|
|
|
120
288
|
): Promise<void> {
|
|
121
289
|
if (!isWithinActiveHours()) {
|
|
122
290
|
if (PROACTIVE_QUEUE_OUTSIDE_HOURS) {
|
|
123
|
-
const { queued, dropped } = enqueueProactiveMessage(
|
|
291
|
+
const { queued, dropped } = enqueueProactiveMessage(
|
|
292
|
+
channelId,
|
|
293
|
+
text,
|
|
294
|
+
source,
|
|
295
|
+
MAX_QUEUED_PROACTIVE_MESSAGES,
|
|
296
|
+
);
|
|
124
297
|
logger.info(
|
|
125
298
|
{
|
|
126
299
|
source,
|
|
@@ -140,7 +313,10 @@ async function deliverProactiveMessage(
|
|
|
140
313
|
}
|
|
141
314
|
return;
|
|
142
315
|
}
|
|
143
|
-
logger.info(
|
|
316
|
+
logger.info(
|
|
317
|
+
{ source, channelId, activeHours: proactiveWindowLabel() },
|
|
318
|
+
'Proactive message suppressed (outside active hours)',
|
|
319
|
+
);
|
|
144
320
|
return;
|
|
145
321
|
}
|
|
146
322
|
|
|
@@ -155,18 +331,59 @@ async function sendProactiveMessageNow(
|
|
|
155
331
|
): Promise<void> {
|
|
156
332
|
const attachments = buildArtifactAttachments(artifacts);
|
|
157
333
|
if (!DISCORD_TOKEN || !isDiscordChannelId(channelId)) {
|
|
158
|
-
logger.info(
|
|
334
|
+
logger.info(
|
|
335
|
+
{ source, channelId, text, artifactCount: attachments.length },
|
|
336
|
+
'Proactive message (no Discord delivery)',
|
|
337
|
+
);
|
|
159
338
|
return;
|
|
160
339
|
}
|
|
161
340
|
|
|
162
341
|
try {
|
|
163
342
|
await sendToChannel(channelId, text, attachments);
|
|
164
343
|
} catch (error) {
|
|
165
|
-
logger.warn(
|
|
344
|
+
logger.warn(
|
|
345
|
+
{ source, channelId, error, artifactCount: attachments.length },
|
|
346
|
+
'Failed to send proactive message to Discord channel',
|
|
347
|
+
);
|
|
166
348
|
logger.info({ source, channelId, text }, 'Proactive message fallback');
|
|
167
349
|
}
|
|
168
350
|
}
|
|
169
351
|
|
|
352
|
+
async function deliverWebhookMessage(
|
|
353
|
+
webhookUrl: string,
|
|
354
|
+
text: string,
|
|
355
|
+
source: string,
|
|
356
|
+
artifacts?: ArtifactMetadata[],
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
const response = await fetch(webhookUrl, {
|
|
359
|
+
method: 'POST',
|
|
360
|
+
headers: {
|
|
361
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
362
|
+
},
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
text,
|
|
365
|
+
source,
|
|
366
|
+
artifactCount: artifacts?.length || 0,
|
|
367
|
+
artifacts: (artifacts || []).map((artifact) => ({
|
|
368
|
+
filename: artifact.filename,
|
|
369
|
+
mimeType: artifact.mimeType,
|
|
370
|
+
})),
|
|
371
|
+
}),
|
|
372
|
+
});
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
const body = await response.text().catch(() => '');
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Webhook delivery failed (${response.status}): ${body.slice(0, 300)}`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveLastUsedDiscordChannelId(): string | null {
|
|
382
|
+
const channelId = getMostRecentSessionChannelId();
|
|
383
|
+
if (!channelId) return null;
|
|
384
|
+
return isDiscordChannelId(channelId) ? channelId : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
170
387
|
async function flushQueuedProactiveMessages(): Promise<void> {
|
|
171
388
|
if (!isWithinActiveHours()) return;
|
|
172
389
|
const pending = listQueuedProactiveMessages(MAX_QUEUED_PROACTIVE_MESSAGES);
|
|
@@ -178,7 +395,11 @@ async function flushQueuedProactiveMessages(): Promise<void> {
|
|
|
178
395
|
|
|
179
396
|
for (const item of pending) {
|
|
180
397
|
if (!isWithinActiveHours()) break;
|
|
181
|
-
await sendProactiveMessageNow(
|
|
398
|
+
await sendProactiveMessageNow(
|
|
399
|
+
item.channel_id,
|
|
400
|
+
item.text,
|
|
401
|
+
`${item.source}:queued`,
|
|
402
|
+
);
|
|
182
403
|
deleteQueuedProactiveMessage(item.id);
|
|
183
404
|
}
|
|
184
405
|
}
|
|
@@ -202,6 +423,7 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
202
423
|
context,
|
|
203
424
|
) => {
|
|
204
425
|
try {
|
|
426
|
+
let sawTextDelta = false;
|
|
205
427
|
const result = await handleGatewayMessage({
|
|
206
428
|
sessionId,
|
|
207
429
|
guildId,
|
|
@@ -211,15 +433,35 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
211
433
|
content,
|
|
212
434
|
media,
|
|
213
435
|
onTextDelta: (delta) => {
|
|
436
|
+
if (!sawTextDelta) {
|
|
437
|
+
sawTextDelta = true;
|
|
438
|
+
context.emitLifecyclePhase('streaming');
|
|
439
|
+
}
|
|
214
440
|
void context.stream.append(delta);
|
|
215
441
|
},
|
|
442
|
+
onToolProgress: (event) => {
|
|
443
|
+
if (sawTextDelta) return;
|
|
444
|
+
if (event.phase === 'start') {
|
|
445
|
+
context.emitLifecyclePhase('toolUse');
|
|
446
|
+
} else {
|
|
447
|
+
context.emitLifecyclePhase('thinking');
|
|
448
|
+
}
|
|
449
|
+
},
|
|
216
450
|
onProactiveMessage: async (message) => {
|
|
217
|
-
await deliverProactiveMessage(
|
|
451
|
+
await deliverProactiveMessage(
|
|
452
|
+
channelId,
|
|
453
|
+
message.text,
|
|
454
|
+
'delegate',
|
|
455
|
+
message.artifacts,
|
|
456
|
+
);
|
|
218
457
|
},
|
|
219
458
|
abortSignal: context.abortSignal,
|
|
220
459
|
});
|
|
221
460
|
if (result.status === 'error') {
|
|
222
|
-
const errorText = formatError(
|
|
461
|
+
const errorText = formatError(
|
|
462
|
+
'Agent Error',
|
|
463
|
+
result.error || 'Unknown error',
|
|
464
|
+
);
|
|
223
465
|
await context.stream.fail(errorText);
|
|
224
466
|
return;
|
|
225
467
|
}
|
|
@@ -233,13 +475,27 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
233
475
|
context.sourceMessage,
|
|
234
476
|
context.mentionLookup,
|
|
235
477
|
);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
478
|
+
const responseText = buildResponseText(renderedText, result.toolsUsed);
|
|
479
|
+
if (isApprovalPromptForPrivateDelivery(responseText)) {
|
|
480
|
+
pendingApprovalBySession.set(sessionId, {
|
|
481
|
+
prompt: responseText,
|
|
482
|
+
createdAt: Date.now(),
|
|
483
|
+
expiresAt: Date.now() + parseApprovalExpiryMs(responseText),
|
|
484
|
+
userId,
|
|
485
|
+
});
|
|
486
|
+
await context.stream.finalize(
|
|
487
|
+
`<@${userId}> approval required. Use \`/approve\` to view and respond privately.`,
|
|
488
|
+
);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
pendingApprovalBySession.delete(sessionId);
|
|
492
|
+
await context.stream.finalize(responseText, attachments);
|
|
240
493
|
} catch (error) {
|
|
241
494
|
const text = error instanceof Error ? error.message : String(error);
|
|
242
|
-
logger.error(
|
|
495
|
+
logger.error(
|
|
496
|
+
{ error, sessionId, channelId },
|
|
497
|
+
'Discord message handling failed',
|
|
498
|
+
);
|
|
243
499
|
const errorText = formatError('Gateway Error', text);
|
|
244
500
|
await context.stream.fail(errorText);
|
|
245
501
|
}
|
|
@@ -248,10 +504,25 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
248
504
|
sessionId: string,
|
|
249
505
|
guildId: string | null,
|
|
250
506
|
channelId: string,
|
|
507
|
+
userId: string,
|
|
508
|
+
username: string,
|
|
251
509
|
args: string[],
|
|
252
510
|
reply: ReplyFn,
|
|
253
511
|
) => {
|
|
254
512
|
try {
|
|
513
|
+
if (
|
|
514
|
+
await handleApprovalCommand({
|
|
515
|
+
sessionId,
|
|
516
|
+
guildId,
|
|
517
|
+
channelId,
|
|
518
|
+
userId,
|
|
519
|
+
username,
|
|
520
|
+
args,
|
|
521
|
+
reply,
|
|
522
|
+
})
|
|
523
|
+
) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
255
526
|
const result = await handleGatewayCommand({
|
|
256
527
|
sessionId,
|
|
257
528
|
guildId,
|
|
@@ -269,7 +540,10 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
269
540
|
await reply(renderGatewayCommand(result));
|
|
270
541
|
} catch (error) {
|
|
271
542
|
const text = error instanceof Error ? error.message : String(error);
|
|
272
|
-
logger.error(
|
|
543
|
+
logger.error(
|
|
544
|
+
{ error, sessionId, channelId, args },
|
|
545
|
+
'Discord command handling failed',
|
|
546
|
+
);
|
|
273
547
|
await reply(formatError('Gateway Error', text));
|
|
274
548
|
}
|
|
275
549
|
},
|
|
@@ -278,47 +552,141 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
278
552
|
}
|
|
279
553
|
|
|
280
554
|
function setupShutdown(): void {
|
|
281
|
-
|
|
555
|
+
let shuttingDown = false;
|
|
556
|
+
const shutdown = async () => {
|
|
557
|
+
if (shuttingDown) return;
|
|
558
|
+
shuttingDown = true;
|
|
282
559
|
logger.info('Shutting down gateway...');
|
|
283
560
|
if (detachConfigListener) {
|
|
284
561
|
detachConfigListener();
|
|
285
562
|
detachConfigListener = null;
|
|
286
563
|
}
|
|
564
|
+
await setDiscordMaintenancePresence().catch((error) => {
|
|
565
|
+
logger.debug(
|
|
566
|
+
{ error },
|
|
567
|
+
'Failed to set Discord maintenance presence during shutdown',
|
|
568
|
+
);
|
|
569
|
+
});
|
|
287
570
|
stopHeartbeat();
|
|
288
571
|
stopObservabilityIngest();
|
|
289
572
|
stopAllContainers();
|
|
290
573
|
stopScheduler();
|
|
574
|
+
stopMemoryConsolidationScheduler();
|
|
291
575
|
if (proactiveFlushTimer) {
|
|
292
576
|
clearInterval(proactiveFlushTimer);
|
|
293
577
|
proactiveFlushTimer = null;
|
|
294
578
|
}
|
|
295
579
|
process.exit(0);
|
|
296
580
|
};
|
|
297
|
-
process.on('SIGINT',
|
|
298
|
-
|
|
581
|
+
process.on('SIGINT', () => {
|
|
582
|
+
void shutdown();
|
|
583
|
+
});
|
|
584
|
+
process.on('SIGTERM', () => {
|
|
585
|
+
void shutdown();
|
|
586
|
+
});
|
|
299
587
|
}
|
|
300
588
|
|
|
301
589
|
async function runScheduledTask(
|
|
302
|
-
|
|
303
|
-
channelId: string,
|
|
304
|
-
prompt: string,
|
|
305
|
-
taskId: number,
|
|
590
|
+
request: SchedulerDispatchRequest,
|
|
306
591
|
): Promise<void> {
|
|
592
|
+
const sourceLabel =
|
|
593
|
+
request.source === 'db-task'
|
|
594
|
+
? `schedule:${request.taskId ?? 'unknown'}`
|
|
595
|
+
: `schedule-job:${request.jobId ?? 'unknown'}`;
|
|
596
|
+
const resolvedDeliveryChannelId =
|
|
597
|
+
request.delivery.kind === 'channel'
|
|
598
|
+
? request.delivery.channelId
|
|
599
|
+
: request.delivery.kind === 'last-channel'
|
|
600
|
+
? resolveLastUsedDiscordChannelId()
|
|
601
|
+
: null;
|
|
602
|
+
|
|
603
|
+
if (request.actionKind === 'system_event') {
|
|
604
|
+
if (request.delivery.kind === 'webhook') {
|
|
605
|
+
await deliverWebhookMessage(
|
|
606
|
+
request.delivery.webhookUrl,
|
|
607
|
+
request.prompt,
|
|
608
|
+
`${sourceLabel}:system`,
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!resolvedDeliveryChannelId) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
'No Discord channel available for scheduled system event delivery.',
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
await deliverProactiveMessage(
|
|
618
|
+
resolvedDeliveryChannelId,
|
|
619
|
+
request.prompt,
|
|
620
|
+
`${sourceLabel}:system`,
|
|
621
|
+
);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const runChannelId =
|
|
626
|
+
request.channelId || resolvedDeliveryChannelId || 'scheduler';
|
|
627
|
+
const taskId = request.taskId ?? -1;
|
|
628
|
+
|
|
307
629
|
await runGatewayScheduledTask(
|
|
308
|
-
sessionId,
|
|
309
|
-
|
|
310
|
-
prompt,
|
|
630
|
+
request.sessionId,
|
|
631
|
+
runChannelId,
|
|
632
|
+
request.prompt,
|
|
311
633
|
taskId,
|
|
312
634
|
async (result) => {
|
|
313
|
-
|
|
635
|
+
if (request.delivery.kind === 'webhook') {
|
|
636
|
+
await deliverWebhookMessage(
|
|
637
|
+
request.delivery.webhookUrl,
|
|
638
|
+
result.text,
|
|
639
|
+
sourceLabel,
|
|
640
|
+
result.artifacts,
|
|
641
|
+
);
|
|
642
|
+
logger.info(
|
|
643
|
+
{
|
|
644
|
+
jobId: request.jobId,
|
|
645
|
+
taskId: request.taskId,
|
|
646
|
+
source: request.source,
|
|
647
|
+
delivery: 'webhook',
|
|
648
|
+
result: result.text,
|
|
649
|
+
artifactCount: result.artifacts?.length || 0,
|
|
650
|
+
},
|
|
651
|
+
'Scheduled task completed',
|
|
652
|
+
);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!resolvedDeliveryChannelId) {
|
|
657
|
+
throw new Error('No Discord channel available for scheduled delivery.');
|
|
658
|
+
}
|
|
659
|
+
await deliverProactiveMessage(
|
|
660
|
+
resolvedDeliveryChannelId,
|
|
661
|
+
result.text,
|
|
662
|
+
sourceLabel,
|
|
663
|
+
result.artifacts,
|
|
664
|
+
);
|
|
314
665
|
logger.info(
|
|
315
|
-
{
|
|
666
|
+
{
|
|
667
|
+
jobId: request.jobId,
|
|
668
|
+
taskId: request.taskId,
|
|
669
|
+
source: request.source,
|
|
670
|
+
channelId: resolvedDeliveryChannelId,
|
|
671
|
+
result: result.text,
|
|
672
|
+
artifactCount: result.artifacts?.length || 0,
|
|
673
|
+
},
|
|
316
674
|
'Scheduled task completed',
|
|
317
675
|
);
|
|
318
676
|
},
|
|
319
677
|
(error) => {
|
|
320
|
-
logger.error(
|
|
678
|
+
logger.error(
|
|
679
|
+
{
|
|
680
|
+
jobId: request.jobId,
|
|
681
|
+
taskId: request.taskId,
|
|
682
|
+
source: request.source,
|
|
683
|
+
delivery: request.delivery.kind,
|
|
684
|
+
error,
|
|
685
|
+
},
|
|
686
|
+
'Scheduled task failed',
|
|
687
|
+
);
|
|
321
688
|
},
|
|
689
|
+
request.sessionId,
|
|
322
690
|
);
|
|
323
691
|
}
|
|
324
692
|
|
|
@@ -332,6 +700,46 @@ function startOrRestartHeartbeat(): void {
|
|
|
332
700
|
});
|
|
333
701
|
}
|
|
334
702
|
|
|
703
|
+
function stopMemoryConsolidationScheduler(): void {
|
|
704
|
+
if (!memoryConsolidationTimer) return;
|
|
705
|
+
clearInterval(memoryConsolidationTimer);
|
|
706
|
+
memoryConsolidationTimer = null;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function startOrRestartMemoryConsolidationScheduler(): void {
|
|
710
|
+
stopMemoryConsolidationScheduler();
|
|
711
|
+
const intervalHours = Math.max(
|
|
712
|
+
0,
|
|
713
|
+
Math.trunc(getConfigSnapshot().memory.consolidationIntervalHours),
|
|
714
|
+
);
|
|
715
|
+
if (intervalHours <= 0) {
|
|
716
|
+
logger.info('Memory consolidation scheduler disabled');
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const intervalMs = intervalHours * 3_600_000;
|
|
721
|
+
memoryConsolidationTimer = setInterval(() => {
|
|
722
|
+
const { decayRate } = getConfigSnapshot().memory;
|
|
723
|
+
try {
|
|
724
|
+
const report = memoryService.consolidateMemories({ decayRate });
|
|
725
|
+
if (report.memoriesDecayed > 0) {
|
|
726
|
+
logger.info(
|
|
727
|
+
{
|
|
728
|
+
decayed: report.memoriesDecayed,
|
|
729
|
+
durationMs: report.durationMs,
|
|
730
|
+
decayRate,
|
|
731
|
+
},
|
|
732
|
+
'Memory consolidation completed',
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
logger.warn({ error, decayRate }, 'Memory consolidation failed');
|
|
737
|
+
}
|
|
738
|
+
}, intervalMs);
|
|
739
|
+
|
|
740
|
+
logger.info({ intervalHours }, 'Memory consolidation scheduled');
|
|
741
|
+
}
|
|
742
|
+
|
|
335
743
|
async function main(): Promise<void> {
|
|
336
744
|
logger.info('Starting HybridClaw gateway');
|
|
337
745
|
initDatabase();
|
|
@@ -343,9 +751,9 @@ async function main(): Promise<void> {
|
|
|
343
751
|
startObservabilityIngest();
|
|
344
752
|
detachConfigListener = onConfigChange((next, prev) => {
|
|
345
753
|
const shouldRestart =
|
|
346
|
-
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId
|
|
347
|
-
|
|
348
|
-
|
|
754
|
+
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId ||
|
|
755
|
+
next.heartbeat.intervalMs !== prev.heartbeat.intervalMs ||
|
|
756
|
+
next.heartbeat.enabled !== prev.heartbeat.enabled;
|
|
349
757
|
if (shouldRestart) {
|
|
350
758
|
logger.info(
|
|
351
759
|
{
|
|
@@ -358,9 +766,32 @@ async function main(): Promise<void> {
|
|
|
358
766
|
startOrRestartHeartbeat();
|
|
359
767
|
}
|
|
360
768
|
|
|
769
|
+
const schedulerChanged =
|
|
770
|
+
JSON.stringify(next.scheduler) !== JSON.stringify(prev.scheduler);
|
|
771
|
+
if (schedulerChanged) {
|
|
772
|
+
logger.info(
|
|
773
|
+
'Config changed, re-arming scheduler for updated scheduler.jobs',
|
|
774
|
+
);
|
|
775
|
+
rearmScheduler();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const memoryChanged =
|
|
779
|
+
JSON.stringify(next.memory) !== JSON.stringify(prev.memory);
|
|
780
|
+
if (memoryChanged) {
|
|
781
|
+
logger.info(
|
|
782
|
+
{
|
|
783
|
+
consolidationIntervalHours: next.memory.consolidationIntervalHours,
|
|
784
|
+
decayRate: next.memory.decayRate,
|
|
785
|
+
},
|
|
786
|
+
'Config changed, restarting memory consolidation scheduler',
|
|
787
|
+
);
|
|
788
|
+
startOrRestartMemoryConsolidationScheduler();
|
|
789
|
+
}
|
|
790
|
+
|
|
361
791
|
const shouldRestartObservability =
|
|
362
|
-
JSON.stringify(next.observability) !==
|
|
363
|
-
|
|
792
|
+
JSON.stringify(next.observability) !==
|
|
793
|
+
JSON.stringify(prev.observability) ||
|
|
794
|
+
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId;
|
|
364
795
|
if (!shouldRestartObservability) return;
|
|
365
796
|
|
|
366
797
|
logger.info(
|
|
@@ -374,6 +805,7 @@ async function main(): Promise<void> {
|
|
|
374
805
|
startObservabilityIngest();
|
|
375
806
|
});
|
|
376
807
|
startScheduler(runScheduledTask);
|
|
808
|
+
startOrRestartMemoryConsolidationScheduler();
|
|
377
809
|
proactiveFlushTimer = setInterval(() => {
|
|
378
810
|
void flushQueuedProactiveMessages().catch((err) => {
|
|
379
811
|
logger.warn({ err }, 'Failed to flush queued proactive messages');
|
|
@@ -383,7 +815,10 @@ async function main(): Promise<void> {
|
|
|
383
815
|
logger.warn({ err }, 'Initial proactive queue flush failed');
|
|
384
816
|
});
|
|
385
817
|
|
|
386
|
-
logger.info(
|
|
818
|
+
logger.info(
|
|
819
|
+
{ ...getGatewayStatus(), discord: !!DISCORD_TOKEN },
|
|
820
|
+
'HybridClaw gateway started',
|
|
821
|
+
);
|
|
387
822
|
}
|
|
388
823
|
|
|
389
824
|
main().catch((err) => {
|