@dotsetlabs/dotclaw 1.3.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 +106 -3
- 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 +38 -6
- 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 +4 -0
- package/dist/background-job-classifier.d.ts.map +1 -1
- package/dist/background-job-classifier.js +36 -15
- package/dist/background-job-classifier.js.map +1 -1
- 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 +11 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +36 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +300 -37
- 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 +67 -0
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +177 -14
- 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, upsertChat, 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';
|
|
@@ -26,6 +26,8 @@ import { logger } from './logger.js';
|
|
|
26
26
|
import { startDashboard, setTelegramConnected, setLastMessageTime, setMessageQueueDepth } from './dashboard.js';
|
|
27
27
|
import { humanizeError } from './error-messages.js';
|
|
28
28
|
import { classifyBackgroundJob } from './background-job-classifier.js';
|
|
29
|
+
import { routeRequest, routePrompt } from './request-router.js';
|
|
30
|
+
import { probePlanner } from './planner-probe.js';
|
|
29
31
|
const runtime = loadRuntimeConfig();
|
|
30
32
|
function buildTriggerRegex(pattern) {
|
|
31
33
|
if (!pattern)
|
|
@@ -146,13 +148,6 @@ const TELEGRAM_STREAM_MIN_CHARS = runtime.host.telegram.streamMinChars;
|
|
|
146
148
|
const MEMORY_RECALL_MAX_RESULTS = runtime.host.memory.recall.maxResults;
|
|
147
149
|
const MEMORY_RECALL_MAX_TOKENS = runtime.host.memory.recall.maxTokens;
|
|
148
150
|
const INPUT_MESSAGE_MAX_CHARS = runtime.host.telegram.inputMessageMaxChars;
|
|
149
|
-
const PROGRESS_ENABLED = runtime.host.progress.enabled;
|
|
150
|
-
const PROGRESS_INITIAL_MS = runtime.host.progress.initialMs;
|
|
151
|
-
const PROGRESS_INTERVAL_MS = runtime.host.progress.intervalMs;
|
|
152
|
-
const PROGRESS_MAX_UPDATES = runtime.host.progress.maxUpdates;
|
|
153
|
-
const PROGRESS_MESSAGES = runtime.host.progress.messages.length > 0
|
|
154
|
-
? runtime.host.progress.messages
|
|
155
|
-
: DEFAULT_PROGRESS_MESSAGES;
|
|
156
151
|
const HEARTBEAT_ENABLED = runtime.host.heartbeat.enabled;
|
|
157
152
|
const HEARTBEAT_INTERVAL_MS = runtime.host.heartbeat.intervalMs;
|
|
158
153
|
const HEARTBEAT_GROUP_FOLDER = (runtime.host.heartbeat.groupFolder || MAIN_GROUP_FOLDER).trim() || MAIN_GROUP_FOLDER;
|
|
@@ -176,6 +171,64 @@ let registeredGroups = {};
|
|
|
176
171
|
const TELEGRAM_MAX_MESSAGE_LENGTH = 4000;
|
|
177
172
|
const TELEGRAM_SEND_DELAY_MS = 250;
|
|
178
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
|
+
}
|
|
179
232
|
const draftSessions = new Map();
|
|
180
233
|
function parseTelegramStreamMode(value) {
|
|
181
234
|
const normalized = value.trim().toLowerCase();
|
|
@@ -566,6 +619,17 @@ async function finalizeStreamedMessage(msg, draftId, text) {
|
|
|
566
619
|
clearDraftSession(msg.chatId, draftId);
|
|
567
620
|
}
|
|
568
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
|
+
}
|
|
569
633
|
const existing = messageQueues.get(msg.chatId);
|
|
570
634
|
if (existing) {
|
|
571
635
|
existing.pendingMessage = msg;
|
|
@@ -641,6 +705,20 @@ async function processMessage(msg) {
|
|
|
641
705
|
${lines.join('\n')}
|
|
642
706
|
</messages>`;
|
|
643
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');
|
|
644
722
|
const traceBase = createTraceBase({
|
|
645
723
|
chatId: msg.chatId,
|
|
646
724
|
groupFolder: group.folder,
|
|
@@ -662,28 +740,65 @@ ${lines.join('\n')}
|
|
|
662
740
|
return false;
|
|
663
741
|
return /timed out|timeout/i.test(value);
|
|
664
742
|
};
|
|
665
|
-
const
|
|
666
|
-
|
|
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)
|
|
667
756
|
return false;
|
|
668
757
|
if (reason === 'timeout' && !AUTO_SPAWN_ON_TIMEOUT)
|
|
669
758
|
return false;
|
|
670
759
|
if (reason === 'tool_limit' && !AUTO_SPAWN_ON_TOOL_LIMIT)
|
|
671
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;
|
|
672
774
|
const result = spawnBackgroundJob({
|
|
673
775
|
prompt,
|
|
674
776
|
groupFolder: group.folder,
|
|
675
777
|
chatJid: msg.chatId,
|
|
676
778
|
contextMode: 'group',
|
|
677
|
-
tags
|
|
779
|
+
tags,
|
|
678
780
|
parentTraceId: traceBase.trace_id,
|
|
679
|
-
parentMessageId: msg.messageId
|
|
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
|
|
680
787
|
});
|
|
681
788
|
if (!result.ok || !result.jobId) {
|
|
682
789
|
logger.warn({ chatId: msg.chatId, reason, error: result.error }, 'Auto-spawn background job failed');
|
|
683
790
|
return false;
|
|
684
791
|
}
|
|
792
|
+
const queuePosition = getBackgroundJobQueuePosition({ jobId: result.jobId, groupFolder: group.folder });
|
|
793
|
+
const eta = routingDecision.estimatedMinutes ? `${routingDecision.estimatedMinutes} min` : null;
|
|
685
794
|
const detailLine = detail ? `\n\nReason: ${detail}` : '';
|
|
686
|
-
|
|
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 });
|
|
687
802
|
if (lastMessage) {
|
|
688
803
|
updateChatState(msg.chatId, lastMessage.timestamp, lastMessage.id);
|
|
689
804
|
}
|
|
@@ -692,14 +807,58 @@ ${lines.join('\n')}
|
|
|
692
807
|
}
|
|
693
808
|
return true;
|
|
694
809
|
};
|
|
695
|
-
|
|
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) {
|
|
696
848
|
try {
|
|
849
|
+
const queueDepth = getBackgroundJobQueueDepth({ groupFolder: group.folder });
|
|
697
850
|
const classifierResult = await classifyBackgroundJob({
|
|
698
851
|
lastMessage,
|
|
699
852
|
recentMessages: missedMessages,
|
|
700
853
|
isGroup: msg.isGroup,
|
|
701
|
-
chatType: msg.chatType
|
|
854
|
+
chatType: msg.chatType,
|
|
855
|
+
queueDepth,
|
|
856
|
+
metricsSource: 'telegram'
|
|
702
857
|
});
|
|
858
|
+
if (classifierResult.latencyMs) {
|
|
859
|
+
classifierMs = classifierResult.latencyMs;
|
|
860
|
+
recordStageLatency('classifier', classifierResult.latencyMs, 'telegram');
|
|
861
|
+
}
|
|
703
862
|
logger.info({
|
|
704
863
|
chatId: msg.chatId,
|
|
705
864
|
decision: classifierResult.shouldBackground,
|
|
@@ -710,7 +869,11 @@ ${lines.join('\n')}
|
|
|
710
869
|
error: classifierResult.error
|
|
711
870
|
}, 'Background job classifier decision');
|
|
712
871
|
if (classifierResult.shouldBackground) {
|
|
713
|
-
const
|
|
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);
|
|
714
877
|
if (autoSpawned) {
|
|
715
878
|
return true;
|
|
716
879
|
}
|
|
@@ -720,17 +883,76 @@ ${lines.join('\n')}
|
|
|
720
883
|
logger.warn({ chatId: msg.chatId, err }, 'Background job classifier failed');
|
|
721
884
|
}
|
|
722
885
|
}
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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,
|
|
729
909
|
send: async (text) => { await sendMessage(msg.chatId, text, { messageThreadId: msg.messageThreadId }); },
|
|
730
910
|
onError: (err) => logger.debug({ chatId: msg.chatId, err }, 'Failed to send progress update')
|
|
731
911
|
});
|
|
732
|
-
|
|
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);
|
|
733
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;
|
|
734
956
|
const execution = await executeAgentRun({
|
|
735
957
|
group,
|
|
736
958
|
prompt,
|
|
@@ -738,8 +960,10 @@ ${lines.join('\n')}
|
|
|
738
960
|
userId: msg.senderId,
|
|
739
961
|
userName: msg.senderName,
|
|
740
962
|
recallQuery: recallQuery || msg.content,
|
|
741
|
-
recallMaxResults
|
|
742
|
-
recallMaxTokens
|
|
963
|
+
recallMaxResults,
|
|
964
|
+
recallMaxTokens,
|
|
965
|
+
toolAllow: routingDecision.toolAllow,
|
|
966
|
+
toolDeny: routingDecision.toolDeny,
|
|
743
967
|
sessionId: sessions[group.folder],
|
|
744
968
|
onSessionUpdate: (sessionId) => { sessions[group.folder] = sessionId; },
|
|
745
969
|
streaming: streamingEnabled && draftId
|
|
@@ -751,12 +975,21 @@ ${lines.join('\n')}
|
|
|
751
975
|
}
|
|
752
976
|
: undefined,
|
|
753
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,
|
|
754
986
|
timeoutMs: AUTO_SPAWN_ENABLED && AUTO_SPAWN_FOREGROUND_TIMEOUT_MS > 0
|
|
755
987
|
? AUTO_SPAWN_FOREGROUND_TIMEOUT_MS
|
|
756
988
|
: undefined
|
|
757
989
|
});
|
|
758
990
|
output = execution.output;
|
|
759
991
|
context = execution.context;
|
|
992
|
+
progressManager.setStage('finalizing');
|
|
760
993
|
if (output.status === 'error') {
|
|
761
994
|
errorMessage = output.error || 'Unknown error';
|
|
762
995
|
}
|
|
@@ -772,8 +1005,15 @@ ${lines.join('\n')}
|
|
|
772
1005
|
logger.error({ group: group.name, err }, 'Agent error');
|
|
773
1006
|
}
|
|
774
1007
|
finally {
|
|
775
|
-
|
|
776
|
-
|
|
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;
|
|
777
1017
|
if (!output) {
|
|
778
1018
|
const message = errorMessage || 'No output from agent';
|
|
779
1019
|
if (context) {
|
|
@@ -784,7 +1024,8 @@ ${lines.join('\n')}
|
|
|
784
1024
|
metricsSource: 'telegram',
|
|
785
1025
|
toolAuditSource: 'message',
|
|
786
1026
|
errorMessage: message,
|
|
787
|
-
errorType: 'agent'
|
|
1027
|
+
errorType: 'agent',
|
|
1028
|
+
extraTimings
|
|
788
1029
|
});
|
|
789
1030
|
}
|
|
790
1031
|
else {
|
|
@@ -826,7 +1067,8 @@ ${lines.join('\n')}
|
|
|
826
1067
|
metricsSource: 'telegram',
|
|
827
1068
|
toolAuditSource: 'message',
|
|
828
1069
|
errorMessage: errorMessage || output.error || 'Unknown error',
|
|
829
|
-
errorType: 'agent'
|
|
1070
|
+
errorType: 'agent',
|
|
1071
|
+
extraTimings
|
|
830
1072
|
});
|
|
831
1073
|
}
|
|
832
1074
|
logger.error({ group: group.name, error: output.error }, 'Container agent error');
|
|
@@ -878,7 +1120,8 @@ ${lines.join('\n')}
|
|
|
878
1120
|
output,
|
|
879
1121
|
context,
|
|
880
1122
|
metricsSource: 'telegram',
|
|
881
|
-
toolAuditSource: 'message'
|
|
1123
|
+
toolAuditSource: 'message',
|
|
1124
|
+
extraTimings
|
|
882
1125
|
});
|
|
883
1126
|
}
|
|
884
1127
|
return true;
|
|
@@ -901,7 +1144,8 @@ ${lines.join('\n')}
|
|
|
901
1144
|
output,
|
|
902
1145
|
context,
|
|
903
1146
|
metricsSource: 'telegram',
|
|
904
|
-
toolAuditSource: 'message'
|
|
1147
|
+
toolAuditSource: 'message',
|
|
1148
|
+
extraTimings
|
|
905
1149
|
});
|
|
906
1150
|
}
|
|
907
1151
|
return true;
|
|
@@ -1101,11 +1345,22 @@ async function runHeartbeatOnce() {
|
|
|
1101
1345
|
inputText: prompt,
|
|
1102
1346
|
source: 'dotclaw-heartbeat'
|
|
1103
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');
|
|
1104
1353
|
let output = null;
|
|
1105
1354
|
let context = null;
|
|
1106
1355
|
let errorMessage = null;
|
|
1107
|
-
const
|
|
1108
|
-
|
|
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;
|
|
1109
1364
|
try {
|
|
1110
1365
|
const execution = await executeAgentRun({
|
|
1111
1366
|
group,
|
|
@@ -1118,7 +1373,14 @@ async function runHeartbeatOnce() {
|
|
|
1118
1373
|
sessionId: sessions[group.folder],
|
|
1119
1374
|
onSessionUpdate: (sessionId) => { sessions[group.folder] = sessionId; },
|
|
1120
1375
|
isScheduledTask: true,
|
|
1121
|
-
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
|
|
1122
1384
|
});
|
|
1123
1385
|
output = execution.output;
|
|
1124
1386
|
context = execution.context;
|
|
@@ -1142,7 +1404,8 @@ async function runHeartbeatOnce() {
|
|
|
1142
1404
|
output,
|
|
1143
1405
|
context,
|
|
1144
1406
|
toolAuditSource: 'heartbeat',
|
|
1145
|
-
errorMessage: errorMessage ?? undefined
|
|
1407
|
+
errorMessage: errorMessage ?? undefined,
|
|
1408
|
+
extraTimings: { router_ms: routerMs }
|
|
1146
1409
|
});
|
|
1147
1410
|
}
|
|
1148
1411
|
else if (errorMessage) {
|