@dotsetlabs/dotclaw 1.1.0 → 1.3.0
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/README.md +1 -0
- package/config-examples/runtime.json +19 -0
- package/container/agent-runner/package-lock.json +2 -2
- package/container/agent-runner/package.json +1 -1
- package/container/agent-runner/src/container-protocol.ts +3 -0
- package/container/agent-runner/src/index.ts +20 -1
- package/container/agent-runner/src/ipc.ts +35 -0
- package/container/agent-runner/src/tools.ts +115 -0
- package/dist/agent-context.d.ts +1 -0
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +8 -1
- package/dist/agent-context.js.map +1 -1
- package/dist/agent-execution.d.ts +7 -0
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +12 -6
- package/dist/agent-execution.js.map +1 -1
- package/dist/background-job-classifier.d.ts +16 -0
- package/dist/background-job-classifier.d.ts.map +1 -0
- package/dist/background-job-classifier.js +124 -0
- package/dist/background-job-classifier.js.map +1 -0
- package/dist/background-jobs.d.ts +47 -0
- package/dist/background-jobs.d.ts.map +1 -0
- package/dist/background-jobs.js +406 -0
- package/dist/background-jobs.js.map +1 -0
- package/dist/container-protocol.d.ts +3 -0
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts +1 -0
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +2 -2
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +1 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +8 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +39 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +198 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +257 -224
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +1 -0
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +9 -0
- package/dist/metrics.js.map +1 -1
- package/dist/runtime-config.d.ts +22 -4
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +29 -5
- package/dist/runtime-config.js.map +1 -1
- package/dist/task-scheduler.d.ts +5 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +121 -2
- package/dist/task-scheduler.js.map +1 -1
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +6 -0
- package/dist/tool-policy.js.map +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import path from 'path';
|
|
|
6
6
|
import { DATA_DIR, MAIN_GROUP_FOLDER, GROUPS_DIR, IPC_POLL_INTERVAL, TIMEZONE, CONTAINER_MODE, WARM_START_ENABLED, ENV_PATH } from './config.js';
|
|
7
7
|
// Load .env from the canonical location (~/.dotclaw/.env)
|
|
8
8
|
dotenv.config({ path: ENV_PATH });
|
|
9
|
-
import { initDatabase, storeMessage, getMessagesSinceCursor, getChatState, updateChatState, createTask, updateTask, deleteTask, getTaskById, getAllGroupSessions, setGroupSession, deleteGroupSession, pauseTasksForGroup, linkMessageToTrace, getTraceIdForMessage, recordUserFeedback } from './db.js';
|
|
10
|
-
import { startSchedulerLoop } from './task-scheduler.js';
|
|
9
|
+
import { initDatabase, storeMessage, upsertChat, getMessagesSinceCursor, getChatState, updateChatState, createTask, updateTask, deleteTask, getTaskById, getAllGroupSessions, setGroupSession, deleteGroupSession, pauseTasksForGroup, linkMessageToTrace, getTraceIdForMessage, recordUserFeedback } from './db.js';
|
|
10
|
+
import { startSchedulerLoop, runTaskNow } from './task-scheduler.js';
|
|
11
|
+
import { startBackgroundJobLoop, spawnBackgroundJob, getBackgroundJobStatus, listBackgroundJobsForGroup, cancelBackgroundJob, recordBackgroundJobUpdate } from './background-jobs.js';
|
|
11
12
|
import { loadJson, saveJson, isSafeGroupFolder } from './utils.js';
|
|
12
13
|
import { writeTrace } from './trace-writer.js';
|
|
13
14
|
import { formatTelegramMessage, TELEGRAM_PARSE_MODE } from './telegram-format.js';
|
|
@@ -24,6 +25,7 @@ import { createTraceBase, executeAgentRun, recordAgentTelemetry, AgentExecutionE
|
|
|
24
25
|
import { logger } from './logger.js';
|
|
25
26
|
import { startDashboard, setTelegramConnected, setLastMessageTime, setMessageQueueDepth } from './dashboard.js';
|
|
26
27
|
import { humanizeError } from './error-messages.js';
|
|
28
|
+
import { classifyBackgroundJob } from './background-job-classifier.js';
|
|
27
29
|
const runtime = loadRuntimeConfig();
|
|
28
30
|
function buildTriggerRegex(pattern) {
|
|
29
31
|
if (!pattern)
|
|
@@ -35,10 +37,6 @@ function buildTriggerRegex(pattern) {
|
|
|
35
37
|
return null;
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
|
-
function isPreemptedError(err) {
|
|
39
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
-
return message.toLowerCase().includes('preempt');
|
|
41
|
-
}
|
|
42
40
|
function buildAvailableGroupsSnapshot() {
|
|
43
41
|
return Object.entries(registeredGroups).map(([jid, info]) => ({
|
|
44
42
|
jid,
|
|
@@ -158,22 +156,14 @@ const PROGRESS_MESSAGES = runtime.host.progress.messages.length > 0
|
|
|
158
156
|
const HEARTBEAT_ENABLED = runtime.host.heartbeat.enabled;
|
|
159
157
|
const HEARTBEAT_INTERVAL_MS = runtime.host.heartbeat.intervalMs;
|
|
160
158
|
const HEARTBEAT_GROUP_FOLDER = (runtime.host.heartbeat.groupFolder || MAIN_GROUP_FOLDER).trim() || MAIN_GROUP_FOLDER;
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return false;
|
|
170
|
-
const trimmed = content.trim();
|
|
171
|
-
if (!trimmed)
|
|
172
|
-
return false;
|
|
173
|
-
if (BACKGROUND_TRIGGER && BACKGROUND_TRIGGER.test(trimmed))
|
|
174
|
-
return true;
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
159
|
+
const BACKGROUND_JOBS_ENABLED = runtime.host.backgroundJobs.enabled;
|
|
160
|
+
const AUTO_SPAWN_CONFIG = runtime.host.backgroundJobs.autoSpawn;
|
|
161
|
+
const AUTO_SPAWN_ENABLED = BACKGROUND_JOBS_ENABLED && AUTO_SPAWN_CONFIG.enabled;
|
|
162
|
+
const AUTO_SPAWN_FOREGROUND_TIMEOUT_MS = AUTO_SPAWN_CONFIG.foregroundTimeoutMs;
|
|
163
|
+
const AUTO_SPAWN_ON_TIMEOUT = AUTO_SPAWN_CONFIG.onTimeout;
|
|
164
|
+
const AUTO_SPAWN_ON_TOOL_LIMIT = AUTO_SPAWN_CONFIG.onToolLimit;
|
|
165
|
+
const AUTO_SPAWN_CLASSIFIER_ENABLED = AUTO_SPAWN_CONFIG.classifier.enabled;
|
|
166
|
+
const TOOL_CALL_FALLBACK_PATTERN = /tool calls? but did not get a final response/i;
|
|
177
167
|
// Initialize Telegram bot with extended timeout for long-running agent tasks
|
|
178
168
|
const telegrafBot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN, {
|
|
179
169
|
handlerTimeout: TELEGRAM_HANDLER_TIMEOUT_MS
|
|
@@ -186,7 +176,6 @@ let registeredGroups = {};
|
|
|
186
176
|
const TELEGRAM_MAX_MESSAGE_LENGTH = 4000;
|
|
187
177
|
const TELEGRAM_SEND_DELAY_MS = 250;
|
|
188
178
|
const messageQueues = new Map();
|
|
189
|
-
const inFlightRuns = new Map();
|
|
190
179
|
const draftSessions = new Map();
|
|
191
180
|
function parseTelegramStreamMode(value) {
|
|
192
181
|
const normalized = value.trim().toLowerCase();
|
|
@@ -577,13 +566,6 @@ async function finalizeStreamedMessage(msg, draftId, text) {
|
|
|
577
566
|
clearDraftSession(msg.chatId, draftId);
|
|
578
567
|
}
|
|
579
568
|
function enqueueMessage(msg) {
|
|
580
|
-
if (PREEMPT_ON_NEW_MESSAGE) {
|
|
581
|
-
const inFlight = inFlightRuns.get(msg.chatId);
|
|
582
|
-
if (inFlight && !inFlight.controller.signal.aborted) {
|
|
583
|
-
logger.warn({ chatId: msg.chatId }, 'Preempting in-flight agent run');
|
|
584
|
-
inFlight.controller.abort();
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
569
|
const existing = messageQueues.get(msg.chatId);
|
|
588
570
|
if (existing) {
|
|
589
571
|
existing.pendingMessage = msg;
|
|
@@ -659,18 +641,6 @@ async function processMessage(msg) {
|
|
|
659
641
|
${lines.join('\n')}
|
|
660
642
|
</messages>`;
|
|
661
643
|
const lastMessage = missedMessages[missedMessages.length - 1];
|
|
662
|
-
if (lastMessage && shouldRunInBackground(lastMessage.content)) {
|
|
663
|
-
logger.info({ group: group.name }, 'Routing message to background task');
|
|
664
|
-
updateChatState(msg.chatId, lastMessage.timestamp, lastMessage.id);
|
|
665
|
-
await sendMessage(msg.chatId, BACKGROUND_ACK_MESSAGE, { messageThreadId: msg.messageThreadId });
|
|
666
|
-
void runBackgroundTask({
|
|
667
|
-
msg,
|
|
668
|
-
group,
|
|
669
|
-
prompt,
|
|
670
|
-
missedMessages
|
|
671
|
-
});
|
|
672
|
-
return true;
|
|
673
|
-
}
|
|
674
644
|
const traceBase = createTraceBase({
|
|
675
645
|
chatId: msg.chatId,
|
|
676
646
|
groupFolder: group.folder,
|
|
@@ -687,6 +657,69 @@ ${lines.join('\n')}
|
|
|
687
657
|
let output = null;
|
|
688
658
|
let context = null;
|
|
689
659
|
let errorMessage = null;
|
|
660
|
+
const isTimeoutError = (value) => {
|
|
661
|
+
if (!value)
|
|
662
|
+
return false;
|
|
663
|
+
return /timed out|timeout/i.test(value);
|
|
664
|
+
};
|
|
665
|
+
const maybeAutoSpawn = async (reason, detail) => {
|
|
666
|
+
if (!AUTO_SPAWN_ENABLED)
|
|
667
|
+
return false;
|
|
668
|
+
if (reason === 'timeout' && !AUTO_SPAWN_ON_TIMEOUT)
|
|
669
|
+
return false;
|
|
670
|
+
if (reason === 'tool_limit' && !AUTO_SPAWN_ON_TOOL_LIMIT)
|
|
671
|
+
return false;
|
|
672
|
+
const result = spawnBackgroundJob({
|
|
673
|
+
prompt,
|
|
674
|
+
groupFolder: group.folder,
|
|
675
|
+
chatJid: msg.chatId,
|
|
676
|
+
contextMode: 'group',
|
|
677
|
+
tags: ['auto-spawn', reason],
|
|
678
|
+
parentTraceId: traceBase.trace_id,
|
|
679
|
+
parentMessageId: msg.messageId
|
|
680
|
+
});
|
|
681
|
+
if (!result.ok || !result.jobId) {
|
|
682
|
+
logger.warn({ chatId: msg.chatId, reason, error: result.error }, 'Auto-spawn background job failed');
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
const detailLine = detail ? `\n\nReason: ${detail}` : '';
|
|
686
|
+
await sendMessage(msg.chatId, `Queued this as background job ${result.jobId}. I'll report back when it's done. You can keep chatting while it runs.${detailLine}`, { messageThreadId: msg.messageThreadId });
|
|
687
|
+
if (lastMessage) {
|
|
688
|
+
updateChatState(msg.chatId, lastMessage.timestamp, lastMessage.id);
|
|
689
|
+
}
|
|
690
|
+
if (draftId) {
|
|
691
|
+
clearDraftSession(msg.chatId, draftId);
|
|
692
|
+
}
|
|
693
|
+
return true;
|
|
694
|
+
};
|
|
695
|
+
if (AUTO_SPAWN_ENABLED && AUTO_SPAWN_CLASSIFIER_ENABLED && lastMessage) {
|
|
696
|
+
try {
|
|
697
|
+
const classifierResult = await classifyBackgroundJob({
|
|
698
|
+
lastMessage,
|
|
699
|
+
recentMessages: missedMessages,
|
|
700
|
+
isGroup: msg.isGroup,
|
|
701
|
+
chatType: msg.chatType
|
|
702
|
+
});
|
|
703
|
+
logger.info({
|
|
704
|
+
chatId: msg.chatId,
|
|
705
|
+
decision: classifierResult.shouldBackground,
|
|
706
|
+
confidence: classifierResult.confidence,
|
|
707
|
+
latencyMs: classifierResult.latencyMs,
|
|
708
|
+
model: classifierResult.model,
|
|
709
|
+
reason: classifierResult.reason,
|
|
710
|
+
error: classifierResult.error
|
|
711
|
+
}, 'Background job classifier decision');
|
|
712
|
+
if (classifierResult.shouldBackground) {
|
|
713
|
+
const autoSpawned = await maybeAutoSpawn('classifier');
|
|
714
|
+
if (autoSpawned) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch (err) {
|
|
720
|
+
logger.warn({ chatId: msg.chatId, err }, 'Background job classifier failed');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
690
723
|
const progressNotifier = createProgressNotifier({
|
|
691
724
|
enabled: PROGRESS_ENABLED && !streamingEnabled,
|
|
692
725
|
initialDelayMs: PROGRESS_INITIAL_MS,
|
|
@@ -717,7 +750,10 @@ ${lines.join('\n')}
|
|
|
717
750
|
minChars: TELEGRAM_STREAM_MIN_CHARS
|
|
718
751
|
}
|
|
719
752
|
: undefined,
|
|
720
|
-
availableGroups: buildAvailableGroupsSnapshot()
|
|
753
|
+
availableGroups: buildAvailableGroupsSnapshot(),
|
|
754
|
+
timeoutMs: AUTO_SPAWN_ENABLED && AUTO_SPAWN_FOREGROUND_TIMEOUT_MS > 0
|
|
755
|
+
? AUTO_SPAWN_FOREGROUND_TIMEOUT_MS
|
|
756
|
+
: undefined
|
|
721
757
|
});
|
|
722
758
|
output = execution.output;
|
|
723
759
|
context = execution.context;
|
|
@@ -768,6 +804,12 @@ ${lines.join('\n')}
|
|
|
768
804
|
source: traceBase.source
|
|
769
805
|
});
|
|
770
806
|
}
|
|
807
|
+
if (isTimeoutError(message)) {
|
|
808
|
+
const autoSpawned = await maybeAutoSpawn('timeout', message);
|
|
809
|
+
if (autoSpawned) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
771
813
|
const userMessage = humanizeError(errorMessage || 'Unknown error');
|
|
772
814
|
await sendMessage(msg.chatId, userMessage, { messageThreadId: msg.messageThreadId });
|
|
773
815
|
if (draftId) {
|
|
@@ -788,7 +830,14 @@ ${lines.join('\n')}
|
|
|
788
830
|
});
|
|
789
831
|
}
|
|
790
832
|
logger.error({ group: group.name, error: output.error }, 'Container agent error');
|
|
791
|
-
const
|
|
833
|
+
const errorText = errorMessage || output.error || 'Unknown error';
|
|
834
|
+
if (isTimeoutError(errorText)) {
|
|
835
|
+
const autoSpawned = await maybeAutoSpawn('timeout', errorText);
|
|
836
|
+
if (autoSpawned) {
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const userMessage = humanizeError(errorText);
|
|
792
841
|
await sendMessage(msg.chatId, userMessage, { messageThreadId: msg.messageThreadId });
|
|
793
842
|
if (draftId) {
|
|
794
843
|
clearDraftSession(msg.chatId, draftId);
|
|
@@ -819,6 +868,22 @@ ${lines.join('\n')}
|
|
|
819
868
|
}
|
|
820
869
|
}
|
|
821
870
|
else if (output.tool_calls && output.tool_calls.length > 0) {
|
|
871
|
+
const toolLimitHit = !output.result || !output.result.trim() || TOOL_CALL_FALLBACK_PATTERN.test(output.result);
|
|
872
|
+
if (toolLimitHit) {
|
|
873
|
+
const autoSpawned = await maybeAutoSpawn('tool_limit', 'Tool-call step limit reached');
|
|
874
|
+
if (autoSpawned) {
|
|
875
|
+
if (context) {
|
|
876
|
+
recordAgentTelemetry({
|
|
877
|
+
traceBase,
|
|
878
|
+
output,
|
|
879
|
+
context,
|
|
880
|
+
metricsSource: 'telegram',
|
|
881
|
+
toolAuditSource: 'message'
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
822
887
|
await sendMessage(msg.chatId, 'I hit my tool-call step limit before I could finish. If you want me to keep going, please narrow the scope or ask for a specific subtask.', { messageThreadId: msg.messageThreadId });
|
|
823
888
|
if (draftId) {
|
|
824
889
|
clearDraftSession(msg.chatId, draftId);
|
|
@@ -841,186 +906,6 @@ ${lines.join('\n')}
|
|
|
841
906
|
}
|
|
842
907
|
return true;
|
|
843
908
|
}
|
|
844
|
-
async function runBackgroundTask(params) {
|
|
845
|
-
const { msg, group, prompt, missedMessages } = params;
|
|
846
|
-
const traceBase = createTraceBase({
|
|
847
|
-
chatId: msg.chatId,
|
|
848
|
-
groupFolder: group.folder,
|
|
849
|
-
userId: msg.senderId,
|
|
850
|
-
inputText: prompt,
|
|
851
|
-
source: 'dotclaw-background'
|
|
852
|
-
});
|
|
853
|
-
const recallQuery = missedMessages.map(entry => entry.content).join('\n');
|
|
854
|
-
const runId = `bg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
855
|
-
const abortController = new AbortController();
|
|
856
|
-
if (PREEMPT_ON_NEW_MESSAGE) {
|
|
857
|
-
inFlightRuns.set(msg.chatId, { controller: abortController, runId });
|
|
858
|
-
}
|
|
859
|
-
let output = null;
|
|
860
|
-
let context = null;
|
|
861
|
-
let errorMessage = null;
|
|
862
|
-
try {
|
|
863
|
-
const execution = await executeAgentRun({
|
|
864
|
-
group,
|
|
865
|
-
prompt,
|
|
866
|
-
chatJid: msg.chatId,
|
|
867
|
-
userId: msg.senderId,
|
|
868
|
-
userName: msg.senderName,
|
|
869
|
-
recallQuery: recallQuery || msg.content,
|
|
870
|
-
recallMaxResults: MEMORY_RECALL_MAX_RESULTS,
|
|
871
|
-
recallMaxTokens: MEMORY_RECALL_MAX_TOKENS,
|
|
872
|
-
toolDeny: BACKGROUND_TOOL_DENY,
|
|
873
|
-
sessionId: runId,
|
|
874
|
-
persistSession: false,
|
|
875
|
-
useGroupLock: false,
|
|
876
|
-
abortSignal: abortController.signal,
|
|
877
|
-
isBackgroundTask: true,
|
|
878
|
-
availableGroups: buildAvailableGroupsSnapshot()
|
|
879
|
-
});
|
|
880
|
-
output = execution.output;
|
|
881
|
-
context = execution.context;
|
|
882
|
-
if (output.status === 'error') {
|
|
883
|
-
errorMessage = output.error || 'Unknown error';
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
catch (err) {
|
|
887
|
-
if (err instanceof AgentExecutionError) {
|
|
888
|
-
context = err.context;
|
|
889
|
-
errorMessage = err.message;
|
|
890
|
-
}
|
|
891
|
-
else {
|
|
892
|
-
errorMessage = err instanceof Error ? err.message : String(err);
|
|
893
|
-
}
|
|
894
|
-
if (abortController.signal.aborted || isPreemptedError(err)) {
|
|
895
|
-
if (context) {
|
|
896
|
-
recordAgentTelemetry({
|
|
897
|
-
traceBase,
|
|
898
|
-
output,
|
|
899
|
-
context,
|
|
900
|
-
metricsSource: 'telegram',
|
|
901
|
-
toolAuditSource: 'background',
|
|
902
|
-
errorMessage: 'preempted'
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
writeTrace({
|
|
907
|
-
trace_id: traceBase.trace_id,
|
|
908
|
-
timestamp: traceBase.timestamp,
|
|
909
|
-
created_at: traceBase.created_at,
|
|
910
|
-
chat_id: traceBase.chat_id,
|
|
911
|
-
group_folder: traceBase.group_folder,
|
|
912
|
-
user_id: traceBase.user_id,
|
|
913
|
-
input_text: traceBase.input_text,
|
|
914
|
-
output_text: null,
|
|
915
|
-
model_id: 'unknown',
|
|
916
|
-
memory_recall: [],
|
|
917
|
-
error_code: 'preempted',
|
|
918
|
-
source: traceBase.source
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
logger.warn({ chatId: msg.chatId }, 'Background run preempted; skipping response');
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
logger.error({ group: group.name, err }, 'Background agent error');
|
|
925
|
-
}
|
|
926
|
-
finally {
|
|
927
|
-
if (PREEMPT_ON_NEW_MESSAGE) {
|
|
928
|
-
const current = inFlightRuns.get(msg.chatId);
|
|
929
|
-
if (current?.runId === runId) {
|
|
930
|
-
inFlightRuns.delete(msg.chatId);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
if (!output) {
|
|
935
|
-
if (context) {
|
|
936
|
-
recordAgentTelemetry({
|
|
937
|
-
traceBase,
|
|
938
|
-
output: null,
|
|
939
|
-
context,
|
|
940
|
-
metricsSource: 'telegram',
|
|
941
|
-
toolAuditSource: 'background',
|
|
942
|
-
errorMessage: errorMessage || 'No output from background agent',
|
|
943
|
-
errorType: 'agent'
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
recordError('agent');
|
|
948
|
-
writeTrace({
|
|
949
|
-
trace_id: traceBase.trace_id,
|
|
950
|
-
timestamp: traceBase.timestamp,
|
|
951
|
-
created_at: traceBase.created_at,
|
|
952
|
-
chat_id: traceBase.chat_id,
|
|
953
|
-
group_folder: traceBase.group_folder,
|
|
954
|
-
user_id: traceBase.user_id,
|
|
955
|
-
input_text: traceBase.input_text,
|
|
956
|
-
output_text: null,
|
|
957
|
-
model_id: 'unknown',
|
|
958
|
-
memory_recall: [],
|
|
959
|
-
error_code: errorMessage || 'No output from background agent',
|
|
960
|
-
source: traceBase.source
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
if (output.status === 'error') {
|
|
966
|
-
if (abortController.signal.aborted || isPreemptedError(output.error)) {
|
|
967
|
-
if (context) {
|
|
968
|
-
recordAgentTelemetry({
|
|
969
|
-
traceBase,
|
|
970
|
-
output,
|
|
971
|
-
context,
|
|
972
|
-
metricsSource: 'telegram',
|
|
973
|
-
toolAuditSource: 'background',
|
|
974
|
-
errorMessage: 'preempted'
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
logger.warn({ chatId: msg.chatId }, 'Background run preempted; skipping response');
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
if (context) {
|
|
981
|
-
recordAgentTelemetry({
|
|
982
|
-
traceBase,
|
|
983
|
-
output,
|
|
984
|
-
context,
|
|
985
|
-
metricsSource: 'telegram',
|
|
986
|
-
toolAuditSource: 'background',
|
|
987
|
-
errorMessage: errorMessage || output.error || 'Unknown error',
|
|
988
|
-
errorType: 'agent'
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
logger.error({ group: group.name, error: output.error }, 'Background agent error');
|
|
992
|
-
const userMessage = humanizeError(errorMessage || output.error || 'Unknown error');
|
|
993
|
-
await sendMessage(msg.chatId, userMessage, { messageThreadId: msg.messageThreadId });
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
if (output.result && output.result.trim()) {
|
|
997
|
-
const sendResult = await sendMessage(msg.chatId, output.result, { messageThreadId: msg.messageThreadId });
|
|
998
|
-
// Link the sent message to the trace for feedback tracking
|
|
999
|
-
if (sendResult.messageId) {
|
|
1000
|
-
try {
|
|
1001
|
-
linkMessageToTrace(sendResult.messageId, msg.chatId, traceBase.trace_id);
|
|
1002
|
-
}
|
|
1003
|
-
catch {
|
|
1004
|
-
// Don't fail if linking fails
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
else if (output.tool_calls && output.tool_calls.length > 0) {
|
|
1009
|
-
await sendMessage(msg.chatId, 'I hit my tool-call step limit before I could finish. If you want me to keep going, please narrow the scope or ask for a specific subtask.', { messageThreadId: msg.messageThreadId });
|
|
1010
|
-
}
|
|
1011
|
-
else {
|
|
1012
|
-
await sendMessage(msg.chatId, "I wasn't able to generate a response this time. Please try again or rephrase your request.", { messageThreadId: msg.messageThreadId });
|
|
1013
|
-
}
|
|
1014
|
-
if (context) {
|
|
1015
|
-
recordAgentTelemetry({
|
|
1016
|
-
traceBase,
|
|
1017
|
-
output,
|
|
1018
|
-
context,
|
|
1019
|
-
metricsSource: 'telegram',
|
|
1020
|
-
toolAuditSource: 'background'
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
909
|
function startIpcWatcher() {
|
|
1025
910
|
const ipcBaseDir = path.join(DATA_DIR, 'ipc');
|
|
1026
911
|
fs.mkdirSync(ipcBaseDir, { recursive: true });
|
|
@@ -1597,6 +1482,139 @@ async function processRequestIpc(data, sourceGroup, isMain) {
|
|
|
1597
1482
|
const groups = listRegisteredGroups();
|
|
1598
1483
|
return { id: requestId, ok: true, result: { groups } };
|
|
1599
1484
|
}
|
|
1485
|
+
case 'run_task': {
|
|
1486
|
+
const taskId = typeof payload.task_id === 'string' ? payload.task_id : '';
|
|
1487
|
+
if (!taskId) {
|
|
1488
|
+
return { id: requestId, ok: false, error: 'task_id is required.' };
|
|
1489
|
+
}
|
|
1490
|
+
const task = getTaskById(taskId);
|
|
1491
|
+
if (!task) {
|
|
1492
|
+
return { id: requestId, ok: false, error: 'Task not found.' };
|
|
1493
|
+
}
|
|
1494
|
+
if (!isMain && task.group_folder !== sourceGroup) {
|
|
1495
|
+
return { id: requestId, ok: false, error: 'Unauthorized task run attempt.' };
|
|
1496
|
+
}
|
|
1497
|
+
const result = await runTaskNow(taskId, {
|
|
1498
|
+
sendMessage: async (jid, text) => { await sendMessage(jid, text); },
|
|
1499
|
+
registeredGroups: () => registeredGroups,
|
|
1500
|
+
getSessions: () => sessions,
|
|
1501
|
+
setSession: (groupFolder, sessionId) => { sessions[groupFolder] = sessionId; }
|
|
1502
|
+
});
|
|
1503
|
+
return {
|
|
1504
|
+
id: requestId,
|
|
1505
|
+
ok: result.ok,
|
|
1506
|
+
result: { result: result.result ?? null },
|
|
1507
|
+
error: result.ok ? undefined : result.error
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
case 'spawn_job': {
|
|
1511
|
+
const prompt = typeof payload.prompt === 'string' ? payload.prompt.trim() : '';
|
|
1512
|
+
if (!prompt) {
|
|
1513
|
+
return { id: requestId, ok: false, error: 'prompt is required.' };
|
|
1514
|
+
}
|
|
1515
|
+
const targetGroup = (typeof payload.target_group === 'string' && isMain)
|
|
1516
|
+
? payload.target_group
|
|
1517
|
+
: sourceGroup;
|
|
1518
|
+
const groupEntry = Object.entries(registeredGroups).find(([, group]) => group.folder === targetGroup);
|
|
1519
|
+
if (!groupEntry) {
|
|
1520
|
+
return { id: requestId, ok: false, error: 'Target group not registered.' };
|
|
1521
|
+
}
|
|
1522
|
+
const [chatId, group] = groupEntry;
|
|
1523
|
+
const result = spawnBackgroundJob({
|
|
1524
|
+
prompt,
|
|
1525
|
+
groupFolder: group.folder,
|
|
1526
|
+
chatJid: chatId,
|
|
1527
|
+
contextMode: (payload.context_mode === 'group' || payload.context_mode === 'isolated')
|
|
1528
|
+
? payload.context_mode
|
|
1529
|
+
: undefined,
|
|
1530
|
+
timeoutMs: typeof payload.timeout_ms === 'number' ? payload.timeout_ms : undefined,
|
|
1531
|
+
maxToolSteps: typeof payload.max_tool_steps === 'number' ? payload.max_tool_steps : undefined,
|
|
1532
|
+
toolAllow: Array.isArray(payload.tool_allow) ? payload.tool_allow : undefined,
|
|
1533
|
+
toolDeny: Array.isArray(payload.tool_deny) ? payload.tool_deny : undefined,
|
|
1534
|
+
modelOverride: typeof payload.model_override === 'string' ? payload.model_override : undefined,
|
|
1535
|
+
priority: typeof payload.priority === 'number' ? payload.priority : undefined,
|
|
1536
|
+
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
|
1537
|
+
parentTraceId: typeof payload.parent_trace_id === 'string' ? payload.parent_trace_id : undefined,
|
|
1538
|
+
parentMessageId: typeof payload.parent_message_id === 'string' ? payload.parent_message_id : undefined
|
|
1539
|
+
});
|
|
1540
|
+
return {
|
|
1541
|
+
id: requestId,
|
|
1542
|
+
ok: result.ok,
|
|
1543
|
+
result: result.ok ? { job_id: result.jobId } : undefined,
|
|
1544
|
+
error: result.ok ? undefined : result.error
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
case 'job_status': {
|
|
1548
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1549
|
+
if (!jobId) {
|
|
1550
|
+
return { id: requestId, ok: false, error: 'job_id is required.' };
|
|
1551
|
+
}
|
|
1552
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1553
|
+
if (!job) {
|
|
1554
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1555
|
+
}
|
|
1556
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1557
|
+
return { id: requestId, ok: false, error: 'Unauthorized job status request.' };
|
|
1558
|
+
}
|
|
1559
|
+
return { id: requestId, ok: true, result: { job } };
|
|
1560
|
+
}
|
|
1561
|
+
case 'list_jobs': {
|
|
1562
|
+
const targetGroup = (typeof payload.target_group === 'string' && isMain)
|
|
1563
|
+
? payload.target_group
|
|
1564
|
+
: sourceGroup;
|
|
1565
|
+
const statusRaw = typeof payload.status === 'string' ? payload.status : undefined;
|
|
1566
|
+
const allowedStatuses = ['queued', 'running', 'succeeded', 'failed', 'canceled', 'timed_out'];
|
|
1567
|
+
const status = statusRaw && allowedStatuses.includes(statusRaw)
|
|
1568
|
+
? statusRaw
|
|
1569
|
+
: undefined;
|
|
1570
|
+
const limit = typeof payload.limit === 'number' ? payload.limit : undefined;
|
|
1571
|
+
const jobs = listBackgroundJobsForGroup({ groupFolder: targetGroup, status, limit });
|
|
1572
|
+
return { id: requestId, ok: true, result: { jobs } };
|
|
1573
|
+
}
|
|
1574
|
+
case 'cancel_job': {
|
|
1575
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1576
|
+
if (!jobId) {
|
|
1577
|
+
return { id: requestId, ok: false, error: 'job_id is required.' };
|
|
1578
|
+
}
|
|
1579
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1580
|
+
if (!job) {
|
|
1581
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1582
|
+
}
|
|
1583
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1584
|
+
return { id: requestId, ok: false, error: 'Unauthorized job cancel attempt.' };
|
|
1585
|
+
}
|
|
1586
|
+
const result = cancelBackgroundJob(jobId);
|
|
1587
|
+
return { id: requestId, ok: result.ok, error: result.error };
|
|
1588
|
+
}
|
|
1589
|
+
case 'job_update': {
|
|
1590
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1591
|
+
const message = typeof payload.message === 'string' ? payload.message.trim() : '';
|
|
1592
|
+
const levelRaw = typeof payload.level === 'string' ? payload.level : 'progress';
|
|
1593
|
+
const allowedLevels = ['info', 'progress', 'warn', 'error'];
|
|
1594
|
+
const level = allowedLevels.includes(levelRaw)
|
|
1595
|
+
? levelRaw
|
|
1596
|
+
: 'progress';
|
|
1597
|
+
if (!jobId || !message) {
|
|
1598
|
+
return { id: requestId, ok: false, error: 'job_id and message are required.' };
|
|
1599
|
+
}
|
|
1600
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1601
|
+
if (!job) {
|
|
1602
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1603
|
+
}
|
|
1604
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1605
|
+
return { id: requestId, ok: false, error: 'Unauthorized job update attempt.' };
|
|
1606
|
+
}
|
|
1607
|
+
const result = recordBackgroundJobUpdate({
|
|
1608
|
+
jobId,
|
|
1609
|
+
level,
|
|
1610
|
+
message,
|
|
1611
|
+
data: typeof payload.data === 'object' && payload.data ? payload.data : undefined
|
|
1612
|
+
});
|
|
1613
|
+
if (result.ok && payload.notify === true && job.chat_jid) {
|
|
1614
|
+
await sendMessage(job.chat_jid, `Background job ${job.id} update:\n\n${message}`);
|
|
1615
|
+
}
|
|
1616
|
+
return { id: requestId, ok: result.ok, error: result.error };
|
|
1617
|
+
}
|
|
1600
1618
|
default:
|
|
1601
1619
|
return { id: requestId, ok: false, error: `Unknown request type: ${data.type}` };
|
|
1602
1620
|
}
|
|
@@ -1903,6 +1921,11 @@ function setupTelegramHandlers() {
|
|
|
1903
1921
|
const isPrivate = chatType === 'private';
|
|
1904
1922
|
const senderId = String(ctx.from?.id || ctx.chat.id);
|
|
1905
1923
|
const senderName = ctx.from?.first_name || ctx.from?.username || 'User';
|
|
1924
|
+
const chatName = ('title' in ctx.chat && ctx.chat.title)
|
|
1925
|
+
|| ('username' in ctx.chat && ctx.chat.username)
|
|
1926
|
+
|| ctx.from?.first_name
|
|
1927
|
+
|| ctx.from?.username
|
|
1928
|
+
|| senderName;
|
|
1906
1929
|
const content = ctx.message.text;
|
|
1907
1930
|
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
1908
1931
|
const messageId = String(ctx.message.message_id);
|
|
@@ -1911,6 +1934,7 @@ function setupTelegramHandlers() {
|
|
|
1911
1934
|
logger.info({ chatId, isGroup, senderName }, `Telegram message: ${content.substring(0, 50)}...`);
|
|
1912
1935
|
try {
|
|
1913
1936
|
// Store message in database
|
|
1937
|
+
upsertChat({ chatId, name: chatName, lastMessageTime: timestamp });
|
|
1914
1938
|
storeMessage(String(ctx.message.message_id), chatId, senderId, senderName, content, timestamp, false);
|
|
1915
1939
|
}
|
|
1916
1940
|
catch (error) {
|
|
@@ -2054,6 +2078,15 @@ async function main() {
|
|
|
2054
2078
|
setGroupSession(groupFolder, sessionId);
|
|
2055
2079
|
}
|
|
2056
2080
|
});
|
|
2081
|
+
startBackgroundJobLoop({
|
|
2082
|
+
sendMessage: sendMessageForScheduler,
|
|
2083
|
+
registeredGroups: () => registeredGroups,
|
|
2084
|
+
getSessions: () => sessions,
|
|
2085
|
+
setSession: (groupFolder, sessionId) => {
|
|
2086
|
+
sessions[groupFolder] = sessionId;
|
|
2087
|
+
setGroupSession(groupFolder, sessionId);
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2057
2090
|
startIpcWatcher();
|
|
2058
2091
|
startMaintenanceLoop();
|
|
2059
2092
|
startHeartbeatLoop();
|