@hybridaione/hybridclaw 0.2.2 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/gateway-types.ts
CHANGED
|
@@ -20,9 +20,27 @@ export interface GatewayChatResult {
|
|
|
20
20
|
arguments: string;
|
|
21
21
|
result: string;
|
|
22
22
|
durationMs: number;
|
|
23
|
+
isError?: boolean;
|
|
24
|
+
blocked?: boolean;
|
|
25
|
+
blockedReason?: string;
|
|
26
|
+
approvalTier?: 'green' | 'yellow' | 'red';
|
|
27
|
+
approvalBaseTier?: 'green' | 'yellow' | 'red';
|
|
28
|
+
approvalDecision?:
|
|
29
|
+
| 'auto'
|
|
30
|
+
| 'implicit'
|
|
31
|
+
| 'approved_once'
|
|
32
|
+
| 'approved_session'
|
|
33
|
+
| 'approved_agent'
|
|
34
|
+
| 'promoted'
|
|
35
|
+
| 'required'
|
|
36
|
+
| 'denied';
|
|
37
|
+
approvalActionKey?: string;
|
|
38
|
+
approvalReason?: string;
|
|
39
|
+
approvalRequestId?: string;
|
|
23
40
|
}>;
|
|
24
41
|
tokenUsage?: TokenUsageStats;
|
|
25
42
|
error?: string;
|
|
43
|
+
effectiveUserPrompt?: string;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export interface GatewayChatToolProgressEvent {
|
|
@@ -75,6 +93,18 @@ export interface GatewayCommandRequest {
|
|
|
75
93
|
args: string[];
|
|
76
94
|
}
|
|
77
95
|
|
|
96
|
+
export interface GatewaySchedulerJobStatus {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
description: string | null;
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
lastRun: string | null;
|
|
102
|
+
lastStatus: 'success' | 'error' | null;
|
|
103
|
+
nextRunAt: string | null;
|
|
104
|
+
disabled: boolean;
|
|
105
|
+
consecutiveErrors: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
export interface GatewayStatus {
|
|
79
109
|
status: 'ok';
|
|
80
110
|
pid?: number;
|
|
@@ -96,6 +126,9 @@ export interface GatewayStatus {
|
|
|
96
126
|
lastFailureAt: string | null;
|
|
97
127
|
lastError: string | null;
|
|
98
128
|
};
|
|
129
|
+
scheduler?: {
|
|
130
|
+
jobs: GatewaySchedulerJobStatus[];
|
|
131
|
+
};
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
export function renderGatewayCommand(result: GatewayCommandResult): string {
|
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,23 +37,29 @@ 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;
|
|
47
65
|
|
|
@@ -57,9 +75,14 @@ function buildArtifactAttachments(
|
|
|
57
75
|
for (const artifact of artifacts) {
|
|
58
76
|
try {
|
|
59
77
|
const content = fs.readFileSync(artifact.path);
|
|
60
|
-
attachments.push(
|
|
78
|
+
attachments.push(
|
|
79
|
+
new AttachmentBuilder(content, { name: artifact.filename }),
|
|
80
|
+
);
|
|
61
81
|
} catch (error) {
|
|
62
|
-
logger.warn(
|
|
82
|
+
logger.warn(
|
|
83
|
+
{ artifactPath: artifact.path, error },
|
|
84
|
+
'Failed to read artifact for Discord attachment',
|
|
85
|
+
);
|
|
63
86
|
}
|
|
64
87
|
}
|
|
65
88
|
return attachments;
|
|
@@ -75,7 +98,9 @@ function simplifyImageAttachmentNarration(
|
|
|
75
98
|
): string {
|
|
76
99
|
if (!text.trim() || !artifacts || artifacts.length === 0) return text;
|
|
77
100
|
|
|
78
|
-
const imageArtifacts = artifacts.filter((artifact) =>
|
|
101
|
+
const imageArtifacts = artifacts.filter((artifact) =>
|
|
102
|
+
artifact.mimeType.startsWith('image/'),
|
|
103
|
+
);
|
|
79
104
|
if (imageArtifacts.length === 0) return text;
|
|
80
105
|
|
|
81
106
|
const pathHints = new Set<string>();
|
|
@@ -88,14 +113,18 @@ function simplifyImageAttachmentNarration(
|
|
|
88
113
|
if (filename) pathHints.add(`.browser-artifacts/${filename}`);
|
|
89
114
|
}
|
|
90
115
|
|
|
91
|
-
const pathishLine =
|
|
92
|
-
|
|
116
|
+
const pathishLine =
|
|
117
|
+
/(^`?\s*(\.\/|\/|~\/|[a-zA-Z]:\\|\.browser-artifacts\/))|([\\/][^\\/\s]+\.[a-zA-Z0-9]{1,8})/;
|
|
118
|
+
const locationNarration =
|
|
119
|
+
/(workspace|saved to|find it at|located at|liegt unter|pfad|path)/i;
|
|
93
120
|
|
|
94
121
|
let removedPathNarration = false;
|
|
95
122
|
const keptLines: string[] = [];
|
|
96
123
|
for (const line of text.split('\n')) {
|
|
97
124
|
const normalizedLine = normalizePathForMatch(line);
|
|
98
|
-
const mentionsArtifact = Array.from(pathHints).some((hint) =>
|
|
125
|
+
const mentionsArtifact = Array.from(pathHints).some((hint) =>
|
|
126
|
+
normalizedLine.includes(hint),
|
|
127
|
+
);
|
|
99
128
|
const isPathLine = pathishLine.test(line.trim());
|
|
100
129
|
const isLocationNarration = locationNarration.test(line);
|
|
101
130
|
if (mentionsArtifact && (isPathLine || isLocationNarration)) {
|
|
@@ -107,7 +136,10 @@ function simplifyImageAttachmentNarration(
|
|
|
107
136
|
|
|
108
137
|
if (!removedPathNarration) return text;
|
|
109
138
|
|
|
110
|
-
const cleaned = keptLines
|
|
139
|
+
const cleaned = keptLines
|
|
140
|
+
.join('\n')
|
|
141
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
142
|
+
.trim();
|
|
111
143
|
if (cleaned) return cleaned;
|
|
112
144
|
return imageArtifacts.length === 1 ? 'Here it is.' : 'Here they are.';
|
|
113
145
|
}
|
|
@@ -120,7 +152,12 @@ async function deliverProactiveMessage(
|
|
|
120
152
|
): Promise<void> {
|
|
121
153
|
if (!isWithinActiveHours()) {
|
|
122
154
|
if (PROACTIVE_QUEUE_OUTSIDE_HOURS) {
|
|
123
|
-
const { queued, dropped } = enqueueProactiveMessage(
|
|
155
|
+
const { queued, dropped } = enqueueProactiveMessage(
|
|
156
|
+
channelId,
|
|
157
|
+
text,
|
|
158
|
+
source,
|
|
159
|
+
MAX_QUEUED_PROACTIVE_MESSAGES,
|
|
160
|
+
);
|
|
124
161
|
logger.info(
|
|
125
162
|
{
|
|
126
163
|
source,
|
|
@@ -140,7 +177,10 @@ async function deliverProactiveMessage(
|
|
|
140
177
|
}
|
|
141
178
|
return;
|
|
142
179
|
}
|
|
143
|
-
logger.info(
|
|
180
|
+
logger.info(
|
|
181
|
+
{ source, channelId, activeHours: proactiveWindowLabel() },
|
|
182
|
+
'Proactive message suppressed (outside active hours)',
|
|
183
|
+
);
|
|
144
184
|
return;
|
|
145
185
|
}
|
|
146
186
|
|
|
@@ -155,18 +195,59 @@ async function sendProactiveMessageNow(
|
|
|
155
195
|
): Promise<void> {
|
|
156
196
|
const attachments = buildArtifactAttachments(artifacts);
|
|
157
197
|
if (!DISCORD_TOKEN || !isDiscordChannelId(channelId)) {
|
|
158
|
-
logger.info(
|
|
198
|
+
logger.info(
|
|
199
|
+
{ source, channelId, text, artifactCount: attachments.length },
|
|
200
|
+
'Proactive message (no Discord delivery)',
|
|
201
|
+
);
|
|
159
202
|
return;
|
|
160
203
|
}
|
|
161
204
|
|
|
162
205
|
try {
|
|
163
206
|
await sendToChannel(channelId, text, attachments);
|
|
164
207
|
} catch (error) {
|
|
165
|
-
logger.warn(
|
|
208
|
+
logger.warn(
|
|
209
|
+
{ source, channelId, error, artifactCount: attachments.length },
|
|
210
|
+
'Failed to send proactive message to Discord channel',
|
|
211
|
+
);
|
|
166
212
|
logger.info({ source, channelId, text }, 'Proactive message fallback');
|
|
167
213
|
}
|
|
168
214
|
}
|
|
169
215
|
|
|
216
|
+
async function deliverWebhookMessage(
|
|
217
|
+
webhookUrl: string,
|
|
218
|
+
text: string,
|
|
219
|
+
source: string,
|
|
220
|
+
artifacts?: ArtifactMetadata[],
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const response = await fetch(webhookUrl, {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
text,
|
|
229
|
+
source,
|
|
230
|
+
artifactCount: artifacts?.length || 0,
|
|
231
|
+
artifacts: (artifacts || []).map((artifact) => ({
|
|
232
|
+
filename: artifact.filename,
|
|
233
|
+
mimeType: artifact.mimeType,
|
|
234
|
+
})),
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const body = await response.text().catch(() => '');
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Webhook delivery failed (${response.status}): ${body.slice(0, 300)}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveLastUsedDiscordChannelId(): string | null {
|
|
246
|
+
const channelId = getMostRecentSessionChannelId();
|
|
247
|
+
if (!channelId) return null;
|
|
248
|
+
return isDiscordChannelId(channelId) ? channelId : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
170
251
|
async function flushQueuedProactiveMessages(): Promise<void> {
|
|
171
252
|
if (!isWithinActiveHours()) return;
|
|
172
253
|
const pending = listQueuedProactiveMessages(MAX_QUEUED_PROACTIVE_MESSAGES);
|
|
@@ -178,7 +259,11 @@ async function flushQueuedProactiveMessages(): Promise<void> {
|
|
|
178
259
|
|
|
179
260
|
for (const item of pending) {
|
|
180
261
|
if (!isWithinActiveHours()) break;
|
|
181
|
-
await sendProactiveMessageNow(
|
|
262
|
+
await sendProactiveMessageNow(
|
|
263
|
+
item.channel_id,
|
|
264
|
+
item.text,
|
|
265
|
+
`${item.source}:queued`,
|
|
266
|
+
);
|
|
182
267
|
deleteQueuedProactiveMessage(item.id);
|
|
183
268
|
}
|
|
184
269
|
}
|
|
@@ -202,6 +287,7 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
202
287
|
context,
|
|
203
288
|
) => {
|
|
204
289
|
try {
|
|
290
|
+
let sawTextDelta = false;
|
|
205
291
|
const result = await handleGatewayMessage({
|
|
206
292
|
sessionId,
|
|
207
293
|
guildId,
|
|
@@ -211,15 +297,35 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
211
297
|
content,
|
|
212
298
|
media,
|
|
213
299
|
onTextDelta: (delta) => {
|
|
300
|
+
if (!sawTextDelta) {
|
|
301
|
+
sawTextDelta = true;
|
|
302
|
+
context.emitLifecyclePhase('streaming');
|
|
303
|
+
}
|
|
214
304
|
void context.stream.append(delta);
|
|
215
305
|
},
|
|
306
|
+
onToolProgress: (event) => {
|
|
307
|
+
if (sawTextDelta) return;
|
|
308
|
+
if (event.phase === 'start') {
|
|
309
|
+
context.emitLifecyclePhase('toolUse');
|
|
310
|
+
} else {
|
|
311
|
+
context.emitLifecyclePhase('thinking');
|
|
312
|
+
}
|
|
313
|
+
},
|
|
216
314
|
onProactiveMessage: async (message) => {
|
|
217
|
-
await deliverProactiveMessage(
|
|
315
|
+
await deliverProactiveMessage(
|
|
316
|
+
channelId,
|
|
317
|
+
message.text,
|
|
318
|
+
'delegate',
|
|
319
|
+
message.artifacts,
|
|
320
|
+
);
|
|
218
321
|
},
|
|
219
322
|
abortSignal: context.abortSignal,
|
|
220
323
|
});
|
|
221
324
|
if (result.status === 'error') {
|
|
222
|
-
const errorText = formatError(
|
|
325
|
+
const errorText = formatError(
|
|
326
|
+
'Agent Error',
|
|
327
|
+
result.error || 'Unknown error',
|
|
328
|
+
);
|
|
223
329
|
await context.stream.fail(errorText);
|
|
224
330
|
return;
|
|
225
331
|
}
|
|
@@ -239,7 +345,10 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
239
345
|
);
|
|
240
346
|
} catch (error) {
|
|
241
347
|
const text = error instanceof Error ? error.message : String(error);
|
|
242
|
-
logger.error(
|
|
348
|
+
logger.error(
|
|
349
|
+
{ error, sessionId, channelId },
|
|
350
|
+
'Discord message handling failed',
|
|
351
|
+
);
|
|
243
352
|
const errorText = formatError('Gateway Error', text);
|
|
244
353
|
await context.stream.fail(errorText);
|
|
245
354
|
}
|
|
@@ -269,7 +378,10 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
269
378
|
await reply(renderGatewayCommand(result));
|
|
270
379
|
} catch (error) {
|
|
271
380
|
const text = error instanceof Error ? error.message : String(error);
|
|
272
|
-
logger.error(
|
|
381
|
+
logger.error(
|
|
382
|
+
{ error, sessionId, channelId, args },
|
|
383
|
+
'Discord command handling failed',
|
|
384
|
+
);
|
|
273
385
|
await reply(formatError('Gateway Error', text));
|
|
274
386
|
}
|
|
275
387
|
},
|
|
@@ -278,47 +390,141 @@ async function startDiscordIntegration(): Promise<void> {
|
|
|
278
390
|
}
|
|
279
391
|
|
|
280
392
|
function setupShutdown(): void {
|
|
281
|
-
|
|
393
|
+
let shuttingDown = false;
|
|
394
|
+
const shutdown = async () => {
|
|
395
|
+
if (shuttingDown) return;
|
|
396
|
+
shuttingDown = true;
|
|
282
397
|
logger.info('Shutting down gateway...');
|
|
283
398
|
if (detachConfigListener) {
|
|
284
399
|
detachConfigListener();
|
|
285
400
|
detachConfigListener = null;
|
|
286
401
|
}
|
|
402
|
+
await setDiscordMaintenancePresence().catch((error) => {
|
|
403
|
+
logger.debug(
|
|
404
|
+
{ error },
|
|
405
|
+
'Failed to set Discord maintenance presence during shutdown',
|
|
406
|
+
);
|
|
407
|
+
});
|
|
287
408
|
stopHeartbeat();
|
|
288
409
|
stopObservabilityIngest();
|
|
289
410
|
stopAllContainers();
|
|
290
411
|
stopScheduler();
|
|
412
|
+
stopMemoryConsolidationScheduler();
|
|
291
413
|
if (proactiveFlushTimer) {
|
|
292
414
|
clearInterval(proactiveFlushTimer);
|
|
293
415
|
proactiveFlushTimer = null;
|
|
294
416
|
}
|
|
295
417
|
process.exit(0);
|
|
296
418
|
};
|
|
297
|
-
process.on('SIGINT',
|
|
298
|
-
|
|
419
|
+
process.on('SIGINT', () => {
|
|
420
|
+
void shutdown();
|
|
421
|
+
});
|
|
422
|
+
process.on('SIGTERM', () => {
|
|
423
|
+
void shutdown();
|
|
424
|
+
});
|
|
299
425
|
}
|
|
300
426
|
|
|
301
427
|
async function runScheduledTask(
|
|
302
|
-
|
|
303
|
-
channelId: string,
|
|
304
|
-
prompt: string,
|
|
305
|
-
taskId: number,
|
|
428
|
+
request: SchedulerDispatchRequest,
|
|
306
429
|
): Promise<void> {
|
|
430
|
+
const sourceLabel =
|
|
431
|
+
request.source === 'db-task'
|
|
432
|
+
? `schedule:${request.taskId ?? 'unknown'}`
|
|
433
|
+
: `schedule-job:${request.jobId ?? 'unknown'}`;
|
|
434
|
+
const resolvedDeliveryChannelId =
|
|
435
|
+
request.delivery.kind === 'channel'
|
|
436
|
+
? request.delivery.channelId
|
|
437
|
+
: request.delivery.kind === 'last-channel'
|
|
438
|
+
? resolveLastUsedDiscordChannelId()
|
|
439
|
+
: null;
|
|
440
|
+
|
|
441
|
+
if (request.actionKind === 'system_event') {
|
|
442
|
+
if (request.delivery.kind === 'webhook') {
|
|
443
|
+
await deliverWebhookMessage(
|
|
444
|
+
request.delivery.webhookUrl,
|
|
445
|
+
request.prompt,
|
|
446
|
+
`${sourceLabel}:system`,
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!resolvedDeliveryChannelId) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
'No Discord channel available for scheduled system event delivery.',
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
await deliverProactiveMessage(
|
|
456
|
+
resolvedDeliveryChannelId,
|
|
457
|
+
request.prompt,
|
|
458
|
+
`${sourceLabel}:system`,
|
|
459
|
+
);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const runChannelId =
|
|
464
|
+
request.channelId || resolvedDeliveryChannelId || 'scheduler';
|
|
465
|
+
const taskId = request.taskId ?? -1;
|
|
466
|
+
|
|
307
467
|
await runGatewayScheduledTask(
|
|
308
|
-
sessionId,
|
|
309
|
-
|
|
310
|
-
prompt,
|
|
468
|
+
request.sessionId,
|
|
469
|
+
runChannelId,
|
|
470
|
+
request.prompt,
|
|
311
471
|
taskId,
|
|
312
472
|
async (result) => {
|
|
313
|
-
|
|
473
|
+
if (request.delivery.kind === 'webhook') {
|
|
474
|
+
await deliverWebhookMessage(
|
|
475
|
+
request.delivery.webhookUrl,
|
|
476
|
+
result.text,
|
|
477
|
+
sourceLabel,
|
|
478
|
+
result.artifacts,
|
|
479
|
+
);
|
|
480
|
+
logger.info(
|
|
481
|
+
{
|
|
482
|
+
jobId: request.jobId,
|
|
483
|
+
taskId: request.taskId,
|
|
484
|
+
source: request.source,
|
|
485
|
+
delivery: 'webhook',
|
|
486
|
+
result: result.text,
|
|
487
|
+
artifactCount: result.artifacts?.length || 0,
|
|
488
|
+
},
|
|
489
|
+
'Scheduled task completed',
|
|
490
|
+
);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!resolvedDeliveryChannelId) {
|
|
495
|
+
throw new Error('No Discord channel available for scheduled delivery.');
|
|
496
|
+
}
|
|
497
|
+
await deliverProactiveMessage(
|
|
498
|
+
resolvedDeliveryChannelId,
|
|
499
|
+
result.text,
|
|
500
|
+
sourceLabel,
|
|
501
|
+
result.artifacts,
|
|
502
|
+
);
|
|
314
503
|
logger.info(
|
|
315
|
-
{
|
|
504
|
+
{
|
|
505
|
+
jobId: request.jobId,
|
|
506
|
+
taskId: request.taskId,
|
|
507
|
+
source: request.source,
|
|
508
|
+
channelId: resolvedDeliveryChannelId,
|
|
509
|
+
result: result.text,
|
|
510
|
+
artifactCount: result.artifacts?.length || 0,
|
|
511
|
+
},
|
|
316
512
|
'Scheduled task completed',
|
|
317
513
|
);
|
|
318
514
|
},
|
|
319
515
|
(error) => {
|
|
320
|
-
logger.error(
|
|
516
|
+
logger.error(
|
|
517
|
+
{
|
|
518
|
+
jobId: request.jobId,
|
|
519
|
+
taskId: request.taskId,
|
|
520
|
+
source: request.source,
|
|
521
|
+
delivery: request.delivery.kind,
|
|
522
|
+
error,
|
|
523
|
+
},
|
|
524
|
+
'Scheduled task failed',
|
|
525
|
+
);
|
|
321
526
|
},
|
|
527
|
+
request.sessionId,
|
|
322
528
|
);
|
|
323
529
|
}
|
|
324
530
|
|
|
@@ -332,6 +538,46 @@ function startOrRestartHeartbeat(): void {
|
|
|
332
538
|
});
|
|
333
539
|
}
|
|
334
540
|
|
|
541
|
+
function stopMemoryConsolidationScheduler(): void {
|
|
542
|
+
if (!memoryConsolidationTimer) return;
|
|
543
|
+
clearInterval(memoryConsolidationTimer);
|
|
544
|
+
memoryConsolidationTimer = null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function startOrRestartMemoryConsolidationScheduler(): void {
|
|
548
|
+
stopMemoryConsolidationScheduler();
|
|
549
|
+
const intervalHours = Math.max(
|
|
550
|
+
0,
|
|
551
|
+
Math.trunc(getConfigSnapshot().memory.consolidationIntervalHours),
|
|
552
|
+
);
|
|
553
|
+
if (intervalHours <= 0) {
|
|
554
|
+
logger.info('Memory consolidation scheduler disabled');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const intervalMs = intervalHours * 3_600_000;
|
|
559
|
+
memoryConsolidationTimer = setInterval(() => {
|
|
560
|
+
const { decayRate } = getConfigSnapshot().memory;
|
|
561
|
+
try {
|
|
562
|
+
const report = memoryService.consolidateMemories({ decayRate });
|
|
563
|
+
if (report.memoriesDecayed > 0) {
|
|
564
|
+
logger.info(
|
|
565
|
+
{
|
|
566
|
+
decayed: report.memoriesDecayed,
|
|
567
|
+
durationMs: report.durationMs,
|
|
568
|
+
decayRate,
|
|
569
|
+
},
|
|
570
|
+
'Memory consolidation completed',
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
logger.warn({ error, decayRate }, 'Memory consolidation failed');
|
|
575
|
+
}
|
|
576
|
+
}, intervalMs);
|
|
577
|
+
|
|
578
|
+
logger.info({ intervalHours }, 'Memory consolidation scheduled');
|
|
579
|
+
}
|
|
580
|
+
|
|
335
581
|
async function main(): Promise<void> {
|
|
336
582
|
logger.info('Starting HybridClaw gateway');
|
|
337
583
|
initDatabase();
|
|
@@ -343,9 +589,9 @@ async function main(): Promise<void> {
|
|
|
343
589
|
startObservabilityIngest();
|
|
344
590
|
detachConfigListener = onConfigChange((next, prev) => {
|
|
345
591
|
const shouldRestart =
|
|
346
|
-
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId
|
|
347
|
-
|
|
348
|
-
|
|
592
|
+
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId ||
|
|
593
|
+
next.heartbeat.intervalMs !== prev.heartbeat.intervalMs ||
|
|
594
|
+
next.heartbeat.enabled !== prev.heartbeat.enabled;
|
|
349
595
|
if (shouldRestart) {
|
|
350
596
|
logger.info(
|
|
351
597
|
{
|
|
@@ -358,9 +604,32 @@ async function main(): Promise<void> {
|
|
|
358
604
|
startOrRestartHeartbeat();
|
|
359
605
|
}
|
|
360
606
|
|
|
607
|
+
const schedulerChanged =
|
|
608
|
+
JSON.stringify(next.scheduler) !== JSON.stringify(prev.scheduler);
|
|
609
|
+
if (schedulerChanged) {
|
|
610
|
+
logger.info(
|
|
611
|
+
'Config changed, re-arming scheduler for updated scheduler.jobs',
|
|
612
|
+
);
|
|
613
|
+
rearmScheduler();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const memoryChanged =
|
|
617
|
+
JSON.stringify(next.memory) !== JSON.stringify(prev.memory);
|
|
618
|
+
if (memoryChanged) {
|
|
619
|
+
logger.info(
|
|
620
|
+
{
|
|
621
|
+
consolidationIntervalHours: next.memory.consolidationIntervalHours,
|
|
622
|
+
decayRate: next.memory.decayRate,
|
|
623
|
+
},
|
|
624
|
+
'Config changed, restarting memory consolidation scheduler',
|
|
625
|
+
);
|
|
626
|
+
startOrRestartMemoryConsolidationScheduler();
|
|
627
|
+
}
|
|
628
|
+
|
|
361
629
|
const shouldRestartObservability =
|
|
362
|
-
JSON.stringify(next.observability) !==
|
|
363
|
-
|
|
630
|
+
JSON.stringify(next.observability) !==
|
|
631
|
+
JSON.stringify(prev.observability) ||
|
|
632
|
+
next.hybridai.defaultChatbotId !== prev.hybridai.defaultChatbotId;
|
|
364
633
|
if (!shouldRestartObservability) return;
|
|
365
634
|
|
|
366
635
|
logger.info(
|
|
@@ -374,6 +643,7 @@ async function main(): Promise<void> {
|
|
|
374
643
|
startObservabilityIngest();
|
|
375
644
|
});
|
|
376
645
|
startScheduler(runScheduledTask);
|
|
646
|
+
startOrRestartMemoryConsolidationScheduler();
|
|
377
647
|
proactiveFlushTimer = setInterval(() => {
|
|
378
648
|
void flushQueuedProactiveMessages().catch((err) => {
|
|
379
649
|
logger.warn({ err }, 'Failed to flush queued proactive messages');
|
|
@@ -383,7 +653,10 @@ async function main(): Promise<void> {
|
|
|
383
653
|
logger.warn({ err }, 'Initial proactive queue flush failed');
|
|
384
654
|
});
|
|
385
655
|
|
|
386
|
-
logger.info(
|
|
656
|
+
logger.info(
|
|
657
|
+
{ ...getGatewayStatus(), discord: !!DISCORD_TOKEN },
|
|
658
|
+
'HybridClaw gateway started',
|
|
659
|
+
);
|
|
387
660
|
}
|
|
388
661
|
|
|
389
662
|
main().catch((err) => {
|