@dotsetlabs/dotclaw 1.2.0 → 1.4.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 +9 -0
- package/config-examples/runtime.json +119 -2
- package/container/agent-runner/src/agent-config.ts +20 -2
- package/container/agent-runner/src/container-protocol.ts +11 -0
- package/container/agent-runner/src/index.ts +39 -7
- package/container/agent-runner/src/tools.ts +84 -5
- package/dist/agent-context.d.ts +5 -0
- package/dist/agent-context.d.ts.map +1 -1
- package/dist/agent-context.js +19 -8
- package/dist/agent-context.js.map +1 -1
- package/dist/agent-execution.d.ts +6 -0
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +61 -4
- package/dist/agent-execution.js.map +1 -1
- package/dist/background-job-classifier.d.ts +20 -0
- package/dist/background-job-classifier.d.ts.map +1 -0
- package/dist/background-job-classifier.js +145 -0
- package/dist/background-job-classifier.js.map +1 -0
- package/dist/background-jobs.d.ts.map +1 -1
- package/dist/background-jobs.js +81 -4
- package/dist/background-jobs.js.map +1 -1
- package/dist/cli.js +343 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -3
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +11 -0
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +9 -1
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +5 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +58 -8
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +16 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +53 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +403 -30
- package/dist/index.js.map +1 -1
- package/dist/json-helpers.d.ts +6 -0
- package/dist/json-helpers.d.ts.map +1 -0
- package/dist/json-helpers.js +17 -0
- package/dist/json-helpers.js.map +1 -0
- package/dist/logger.d.ts +0 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/metrics.d.ts +3 -0
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +35 -1
- package/dist/metrics.js.map +1 -1
- package/dist/planner-probe.d.ts +14 -0
- package/dist/planner-probe.d.ts.map +1 -0
- package/dist/planner-probe.js +97 -0
- package/dist/planner-probe.js.map +1 -0
- package/dist/progress.d.ts +27 -0
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +151 -0
- package/dist/progress.js.map +1 -1
- package/dist/request-router.d.ts +34 -0
- package/dist/request-router.d.ts.map +1 -0
- package/dist/request-router.js +148 -0
- package/dist/request-router.js.map +1 -0
- package/dist/runtime-config.d.ts +81 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +190 -13
- package/dist/runtime-config.js.map +1 -1
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +56 -9
- package/dist/task-scheduler.js.map +1 -1
- package/dist/trace-writer.d.ts +1 -0
- package/dist/trace-writer.d.ts.map +1 -1
- package/dist/trace-writer.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ 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';
|
|
9
|
+
import { initDatabase, storeMessage, upsertChat, getMessagesSinceCursor, getChatState, updateChatState, createTask, updateTask, deleteTask, getTaskById, getAllGroupSessions, setGroupSession, deleteGroupSession, pauseTasksForGroup, getBackgroundJobQueuePosition, getBackgroundJobQueueDepth, linkMessageToTrace, getTraceIdForMessage, recordUserFeedback } from './db.js';
|
|
10
10
|
import { startSchedulerLoop, runTaskNow } from './task-scheduler.js';
|
|
11
11
|
import { startBackgroundJobLoop, spawnBackgroundJob, getBackgroundJobStatus, listBackgroundJobsForGroup, cancelBackgroundJob, recordBackgroundJobUpdate } from './background-jobs.js';
|
|
12
12
|
import { loadJson, saveJson, isSafeGroupFolder } from './utils.js';
|
|
@@ -14,10 +14,10 @@ import { writeTrace } from './trace-writer.js';
|
|
|
14
14
|
import { formatTelegramMessage, TELEGRAM_PARSE_MODE } from './telegram-format.js';
|
|
15
15
|
import { initMemoryStore, getMemoryStats, upsertMemoryItems, searchMemories, listMemories, forgetMemories, cleanupExpiredMemories } from './memory-store.js';
|
|
16
16
|
import { startEmbeddingWorker } from './memory-embeddings.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createProgressManager, DEFAULT_PROGRESS_MESSAGES, DEFAULT_PROGRESS_STAGES, formatProgressWithPlan, formatPlanStepList } from './progress.js';
|
|
18
18
|
import { parseAdminCommand } from './admin-commands.js';
|
|
19
19
|
import { loadModelRegistry, saveModelRegistry } from './model-registry.js';
|
|
20
|
-
import { startMetricsServer, recordMessage, recordError } from './metrics.js';
|
|
20
|
+
import { startMetricsServer, recordMessage, recordError, recordRoutingDecision, recordStageLatency } from './metrics.js';
|
|
21
21
|
import { startMaintenanceLoop } from './maintenance.js';
|
|
22
22
|
import { warmGroupContainer, startDaemonHealthCheckLoop } from './container-runner.js';
|
|
23
23
|
import { loadRuntimeConfig } from './runtime-config.js';
|
|
@@ -25,6 +25,9 @@ import { createTraceBase, executeAgentRun, recordAgentTelemetry, AgentExecutionE
|
|
|
25
25
|
import { logger } from './logger.js';
|
|
26
26
|
import { startDashboard, setTelegramConnected, setLastMessageTime, setMessageQueueDepth } from './dashboard.js';
|
|
27
27
|
import { humanizeError } from './error-messages.js';
|
|
28
|
+
import { classifyBackgroundJob } from './background-job-classifier.js';
|
|
29
|
+
import { routeRequest, routePrompt } from './request-router.js';
|
|
30
|
+
import { probePlanner } from './planner-probe.js';
|
|
28
31
|
const runtime = loadRuntimeConfig();
|
|
29
32
|
function buildTriggerRegex(pattern) {
|
|
30
33
|
if (!pattern)
|
|
@@ -145,16 +148,17 @@ const TELEGRAM_STREAM_MIN_CHARS = runtime.host.telegram.streamMinChars;
|
|
|
145
148
|
const MEMORY_RECALL_MAX_RESULTS = runtime.host.memory.recall.maxResults;
|
|
146
149
|
const MEMORY_RECALL_MAX_TOKENS = runtime.host.memory.recall.maxTokens;
|
|
147
150
|
const INPUT_MESSAGE_MAX_CHARS = runtime.host.telegram.inputMessageMaxChars;
|
|
148
|
-
const PROGRESS_ENABLED = runtime.host.progress.enabled;
|
|
149
|
-
const PROGRESS_INITIAL_MS = runtime.host.progress.initialMs;
|
|
150
|
-
const PROGRESS_INTERVAL_MS = runtime.host.progress.intervalMs;
|
|
151
|
-
const PROGRESS_MAX_UPDATES = runtime.host.progress.maxUpdates;
|
|
152
|
-
const PROGRESS_MESSAGES = runtime.host.progress.messages.length > 0
|
|
153
|
-
? runtime.host.progress.messages
|
|
154
|
-
: DEFAULT_PROGRESS_MESSAGES;
|
|
155
151
|
const HEARTBEAT_ENABLED = runtime.host.heartbeat.enabled;
|
|
156
152
|
const HEARTBEAT_INTERVAL_MS = runtime.host.heartbeat.intervalMs;
|
|
157
153
|
const HEARTBEAT_GROUP_FOLDER = (runtime.host.heartbeat.groupFolder || MAIN_GROUP_FOLDER).trim() || MAIN_GROUP_FOLDER;
|
|
154
|
+
const BACKGROUND_JOBS_ENABLED = runtime.host.backgroundJobs.enabled;
|
|
155
|
+
const AUTO_SPAWN_CONFIG = runtime.host.backgroundJobs.autoSpawn;
|
|
156
|
+
const AUTO_SPAWN_ENABLED = BACKGROUND_JOBS_ENABLED && AUTO_SPAWN_CONFIG.enabled;
|
|
157
|
+
const AUTO_SPAWN_FOREGROUND_TIMEOUT_MS = AUTO_SPAWN_CONFIG.foregroundTimeoutMs;
|
|
158
|
+
const AUTO_SPAWN_ON_TIMEOUT = AUTO_SPAWN_CONFIG.onTimeout;
|
|
159
|
+
const AUTO_SPAWN_ON_TOOL_LIMIT = AUTO_SPAWN_CONFIG.onToolLimit;
|
|
160
|
+
const AUTO_SPAWN_CLASSIFIER_ENABLED = AUTO_SPAWN_CONFIG.classifier.enabled;
|
|
161
|
+
const TOOL_CALL_FALLBACK_PATTERN = /tool calls? but did not get a final response/i;
|
|
158
162
|
// Initialize Telegram bot with extended timeout for long-running agent tasks
|
|
159
163
|
const telegrafBot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN, {
|
|
160
164
|
handlerTimeout: TELEGRAM_HANDLER_TIMEOUT_MS
|
|
@@ -167,6 +171,64 @@ let registeredGroups = {};
|
|
|
167
171
|
const TELEGRAM_MAX_MESSAGE_LENGTH = 4000;
|
|
168
172
|
const TELEGRAM_SEND_DELAY_MS = 250;
|
|
169
173
|
const messageQueues = new Map();
|
|
174
|
+
const activeRuns = new Map();
|
|
175
|
+
function isCancelMessage(content) {
|
|
176
|
+
if (!content)
|
|
177
|
+
return false;
|
|
178
|
+
const normalized = content.trim().toLowerCase();
|
|
179
|
+
return normalized === 'cancel'
|
|
180
|
+
|| normalized === 'stop'
|
|
181
|
+
|| normalized === 'abort'
|
|
182
|
+
|| normalized === 'cancel request'
|
|
183
|
+
|| normalized === 'stop request';
|
|
184
|
+
}
|
|
185
|
+
function inferProgressStage(params) {
|
|
186
|
+
const content = params.content.toLowerCase();
|
|
187
|
+
const tools = params.plannerTools.map(tool => tool.toLowerCase());
|
|
188
|
+
const hasWebTool = tools.some(tool => tool.includes('web') || tool.includes('search') || tool.includes('fetch'));
|
|
189
|
+
const hasCodeTool = tools.some(tool => tool.includes('bash') || tool.includes('edit') || tool.includes('write') || tool.includes('git'));
|
|
190
|
+
if (params.enablePlanner)
|
|
191
|
+
return 'planning';
|
|
192
|
+
if (hasWebTool || /research|search|browse|web|site|docs/.test(content))
|
|
193
|
+
return 'searching';
|
|
194
|
+
if (hasCodeTool || /build|code|implement|refactor|fix|debug/.test(content))
|
|
195
|
+
return 'coding';
|
|
196
|
+
return 'drafting';
|
|
197
|
+
}
|
|
198
|
+
function estimateForegroundMs(params) {
|
|
199
|
+
if (typeof params.routing.estimatedMinutes === 'number' && Number.isFinite(params.routing.estimatedMinutes)) {
|
|
200
|
+
return Math.max(1000, params.routing.estimatedMinutes * 60_000);
|
|
201
|
+
}
|
|
202
|
+
const baseChars = params.content.length;
|
|
203
|
+
if (baseChars === 0)
|
|
204
|
+
return null;
|
|
205
|
+
const stepFactor = params.plannerSteps.length > 0 ? params.plannerSteps.length * 6000 : 0;
|
|
206
|
+
const toolFactor = params.plannerTools.length > 0 ? params.plannerTools.length * 8000 : 0;
|
|
207
|
+
const lengthFactor = Math.min(60_000, Math.max(3000, Math.round(baseChars / 3)));
|
|
208
|
+
const profileFactor = params.routing.profile === 'deep' ? 1.4 : 1;
|
|
209
|
+
return Math.round((lengthFactor + stepFactor + toolFactor) * profileFactor);
|
|
210
|
+
}
|
|
211
|
+
function inferPlanStepIndex(stage, totalSteps) {
|
|
212
|
+
if (!Number.isFinite(totalSteps) || totalSteps <= 0)
|
|
213
|
+
return null;
|
|
214
|
+
const normalized = stage.trim().toLowerCase();
|
|
215
|
+
if (!normalized)
|
|
216
|
+
return 1;
|
|
217
|
+
switch (normalized) {
|
|
218
|
+
case 'planning':
|
|
219
|
+
return 1;
|
|
220
|
+
case 'searching':
|
|
221
|
+
return Math.min(2, totalSteps);
|
|
222
|
+
case 'coding':
|
|
223
|
+
return Math.min(Math.max(2, Math.ceil(totalSteps * 0.6)), totalSteps);
|
|
224
|
+
case 'drafting':
|
|
225
|
+
return Math.min(Math.max(2, Math.ceil(totalSteps * 0.8)), totalSteps);
|
|
226
|
+
case 'finalizing':
|
|
227
|
+
return totalSteps;
|
|
228
|
+
default:
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
170
232
|
const draftSessions = new Map();
|
|
171
233
|
function parseTelegramStreamMode(value) {
|
|
172
234
|
const normalized = value.trim().toLowerCase();
|
|
@@ -557,6 +619,17 @@ async function finalizeStreamedMessage(msg, draftId, text) {
|
|
|
557
619
|
clearDraftSession(msg.chatId, draftId);
|
|
558
620
|
}
|
|
559
621
|
function enqueueMessage(msg) {
|
|
622
|
+
if (isCancelMessage(msg.content)) {
|
|
623
|
+
const controller = activeRuns.get(msg.chatId);
|
|
624
|
+
if (controller) {
|
|
625
|
+
controller.abort();
|
|
626
|
+
activeRuns.delete(msg.chatId);
|
|
627
|
+
void sendMessage(msg.chatId, 'Canceled the current request.', { messageThreadId: msg.messageThreadId });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
void sendMessage(msg.chatId, 'There is no active request to cancel.', { messageThreadId: msg.messageThreadId });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
560
633
|
const existing = messageQueues.get(msg.chatId);
|
|
561
634
|
if (existing) {
|
|
562
635
|
existing.pendingMessage = msg;
|
|
@@ -632,6 +705,20 @@ async function processMessage(msg) {
|
|
|
632
705
|
${lines.join('\n')}
|
|
633
706
|
</messages>`;
|
|
634
707
|
const lastMessage = missedMessages[missedMessages.length - 1];
|
|
708
|
+
const routingStartedAt = Date.now();
|
|
709
|
+
const routingDecision = routeRequest({
|
|
710
|
+
prompt,
|
|
711
|
+
lastMessage
|
|
712
|
+
});
|
|
713
|
+
recordRoutingDecision(routingDecision.profile);
|
|
714
|
+
const routerMs = Date.now() - routingStartedAt;
|
|
715
|
+
recordStageLatency('router', routerMs, 'telegram');
|
|
716
|
+
logger.info({
|
|
717
|
+
chatId: msg.chatId,
|
|
718
|
+
profile: routingDecision.profile,
|
|
719
|
+
reason: routingDecision.reason,
|
|
720
|
+
shouldBackground: routingDecision.shouldBackground
|
|
721
|
+
}, 'Routing decision');
|
|
635
722
|
const traceBase = createTraceBase({
|
|
636
723
|
chatId: msg.chatId,
|
|
637
724
|
groupFolder: group.folder,
|
|
@@ -648,17 +735,224 @@ ${lines.join('\n')}
|
|
|
648
735
|
let output = null;
|
|
649
736
|
let context = null;
|
|
650
737
|
let errorMessage = null;
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
738
|
+
const isTimeoutError = (value) => {
|
|
739
|
+
if (!value)
|
|
740
|
+
return false;
|
|
741
|
+
return /timed out|timeout/i.test(value);
|
|
742
|
+
};
|
|
743
|
+
const shouldPlannerProbe = () => {
|
|
744
|
+
const config = runtime.host.routing.plannerProbe;
|
|
745
|
+
if (!config.enabled)
|
|
746
|
+
return false;
|
|
747
|
+
if (routingDecision.profile === 'fast' || routingDecision.shouldBackground)
|
|
748
|
+
return false;
|
|
749
|
+
const contentLength = lastMessage?.content?.length || 0;
|
|
750
|
+
return contentLength >= config.minChars;
|
|
751
|
+
};
|
|
752
|
+
const maybeAutoSpawn = async (reason, detail, overrides) => {
|
|
753
|
+
if (!BACKGROUND_JOBS_ENABLED)
|
|
754
|
+
return false;
|
|
755
|
+
if (reason !== 'router' && !AUTO_SPAWN_ENABLED)
|
|
756
|
+
return false;
|
|
757
|
+
if (reason === 'timeout' && !AUTO_SPAWN_ON_TIMEOUT)
|
|
758
|
+
return false;
|
|
759
|
+
if (reason === 'tool_limit' && !AUTO_SPAWN_ON_TOOL_LIMIT)
|
|
760
|
+
return false;
|
|
761
|
+
const tags = ['auto-spawn', reason, `profile:${routingDecision.profile}`];
|
|
762
|
+
if (overrides?.tags && overrides.tags.length > 0) {
|
|
763
|
+
tags.push(...overrides.tags);
|
|
764
|
+
}
|
|
765
|
+
if (routingDecision.estimatedMinutes) {
|
|
766
|
+
tags.push(`eta:${routingDecision.estimatedMinutes}`);
|
|
767
|
+
}
|
|
768
|
+
const estimatedMs = typeof routingDecision.estimatedMinutes === 'number'
|
|
769
|
+
? routingDecision.estimatedMinutes * 60_000
|
|
770
|
+
: null;
|
|
771
|
+
const computedTimeoutMs = estimatedMs
|
|
772
|
+
? Math.min(runtime.host.backgroundJobs.maxRuntimeMs, Math.max(5 * 60_000, Math.round(estimatedMs * 2)))
|
|
773
|
+
: undefined;
|
|
774
|
+
const result = spawnBackgroundJob({
|
|
775
|
+
prompt,
|
|
776
|
+
groupFolder: group.folder,
|
|
777
|
+
chatJid: msg.chatId,
|
|
778
|
+
contextMode: 'group',
|
|
779
|
+
tags,
|
|
780
|
+
parentTraceId: traceBase.trace_id,
|
|
781
|
+
parentMessageId: msg.messageId,
|
|
782
|
+
modelOverride: overrides?.modelOverride ?? routingDecision.modelOverride,
|
|
783
|
+
maxToolSteps: overrides?.maxToolSteps ?? routingDecision.maxToolSteps,
|
|
784
|
+
toolAllow: routingDecision.toolAllow,
|
|
785
|
+
toolDeny: routingDecision.toolDeny,
|
|
786
|
+
timeoutMs: overrides?.timeoutMs ?? computedTimeoutMs
|
|
787
|
+
});
|
|
788
|
+
if (!result.ok || !result.jobId) {
|
|
789
|
+
logger.warn({ chatId: msg.chatId, reason, error: result.error }, 'Auto-spawn background job failed');
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
const queuePosition = getBackgroundJobQueuePosition({ jobId: result.jobId, groupFolder: group.folder });
|
|
793
|
+
const eta = routingDecision.estimatedMinutes ? `${routingDecision.estimatedMinutes} min` : null;
|
|
794
|
+
const detailLine = detail ? `\n\nReason: ${detail}` : '';
|
|
795
|
+
const queueLine = queuePosition ? `\n\nQueue position: ${queuePosition.position} of ${queuePosition.total}` : '';
|
|
796
|
+
const etaLine = eta ? `\n\nEstimated time: ${eta}` : '';
|
|
797
|
+
const planPreview = plannerProbeSteps.length > 0
|
|
798
|
+
? formatPlanStepList({ steps: plannerProbeSteps, currentStep: 1, maxSteps: 4 })
|
|
799
|
+
: '';
|
|
800
|
+
const planLine = planPreview ? `\n\nPlanned steps:\n${planPreview}` : '';
|
|
801
|
+
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.${queueLine}${etaLine}${detailLine}${planLine}`, { messageThreadId: msg.messageThreadId });
|
|
802
|
+
if (lastMessage) {
|
|
803
|
+
updateChatState(msg.chatId, lastMessage.timestamp, lastMessage.id);
|
|
804
|
+
}
|
|
805
|
+
if (draftId) {
|
|
806
|
+
clearDraftSession(msg.chatId, draftId);
|
|
807
|
+
}
|
|
808
|
+
return true;
|
|
809
|
+
};
|
|
810
|
+
let plannerProbeTools = [];
|
|
811
|
+
let plannerProbeSteps = [];
|
|
812
|
+
let plannerProbeMs = null;
|
|
813
|
+
if (shouldPlannerProbe() && lastMessage) {
|
|
814
|
+
const probeStarted = Date.now();
|
|
815
|
+
const probeResult = await probePlanner({
|
|
816
|
+
lastMessage,
|
|
817
|
+
recentMessages: missedMessages
|
|
818
|
+
});
|
|
819
|
+
plannerProbeMs = Date.now() - probeStarted;
|
|
820
|
+
recordStageLatency('planner_probe', plannerProbeMs, 'telegram');
|
|
821
|
+
if (probeResult.steps.length > 0)
|
|
822
|
+
plannerProbeSteps = probeResult.steps;
|
|
823
|
+
if (probeResult.tools.length > 0)
|
|
824
|
+
plannerProbeTools = probeResult.tools;
|
|
825
|
+
logger.info({
|
|
826
|
+
chatId: msg.chatId,
|
|
827
|
+
shouldBackground: probeResult.shouldBackground,
|
|
828
|
+
steps: probeResult.steps.length,
|
|
829
|
+
tools: probeResult.tools.length,
|
|
830
|
+
latencyMs: probeResult.latencyMs,
|
|
831
|
+
model: probeResult.model,
|
|
832
|
+
error: probeResult.error
|
|
833
|
+
}, 'Planner probe decision');
|
|
834
|
+
if (probeResult.shouldBackground) {
|
|
835
|
+
const autoSpawned = await maybeAutoSpawn('planner', 'planner probe predicted multi-step work');
|
|
836
|
+
if (autoSpawned)
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (routingDecision.shouldBackground) {
|
|
841
|
+
const autoSpawned = await maybeAutoSpawn('router', routingDecision.reason);
|
|
842
|
+
if (autoSpawned) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
let classifierMs = null;
|
|
847
|
+
if (AUTO_SPAWN_ENABLED && AUTO_SPAWN_CLASSIFIER_ENABLED && lastMessage && routingDecision.shouldRunClassifier) {
|
|
848
|
+
try {
|
|
849
|
+
const queueDepth = getBackgroundJobQueueDepth({ groupFolder: group.folder });
|
|
850
|
+
const classifierResult = await classifyBackgroundJob({
|
|
851
|
+
lastMessage,
|
|
852
|
+
recentMessages: missedMessages,
|
|
853
|
+
isGroup: msg.isGroup,
|
|
854
|
+
chatType: msg.chatType,
|
|
855
|
+
queueDepth,
|
|
856
|
+
metricsSource: 'telegram'
|
|
857
|
+
});
|
|
858
|
+
if (classifierResult.latencyMs) {
|
|
859
|
+
classifierMs = classifierResult.latencyMs;
|
|
860
|
+
recordStageLatency('classifier', classifierResult.latencyMs, 'telegram');
|
|
861
|
+
}
|
|
862
|
+
logger.info({
|
|
863
|
+
chatId: msg.chatId,
|
|
864
|
+
decision: classifierResult.shouldBackground,
|
|
865
|
+
confidence: classifierResult.confidence,
|
|
866
|
+
latencyMs: classifierResult.latencyMs,
|
|
867
|
+
model: classifierResult.model,
|
|
868
|
+
reason: classifierResult.reason,
|
|
869
|
+
error: classifierResult.error
|
|
870
|
+
}, 'Background job classifier decision');
|
|
871
|
+
if (classifierResult.shouldBackground) {
|
|
872
|
+
const estimated = classifierResult.estimatedMinutes;
|
|
873
|
+
if (typeof estimated === 'number' && Number.isFinite(estimated) && estimated > 0) {
|
|
874
|
+
routingDecision.estimatedMinutes = Math.round(estimated);
|
|
875
|
+
}
|
|
876
|
+
const autoSpawned = await maybeAutoSpawn('classifier', classifierResult.reason);
|
|
877
|
+
if (autoSpawned) {
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
logger.warn({ chatId: msg.chatId, err }, 'Background job classifier failed');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const predictedStage = inferProgressStage({
|
|
887
|
+
content: lastMessage?.content || prompt,
|
|
888
|
+
plannerTools: plannerProbeTools,
|
|
889
|
+
plannerSteps: plannerProbeSteps,
|
|
890
|
+
enablePlanner: routingDecision.enablePlanner
|
|
891
|
+
});
|
|
892
|
+
const predictedMs = estimateForegroundMs({
|
|
893
|
+
content: lastMessage?.content || prompt,
|
|
894
|
+
routing: routingDecision,
|
|
895
|
+
plannerSteps: plannerProbeSteps,
|
|
896
|
+
plannerTools: plannerProbeTools
|
|
897
|
+
});
|
|
898
|
+
const planStepIndex = inferPlanStepIndex(predictedStage, plannerProbeSteps.length);
|
|
899
|
+
const progressManager = createProgressManager({
|
|
900
|
+
enabled: routingDecision.progress.enabled && !streamingEnabled,
|
|
901
|
+
initialDelayMs: routingDecision.progress.initialMs,
|
|
902
|
+
intervalMs: routingDecision.progress.intervalMs,
|
|
903
|
+
maxUpdates: routingDecision.progress.maxUpdates,
|
|
904
|
+
messages: routingDecision.progress.messages.length > 0
|
|
905
|
+
? routingDecision.progress.messages
|
|
906
|
+
: DEFAULT_PROGRESS_MESSAGES,
|
|
907
|
+
stageMessages: DEFAULT_PROGRESS_STAGES,
|
|
908
|
+
stageThrottleMs: 20_000,
|
|
657
909
|
send: async (text) => { await sendMessage(msg.chatId, text, { messageThreadId: msg.messageThreadId }); },
|
|
658
910
|
onError: (err) => logger.debug({ chatId: msg.chatId, err }, 'Failed to send progress update')
|
|
659
911
|
});
|
|
660
|
-
|
|
912
|
+
progressManager.start();
|
|
913
|
+
let sentPlan = false;
|
|
914
|
+
if (predictedMs && predictedMs >= 10_000 && routingDecision.progress.enabled && !streamingEnabled) {
|
|
915
|
+
if (plannerProbeSteps.length > 0) {
|
|
916
|
+
const planMessage = formatProgressWithPlan({
|
|
917
|
+
steps: plannerProbeSteps,
|
|
918
|
+
currentStep: planStepIndex ?? 1,
|
|
919
|
+
stage: predictedStage
|
|
920
|
+
});
|
|
921
|
+
progressManager.notify(planMessage);
|
|
922
|
+
sentPlan = true;
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
progressManager.notify(DEFAULT_PROGRESS_STAGES.ack);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (!(sentPlan && predictedStage === 'planning')) {
|
|
929
|
+
progressManager.setStage(predictedStage);
|
|
930
|
+
}
|
|
931
|
+
if (predictedStage === 'planning') {
|
|
932
|
+
const followUpStage = inferProgressStage({
|
|
933
|
+
content: lastMessage?.content || prompt,
|
|
934
|
+
plannerTools: plannerProbeTools,
|
|
935
|
+
plannerSteps: plannerProbeSteps,
|
|
936
|
+
enablePlanner: false
|
|
937
|
+
});
|
|
938
|
+
if (followUpStage !== 'planning') {
|
|
939
|
+
const delay = Math.min(15_000, Math.max(5_000, Math.floor(routingDecision.progress.initialMs / 2)));
|
|
940
|
+
setTimeout(() => progressManager.setStage(followUpStage), delay);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const abortController = new AbortController();
|
|
944
|
+
activeRuns.set(msg.chatId, abortController);
|
|
661
945
|
try {
|
|
946
|
+
const recallMaxResults = routingDecision.enableMemoryRecall
|
|
947
|
+
? (Number.isFinite(routingDecision.recallMaxResults)
|
|
948
|
+
? Math.max(0, Math.floor(routingDecision.recallMaxResults))
|
|
949
|
+
: MEMORY_RECALL_MAX_RESULTS)
|
|
950
|
+
: 0;
|
|
951
|
+
const recallMaxTokens = routingDecision.enableMemoryRecall
|
|
952
|
+
? (Number.isFinite(routingDecision.recallMaxTokens)
|
|
953
|
+
? Math.max(0, Math.floor(routingDecision.recallMaxTokens))
|
|
954
|
+
: MEMORY_RECALL_MAX_TOKENS)
|
|
955
|
+
: 0;
|
|
662
956
|
const execution = await executeAgentRun({
|
|
663
957
|
group,
|
|
664
958
|
prompt,
|
|
@@ -666,8 +960,10 @@ ${lines.join('\n')}
|
|
|
666
960
|
userId: msg.senderId,
|
|
667
961
|
userName: msg.senderName,
|
|
668
962
|
recallQuery: recallQuery || msg.content,
|
|
669
|
-
recallMaxResults
|
|
670
|
-
recallMaxTokens
|
|
963
|
+
recallMaxResults,
|
|
964
|
+
recallMaxTokens,
|
|
965
|
+
toolAllow: routingDecision.toolAllow,
|
|
966
|
+
toolDeny: routingDecision.toolDeny,
|
|
671
967
|
sessionId: sessions[group.folder],
|
|
672
968
|
onSessionUpdate: (sessionId) => { sessions[group.folder] = sessionId; },
|
|
673
969
|
streaming: streamingEnabled && draftId
|
|
@@ -678,10 +974,22 @@ ${lines.join('\n')}
|
|
|
678
974
|
minChars: TELEGRAM_STREAM_MIN_CHARS
|
|
679
975
|
}
|
|
680
976
|
: undefined,
|
|
681
|
-
availableGroups: buildAvailableGroupsSnapshot()
|
|
977
|
+
availableGroups: buildAvailableGroupsSnapshot(),
|
|
978
|
+
modelOverride: routingDecision.modelOverride,
|
|
979
|
+
modelMaxOutputTokens: routingDecision.maxOutputTokens,
|
|
980
|
+
maxToolSteps: routingDecision.maxToolSteps,
|
|
981
|
+
disablePlanner: !routingDecision.enablePlanner,
|
|
982
|
+
disableResponseValidation: !routingDecision.enableResponseValidation,
|
|
983
|
+
responseValidationMaxRetries: routingDecision.responseValidationMaxRetries,
|
|
984
|
+
disableMemoryExtraction: !routingDecision.enableMemoryExtraction,
|
|
985
|
+
abortSignal: abortController.signal,
|
|
986
|
+
timeoutMs: AUTO_SPAWN_ENABLED && AUTO_SPAWN_FOREGROUND_TIMEOUT_MS > 0
|
|
987
|
+
? AUTO_SPAWN_FOREGROUND_TIMEOUT_MS
|
|
988
|
+
: undefined
|
|
682
989
|
});
|
|
683
990
|
output = execution.output;
|
|
684
991
|
context = execution.context;
|
|
992
|
+
progressManager.setStage('finalizing');
|
|
685
993
|
if (output.status === 'error') {
|
|
686
994
|
errorMessage = output.error || 'Unknown error';
|
|
687
995
|
}
|
|
@@ -697,8 +1005,15 @@ ${lines.join('\n')}
|
|
|
697
1005
|
logger.error({ group: group.name, err }, 'Agent error');
|
|
698
1006
|
}
|
|
699
1007
|
finally {
|
|
700
|
-
|
|
701
|
-
|
|
1008
|
+
progressManager.stop();
|
|
1009
|
+
activeRuns.delete(msg.chatId);
|
|
1010
|
+
}
|
|
1011
|
+
const extraTimings = {};
|
|
1012
|
+
extraTimings.router_ms = routerMs;
|
|
1013
|
+
if (classifierMs !== null)
|
|
1014
|
+
extraTimings.classifier_ms = classifierMs;
|
|
1015
|
+
if (plannerProbeMs !== null)
|
|
1016
|
+
extraTimings.planner_probe_ms = plannerProbeMs;
|
|
702
1017
|
if (!output) {
|
|
703
1018
|
const message = errorMessage || 'No output from agent';
|
|
704
1019
|
if (context) {
|
|
@@ -709,7 +1024,8 @@ ${lines.join('\n')}
|
|
|
709
1024
|
metricsSource: 'telegram',
|
|
710
1025
|
toolAuditSource: 'message',
|
|
711
1026
|
errorMessage: message,
|
|
712
|
-
errorType: 'agent'
|
|
1027
|
+
errorType: 'agent',
|
|
1028
|
+
extraTimings
|
|
713
1029
|
});
|
|
714
1030
|
}
|
|
715
1031
|
else {
|
|
@@ -729,6 +1045,12 @@ ${lines.join('\n')}
|
|
|
729
1045
|
source: traceBase.source
|
|
730
1046
|
});
|
|
731
1047
|
}
|
|
1048
|
+
if (isTimeoutError(message)) {
|
|
1049
|
+
const autoSpawned = await maybeAutoSpawn('timeout', message);
|
|
1050
|
+
if (autoSpawned) {
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
732
1054
|
const userMessage = humanizeError(errorMessage || 'Unknown error');
|
|
733
1055
|
await sendMessage(msg.chatId, userMessage, { messageThreadId: msg.messageThreadId });
|
|
734
1056
|
if (draftId) {
|
|
@@ -745,11 +1067,19 @@ ${lines.join('\n')}
|
|
|
745
1067
|
metricsSource: 'telegram',
|
|
746
1068
|
toolAuditSource: 'message',
|
|
747
1069
|
errorMessage: errorMessage || output.error || 'Unknown error',
|
|
748
|
-
errorType: 'agent'
|
|
1070
|
+
errorType: 'agent',
|
|
1071
|
+
extraTimings
|
|
749
1072
|
});
|
|
750
1073
|
}
|
|
751
1074
|
logger.error({ group: group.name, error: output.error }, 'Container agent error');
|
|
752
|
-
const
|
|
1075
|
+
const errorText = errorMessage || output.error || 'Unknown error';
|
|
1076
|
+
if (isTimeoutError(errorText)) {
|
|
1077
|
+
const autoSpawned = await maybeAutoSpawn('timeout', errorText);
|
|
1078
|
+
if (autoSpawned) {
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const userMessage = humanizeError(errorText);
|
|
753
1083
|
await sendMessage(msg.chatId, userMessage, { messageThreadId: msg.messageThreadId });
|
|
754
1084
|
if (draftId) {
|
|
755
1085
|
clearDraftSession(msg.chatId, draftId);
|
|
@@ -780,6 +1110,23 @@ ${lines.join('\n')}
|
|
|
780
1110
|
}
|
|
781
1111
|
}
|
|
782
1112
|
else if (output.tool_calls && output.tool_calls.length > 0) {
|
|
1113
|
+
const toolLimitHit = !output.result || !output.result.trim() || TOOL_CALL_FALLBACK_PATTERN.test(output.result);
|
|
1114
|
+
if (toolLimitHit) {
|
|
1115
|
+
const autoSpawned = await maybeAutoSpawn('tool_limit', 'Tool-call step limit reached');
|
|
1116
|
+
if (autoSpawned) {
|
|
1117
|
+
if (context) {
|
|
1118
|
+
recordAgentTelemetry({
|
|
1119
|
+
traceBase,
|
|
1120
|
+
output,
|
|
1121
|
+
context,
|
|
1122
|
+
metricsSource: 'telegram',
|
|
1123
|
+
toolAuditSource: 'message',
|
|
1124
|
+
extraTimings
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
783
1130
|
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 });
|
|
784
1131
|
if (draftId) {
|
|
785
1132
|
clearDraftSession(msg.chatId, draftId);
|
|
@@ -797,7 +1144,8 @@ ${lines.join('\n')}
|
|
|
797
1144
|
output,
|
|
798
1145
|
context,
|
|
799
1146
|
metricsSource: 'telegram',
|
|
800
|
-
toolAuditSource: 'message'
|
|
1147
|
+
toolAuditSource: 'message',
|
|
1148
|
+
extraTimings
|
|
801
1149
|
});
|
|
802
1150
|
}
|
|
803
1151
|
return true;
|
|
@@ -997,11 +1345,22 @@ async function runHeartbeatOnce() {
|
|
|
997
1345
|
inputText: prompt,
|
|
998
1346
|
source: 'dotclaw-heartbeat'
|
|
999
1347
|
});
|
|
1348
|
+
const routingStartedAt = Date.now();
|
|
1349
|
+
const routingDecision = routePrompt(prompt);
|
|
1350
|
+
recordRoutingDecision(routingDecision.profile);
|
|
1351
|
+
const routerMs = Date.now() - routingStartedAt;
|
|
1352
|
+
recordStageLatency('router', routerMs, 'scheduler');
|
|
1000
1353
|
let output = null;
|
|
1001
1354
|
let context = null;
|
|
1002
1355
|
let errorMessage = null;
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1356
|
+
const baseRecallResults = Number.isFinite(routingDecision.recallMaxResults)
|
|
1357
|
+
? Math.max(0, Math.floor(routingDecision.recallMaxResults))
|
|
1358
|
+
: MEMORY_RECALL_MAX_RESULTS;
|
|
1359
|
+
const baseRecallTokens = Number.isFinite(routingDecision.recallMaxTokens)
|
|
1360
|
+
? Math.max(0, Math.floor(routingDecision.recallMaxTokens))
|
|
1361
|
+
: MEMORY_RECALL_MAX_TOKENS;
|
|
1362
|
+
const recallMaxResults = routingDecision.enableMemoryRecall ? Math.max(4, baseRecallResults - 2) : 0;
|
|
1363
|
+
const recallMaxTokens = routingDecision.enableMemoryRecall ? Math.max(600, baseRecallTokens - 200) : 0;
|
|
1005
1364
|
try {
|
|
1006
1365
|
const execution = await executeAgentRun({
|
|
1007
1366
|
group,
|
|
@@ -1014,7 +1373,14 @@ async function runHeartbeatOnce() {
|
|
|
1014
1373
|
sessionId: sessions[group.folder],
|
|
1015
1374
|
onSessionUpdate: (sessionId) => { sessions[group.folder] = sessionId; },
|
|
1016
1375
|
isScheduledTask: true,
|
|
1017
|
-
availableGroups: buildAvailableGroupsSnapshot()
|
|
1376
|
+
availableGroups: buildAvailableGroupsSnapshot(),
|
|
1377
|
+
modelOverride: routingDecision.modelOverride,
|
|
1378
|
+
modelMaxOutputTokens: routingDecision.maxOutputTokens,
|
|
1379
|
+
maxToolSteps: routingDecision.maxToolSteps,
|
|
1380
|
+
disablePlanner: !routingDecision.enablePlanner,
|
|
1381
|
+
disableResponseValidation: !routingDecision.enableResponseValidation,
|
|
1382
|
+
responseValidationMaxRetries: routingDecision.responseValidationMaxRetries,
|
|
1383
|
+
disableMemoryExtraction: !routingDecision.enableMemoryExtraction
|
|
1018
1384
|
});
|
|
1019
1385
|
output = execution.output;
|
|
1020
1386
|
context = execution.context;
|
|
@@ -1038,7 +1404,8 @@ async function runHeartbeatOnce() {
|
|
|
1038
1404
|
output,
|
|
1039
1405
|
context,
|
|
1040
1406
|
toolAuditSource: 'heartbeat',
|
|
1041
|
-
errorMessage: errorMessage ?? undefined
|
|
1407
|
+
errorMessage: errorMessage ?? undefined,
|
|
1408
|
+
extraTimings: { router_ms: routerMs }
|
|
1042
1409
|
});
|
|
1043
1410
|
}
|
|
1044
1411
|
else if (errorMessage) {
|
|
@@ -1817,6 +2184,11 @@ function setupTelegramHandlers() {
|
|
|
1817
2184
|
const isPrivate = chatType === 'private';
|
|
1818
2185
|
const senderId = String(ctx.from?.id || ctx.chat.id);
|
|
1819
2186
|
const senderName = ctx.from?.first_name || ctx.from?.username || 'User';
|
|
2187
|
+
const chatName = ('title' in ctx.chat && ctx.chat.title)
|
|
2188
|
+
|| ('username' in ctx.chat && ctx.chat.username)
|
|
2189
|
+
|| ctx.from?.first_name
|
|
2190
|
+
|| ctx.from?.username
|
|
2191
|
+
|| senderName;
|
|
1820
2192
|
const content = ctx.message.text;
|
|
1821
2193
|
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
1822
2194
|
const messageId = String(ctx.message.message_id);
|
|
@@ -1825,6 +2197,7 @@ function setupTelegramHandlers() {
|
|
|
1825
2197
|
logger.info({ chatId, isGroup, senderName }, `Telegram message: ${content.substring(0, 50)}...`);
|
|
1826
2198
|
try {
|
|
1827
2199
|
// Store message in database
|
|
2200
|
+
upsertChat({ chatId, name: chatName, lastMessageTime: timestamp });
|
|
1828
2201
|
storeMessage(String(ctx.message.message_id), chatId, senderId, senderName, content, timestamp, false);
|
|
1829
2202
|
}
|
|
1830
2203
|
catch (error) {
|