@dotsetlabs/dotclaw 1.1.0 → 1.2.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 +5 -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-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 +34 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +181 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +144 -221
- 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 +8 -4
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +15 -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
|
@@ -7,7 +7,8 @@ import { DATA_DIR, MAIN_GROUP_FOLDER, GROUPS_DIR, IPC_POLL_INTERVAL, TIMEZONE, C
|
|
|
7
7
|
// Load .env from the canonical location (~/.dotclaw/.env)
|
|
8
8
|
dotenv.config({ path: ENV_PATH });
|
|
9
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';
|
|
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';
|
|
@@ -35,10 +36,6 @@ function buildTriggerRegex(pattern) {
|
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
function isPreemptedError(err) {
|
|
39
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
-
return message.toLowerCase().includes('preempt');
|
|
41
|
-
}
|
|
42
39
|
function buildAvailableGroupsSnapshot() {
|
|
43
40
|
return Object.entries(registeredGroups).map(([jid, info]) => ({
|
|
44
41
|
jid,
|
|
@@ -158,22 +155,6 @@ const PROGRESS_MESSAGES = runtime.host.progress.messages.length > 0
|
|
|
158
155
|
const HEARTBEAT_ENABLED = runtime.host.heartbeat.enabled;
|
|
159
156
|
const HEARTBEAT_INTERVAL_MS = runtime.host.heartbeat.intervalMs;
|
|
160
157
|
const HEARTBEAT_GROUP_FOLDER = (runtime.host.heartbeat.groupFolder || MAIN_GROUP_FOLDER).trim() || MAIN_GROUP_FOLDER;
|
|
161
|
-
const BACKGROUND_TASKS_ENABLED = runtime.host.backgroundTasks.enabled;
|
|
162
|
-
const BACKGROUND_TRIGGER_REGEX = runtime.host.backgroundTasks.triggerRegex;
|
|
163
|
-
const BACKGROUND_TRIGGER = buildTriggerRegex(BACKGROUND_TRIGGER_REGEX);
|
|
164
|
-
const BACKGROUND_ACK_MESSAGE = runtime.host.backgroundTasks.ackMessage;
|
|
165
|
-
const BACKGROUND_TOOL_DENY = runtime.host.backgroundTasks.toolDeny;
|
|
166
|
-
const PREEMPT_ON_NEW_MESSAGE = runtime.host.backgroundTasks.preemptOnNewMessage;
|
|
167
|
-
function shouldRunInBackground(content) {
|
|
168
|
-
if (!BACKGROUND_TASKS_ENABLED)
|
|
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
|
-
}
|
|
177
158
|
// Initialize Telegram bot with extended timeout for long-running agent tasks
|
|
178
159
|
const telegrafBot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN, {
|
|
179
160
|
handlerTimeout: TELEGRAM_HANDLER_TIMEOUT_MS
|
|
@@ -186,7 +167,6 @@ let registeredGroups = {};
|
|
|
186
167
|
const TELEGRAM_MAX_MESSAGE_LENGTH = 4000;
|
|
187
168
|
const TELEGRAM_SEND_DELAY_MS = 250;
|
|
188
169
|
const messageQueues = new Map();
|
|
189
|
-
const inFlightRuns = new Map();
|
|
190
170
|
const draftSessions = new Map();
|
|
191
171
|
function parseTelegramStreamMode(value) {
|
|
192
172
|
const normalized = value.trim().toLowerCase();
|
|
@@ -577,13 +557,6 @@ async function finalizeStreamedMessage(msg, draftId, text) {
|
|
|
577
557
|
clearDraftSession(msg.chatId, draftId);
|
|
578
558
|
}
|
|
579
559
|
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
560
|
const existing = messageQueues.get(msg.chatId);
|
|
588
561
|
if (existing) {
|
|
589
562
|
existing.pendingMessage = msg;
|
|
@@ -659,18 +632,6 @@ async function processMessage(msg) {
|
|
|
659
632
|
${lines.join('\n')}
|
|
660
633
|
</messages>`;
|
|
661
634
|
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
635
|
const traceBase = createTraceBase({
|
|
675
636
|
chatId: msg.chatId,
|
|
676
637
|
groupFolder: group.folder,
|
|
@@ -841,186 +802,6 @@ ${lines.join('\n')}
|
|
|
841
802
|
}
|
|
842
803
|
return true;
|
|
843
804
|
}
|
|
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
805
|
function startIpcWatcher() {
|
|
1025
806
|
const ipcBaseDir = path.join(DATA_DIR, 'ipc');
|
|
1026
807
|
fs.mkdirSync(ipcBaseDir, { recursive: true });
|
|
@@ -1597,6 +1378,139 @@ async function processRequestIpc(data, sourceGroup, isMain) {
|
|
|
1597
1378
|
const groups = listRegisteredGroups();
|
|
1598
1379
|
return { id: requestId, ok: true, result: { groups } };
|
|
1599
1380
|
}
|
|
1381
|
+
case 'run_task': {
|
|
1382
|
+
const taskId = typeof payload.task_id === 'string' ? payload.task_id : '';
|
|
1383
|
+
if (!taskId) {
|
|
1384
|
+
return { id: requestId, ok: false, error: 'task_id is required.' };
|
|
1385
|
+
}
|
|
1386
|
+
const task = getTaskById(taskId);
|
|
1387
|
+
if (!task) {
|
|
1388
|
+
return { id: requestId, ok: false, error: 'Task not found.' };
|
|
1389
|
+
}
|
|
1390
|
+
if (!isMain && task.group_folder !== sourceGroup) {
|
|
1391
|
+
return { id: requestId, ok: false, error: 'Unauthorized task run attempt.' };
|
|
1392
|
+
}
|
|
1393
|
+
const result = await runTaskNow(taskId, {
|
|
1394
|
+
sendMessage: async (jid, text) => { await sendMessage(jid, text); },
|
|
1395
|
+
registeredGroups: () => registeredGroups,
|
|
1396
|
+
getSessions: () => sessions,
|
|
1397
|
+
setSession: (groupFolder, sessionId) => { sessions[groupFolder] = sessionId; }
|
|
1398
|
+
});
|
|
1399
|
+
return {
|
|
1400
|
+
id: requestId,
|
|
1401
|
+
ok: result.ok,
|
|
1402
|
+
result: { result: result.result ?? null },
|
|
1403
|
+
error: result.ok ? undefined : result.error
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
case 'spawn_job': {
|
|
1407
|
+
const prompt = typeof payload.prompt === 'string' ? payload.prompt.trim() : '';
|
|
1408
|
+
if (!prompt) {
|
|
1409
|
+
return { id: requestId, ok: false, error: 'prompt is required.' };
|
|
1410
|
+
}
|
|
1411
|
+
const targetGroup = (typeof payload.target_group === 'string' && isMain)
|
|
1412
|
+
? payload.target_group
|
|
1413
|
+
: sourceGroup;
|
|
1414
|
+
const groupEntry = Object.entries(registeredGroups).find(([, group]) => group.folder === targetGroup);
|
|
1415
|
+
if (!groupEntry) {
|
|
1416
|
+
return { id: requestId, ok: false, error: 'Target group not registered.' };
|
|
1417
|
+
}
|
|
1418
|
+
const [chatId, group] = groupEntry;
|
|
1419
|
+
const result = spawnBackgroundJob({
|
|
1420
|
+
prompt,
|
|
1421
|
+
groupFolder: group.folder,
|
|
1422
|
+
chatJid: chatId,
|
|
1423
|
+
contextMode: (payload.context_mode === 'group' || payload.context_mode === 'isolated')
|
|
1424
|
+
? payload.context_mode
|
|
1425
|
+
: undefined,
|
|
1426
|
+
timeoutMs: typeof payload.timeout_ms === 'number' ? payload.timeout_ms : undefined,
|
|
1427
|
+
maxToolSteps: typeof payload.max_tool_steps === 'number' ? payload.max_tool_steps : undefined,
|
|
1428
|
+
toolAllow: Array.isArray(payload.tool_allow) ? payload.tool_allow : undefined,
|
|
1429
|
+
toolDeny: Array.isArray(payload.tool_deny) ? payload.tool_deny : undefined,
|
|
1430
|
+
modelOverride: typeof payload.model_override === 'string' ? payload.model_override : undefined,
|
|
1431
|
+
priority: typeof payload.priority === 'number' ? payload.priority : undefined,
|
|
1432
|
+
tags: Array.isArray(payload.tags) ? payload.tags : undefined,
|
|
1433
|
+
parentTraceId: typeof payload.parent_trace_id === 'string' ? payload.parent_trace_id : undefined,
|
|
1434
|
+
parentMessageId: typeof payload.parent_message_id === 'string' ? payload.parent_message_id : undefined
|
|
1435
|
+
});
|
|
1436
|
+
return {
|
|
1437
|
+
id: requestId,
|
|
1438
|
+
ok: result.ok,
|
|
1439
|
+
result: result.ok ? { job_id: result.jobId } : undefined,
|
|
1440
|
+
error: result.ok ? undefined : result.error
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
case 'job_status': {
|
|
1444
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1445
|
+
if (!jobId) {
|
|
1446
|
+
return { id: requestId, ok: false, error: 'job_id is required.' };
|
|
1447
|
+
}
|
|
1448
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1449
|
+
if (!job) {
|
|
1450
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1451
|
+
}
|
|
1452
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1453
|
+
return { id: requestId, ok: false, error: 'Unauthorized job status request.' };
|
|
1454
|
+
}
|
|
1455
|
+
return { id: requestId, ok: true, result: { job } };
|
|
1456
|
+
}
|
|
1457
|
+
case 'list_jobs': {
|
|
1458
|
+
const targetGroup = (typeof payload.target_group === 'string' && isMain)
|
|
1459
|
+
? payload.target_group
|
|
1460
|
+
: sourceGroup;
|
|
1461
|
+
const statusRaw = typeof payload.status === 'string' ? payload.status : undefined;
|
|
1462
|
+
const allowedStatuses = ['queued', 'running', 'succeeded', 'failed', 'canceled', 'timed_out'];
|
|
1463
|
+
const status = statusRaw && allowedStatuses.includes(statusRaw)
|
|
1464
|
+
? statusRaw
|
|
1465
|
+
: undefined;
|
|
1466
|
+
const limit = typeof payload.limit === 'number' ? payload.limit : undefined;
|
|
1467
|
+
const jobs = listBackgroundJobsForGroup({ groupFolder: targetGroup, status, limit });
|
|
1468
|
+
return { id: requestId, ok: true, result: { jobs } };
|
|
1469
|
+
}
|
|
1470
|
+
case 'cancel_job': {
|
|
1471
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1472
|
+
if (!jobId) {
|
|
1473
|
+
return { id: requestId, ok: false, error: 'job_id is required.' };
|
|
1474
|
+
}
|
|
1475
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1476
|
+
if (!job) {
|
|
1477
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1478
|
+
}
|
|
1479
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1480
|
+
return { id: requestId, ok: false, error: 'Unauthorized job cancel attempt.' };
|
|
1481
|
+
}
|
|
1482
|
+
const result = cancelBackgroundJob(jobId);
|
|
1483
|
+
return { id: requestId, ok: result.ok, error: result.error };
|
|
1484
|
+
}
|
|
1485
|
+
case 'job_update': {
|
|
1486
|
+
const jobId = typeof payload.job_id === 'string' ? payload.job_id : '';
|
|
1487
|
+
const message = typeof payload.message === 'string' ? payload.message.trim() : '';
|
|
1488
|
+
const levelRaw = typeof payload.level === 'string' ? payload.level : 'progress';
|
|
1489
|
+
const allowedLevels = ['info', 'progress', 'warn', 'error'];
|
|
1490
|
+
const level = allowedLevels.includes(levelRaw)
|
|
1491
|
+
? levelRaw
|
|
1492
|
+
: 'progress';
|
|
1493
|
+
if (!jobId || !message) {
|
|
1494
|
+
return { id: requestId, ok: false, error: 'job_id and message are required.' };
|
|
1495
|
+
}
|
|
1496
|
+
const job = getBackgroundJobStatus(jobId);
|
|
1497
|
+
if (!job) {
|
|
1498
|
+
return { id: requestId, ok: false, error: 'Job not found.' };
|
|
1499
|
+
}
|
|
1500
|
+
if (!isMain && job.group_folder !== sourceGroup) {
|
|
1501
|
+
return { id: requestId, ok: false, error: 'Unauthorized job update attempt.' };
|
|
1502
|
+
}
|
|
1503
|
+
const result = recordBackgroundJobUpdate({
|
|
1504
|
+
jobId,
|
|
1505
|
+
level,
|
|
1506
|
+
message,
|
|
1507
|
+
data: typeof payload.data === 'object' && payload.data ? payload.data : undefined
|
|
1508
|
+
});
|
|
1509
|
+
if (result.ok && payload.notify === true && job.chat_jid) {
|
|
1510
|
+
await sendMessage(job.chat_jid, `Background job ${job.id} update:\n\n${message}`);
|
|
1511
|
+
}
|
|
1512
|
+
return { id: requestId, ok: result.ok, error: result.error };
|
|
1513
|
+
}
|
|
1600
1514
|
default:
|
|
1601
1515
|
return { id: requestId, ok: false, error: `Unknown request type: ${data.type}` };
|
|
1602
1516
|
}
|
|
@@ -2054,6 +1968,15 @@ async function main() {
|
|
|
2054
1968
|
setGroupSession(groupFolder, sessionId);
|
|
2055
1969
|
}
|
|
2056
1970
|
});
|
|
1971
|
+
startBackgroundJobLoop({
|
|
1972
|
+
sendMessage: sendMessageForScheduler,
|
|
1973
|
+
registeredGroups: () => registeredGroups,
|
|
1974
|
+
getSessions: () => sessions,
|
|
1975
|
+
setSession: (groupFolder, sessionId) => {
|
|
1976
|
+
sessions[groupFolder] = sessionId;
|
|
1977
|
+
setGroupSession(groupFolder, sessionId);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
2057
1980
|
startIpcWatcher();
|
|
2058
1981
|
startMaintenanceLoop();
|
|
2059
1982
|
startHeartbeatLoop();
|