@dotsetlabs/dotclaw 1.3.0 → 1.5.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.
Files changed (79) hide show
  1. package/README.md +9 -0
  2. package/config-examples/runtime.json +106 -3
  3. package/container/agent-runner/package-lock.json +2 -2
  4. package/container/agent-runner/package.json +1 -1
  5. package/container/agent-runner/src/agent-config.ts +20 -2
  6. package/container/agent-runner/src/container-protocol.ts +11 -0
  7. package/container/agent-runner/src/index.ts +128 -11
  8. package/container/agent-runner/src/tools.ts +84 -5
  9. package/dist/agent-context.d.ts +5 -0
  10. package/dist/agent-context.d.ts.map +1 -1
  11. package/dist/agent-context.js +19 -8
  12. package/dist/agent-context.js.map +1 -1
  13. package/dist/agent-execution.d.ts +6 -0
  14. package/dist/agent-execution.d.ts.map +1 -1
  15. package/dist/agent-execution.js +61 -4
  16. package/dist/agent-execution.js.map +1 -1
  17. package/dist/background-job-classifier.d.ts +4 -0
  18. package/dist/background-job-classifier.d.ts.map +1 -1
  19. package/dist/background-job-classifier.js +36 -15
  20. package/dist/background-job-classifier.js.map +1 -1
  21. package/dist/background-jobs.d.ts.map +1 -1
  22. package/dist/background-jobs.js +81 -4
  23. package/dist/background-jobs.js.map +1 -1
  24. package/dist/cli.js +343 -11
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config.d.ts +0 -2
  27. package/dist/config.d.ts.map +1 -1
  28. package/dist/config.js +0 -3
  29. package/dist/config.js.map +1 -1
  30. package/dist/container-protocol.d.ts +11 -0
  31. package/dist/container-protocol.d.ts.map +1 -1
  32. package/dist/container-runner.d.ts.map +1 -1
  33. package/dist/container-runner.js +9 -1
  34. package/dist/container-runner.js.map +1 -1
  35. package/dist/dashboard.d.ts +5 -0
  36. package/dist/dashboard.d.ts.map +1 -1
  37. package/dist/dashboard.js +58 -8
  38. package/dist/dashboard.js.map +1 -1
  39. package/dist/db.d.ts +11 -0
  40. package/dist/db.d.ts.map +1 -1
  41. package/dist/db.js +36 -0
  42. package/dist/db.js.map +1 -1
  43. package/dist/index.js +300 -37
  44. package/dist/index.js.map +1 -1
  45. package/dist/json-helpers.d.ts +6 -0
  46. package/dist/json-helpers.d.ts.map +1 -0
  47. package/dist/json-helpers.js +17 -0
  48. package/dist/json-helpers.js.map +1 -0
  49. package/dist/logger.d.ts +0 -1
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/logger.js +1 -1
  52. package/dist/logger.js.map +1 -1
  53. package/dist/metrics.d.ts +3 -0
  54. package/dist/metrics.d.ts.map +1 -1
  55. package/dist/metrics.js +35 -1
  56. package/dist/metrics.js.map +1 -1
  57. package/dist/planner-probe.d.ts +14 -0
  58. package/dist/planner-probe.d.ts.map +1 -0
  59. package/dist/planner-probe.js +97 -0
  60. package/dist/planner-probe.js.map +1 -0
  61. package/dist/progress.d.ts +27 -0
  62. package/dist/progress.d.ts.map +1 -1
  63. package/dist/progress.js +151 -0
  64. package/dist/progress.js.map +1 -1
  65. package/dist/request-router.d.ts +34 -0
  66. package/dist/request-router.d.ts.map +1 -0
  67. package/dist/request-router.js +148 -0
  68. package/dist/request-router.js.map +1 -0
  69. package/dist/runtime-config.d.ts +67 -0
  70. package/dist/runtime-config.d.ts.map +1 -1
  71. package/dist/runtime-config.js +177 -14
  72. package/dist/runtime-config.js.map +1 -1
  73. package/dist/task-scheduler.d.ts.map +1 -1
  74. package/dist/task-scheduler.js +56 -9
  75. package/dist/task-scheduler.js.map +1 -1
  76. package/dist/trace-writer.d.ts +1 -0
  77. package/dist/trace-writer.d.ts.map +1 -1
  78. package/dist/trace-writer.js.map +1 -1
  79. package/package.json +1 -1
package/README.md CHANGED
@@ -45,6 +45,8 @@ After installation, use the `dotclaw` CLI:
45
45
  ```bash
46
46
  dotclaw setup # Full setup (init + configure + build + install service)
47
47
  dotclaw configure # Re-configure API keys and model
48
+ dotclaw add-instance # Create and start an isolated instance
49
+ dotclaw instances # List discovered instances
48
50
  dotclaw build # Build the Docker container image
49
51
  dotclaw start # Start the service
50
52
  dotclaw stop # Stop the service
@@ -55,6 +57,13 @@ dotclaw doctor # Run diagnostics
55
57
  dotclaw register # Register a new Telegram chat
56
58
  ```
57
59
 
60
+ Instance flags:
61
+
62
+ ```bash
63
+ dotclaw status --id dev # Run against a specific instance (~/.dotclaw-dev)
64
+ dotclaw restart --all # Restart all instances
65
+ ```
66
+
58
67
  ## Configuration
59
68
 
60
69
  All configuration and data is stored in `~/.dotclaw/`:
@@ -2,7 +2,8 @@
2
2
  "host": {
3
3
  "logLevel": "info",
4
4
  "container": {
5
- "mode": "daemon"
5
+ "mode": "daemon",
6
+ "instanceId": ""
6
7
  },
7
8
  "metrics": {
8
9
  "port": 3001,
@@ -20,9 +21,15 @@
20
21
  "enabled": true,
21
22
  "maxConcurrent": 2,
22
23
  "maxRuntimeMs": 2400000,
24
+ "progress": {
25
+ "enabled": true,
26
+ "startDelayMs": 30000,
27
+ "intervalMs": 120000,
28
+ "maxUpdates": 3
29
+ },
23
30
  "autoSpawn": {
24
31
  "enabled": true,
25
- "foregroundTimeoutMs": 180000,
32
+ "foregroundTimeoutMs": 90000,
26
33
  "onTimeout": true,
27
34
  "onToolLimit": true,
28
35
  "classifier": {
@@ -31,7 +38,89 @@
31
38
  "timeoutMs": 3000,
32
39
  "maxOutputTokens": 32,
33
40
  "temperature": 0,
34
- "confidenceThreshold": 0.6
41
+ "confidenceThreshold": 0.6,
42
+ "adaptive": {
43
+ "enabled": true,
44
+ "minThreshold": 0.55,
45
+ "maxThreshold": 0.65,
46
+ "queueDepthLow": 0,
47
+ "queueDepthHigh": 4
48
+ }
49
+ }
50
+ }
51
+ },
52
+ "routing": {
53
+ "enabled": true,
54
+ "maxFastChars": 200,
55
+ "maxStandardChars": 1200,
56
+ "backgroundMinChars": 2000,
57
+ "fastKeywords": ["hi", "hello", "hey", "who are you", "what can you do"],
58
+ "deepKeywords": ["research", "analysis", "report", "dashboard", "refactor", "architecture", "design"],
59
+ "backgroundKeywords": ["background", "long-running", "research", "dashboard", "refactor", "report"],
60
+ "classifierFallback": {
61
+ "enabled": true,
62
+ "minChars": 600
63
+ },
64
+ "plannerProbe": {
65
+ "enabled": true,
66
+ "model": "openai/gpt-5-nano",
67
+ "timeoutMs": 3000,
68
+ "maxOutputTokens": 120,
69
+ "temperature": 0,
70
+ "minChars": 700,
71
+ "minSteps": 4,
72
+ "minTools": 3
73
+ },
74
+ "profiles": {
75
+ "fast": {
76
+ "model": "openai/gpt-5-nano",
77
+ "maxOutputTokens": 256,
78
+ "maxToolSteps": 6,
79
+ "recallMaxResults": 0,
80
+ "recallMaxTokens": 0,
81
+ "enablePlanner": false,
82
+ "enableValidation": false,
83
+ "responseValidationMaxRetries": 0,
84
+ "enableMemoryRecall": false,
85
+ "enableMemoryExtraction": false,
86
+ "progress": { "enabled": false }
87
+ },
88
+ "standard": {
89
+ "model": "openai/gpt-5-mini",
90
+ "maxOutputTokens": 768,
91
+ "maxToolSteps": 16,
92
+ "recallMaxResults": 6,
93
+ "recallMaxTokens": 1500,
94
+ "enablePlanner": true,
95
+ "enableValidation": true,
96
+ "responseValidationMaxRetries": 1,
97
+ "enableMemoryRecall": true,
98
+ "enableMemoryExtraction": true
99
+ },
100
+ "deep": {
101
+ "model": "moonshotai/kimi-k2.5",
102
+ "maxOutputTokens": 1536,
103
+ "maxToolSteps": 32,
104
+ "recallMaxResults": 12,
105
+ "recallMaxTokens": 2500,
106
+ "enablePlanner": true,
107
+ "enableValidation": true,
108
+ "responseValidationMaxRetries": 2,
109
+ "enableMemoryRecall": true,
110
+ "enableMemoryExtraction": true
111
+ },
112
+ "background": {
113
+ "model": "moonshotai/kimi-k2.5",
114
+ "maxOutputTokens": 2048,
115
+ "maxToolSteps": 64,
116
+ "recallMaxResults": 16,
117
+ "recallMaxTokens": 4000,
118
+ "enablePlanner": true,
119
+ "enableValidation": true,
120
+ "responseValidationMaxRetries": 2,
121
+ "enableMemoryRecall": true,
122
+ "enableMemoryExtraction": true,
123
+ "progress": { "enabled": true, "initialMs": 15000, "intervalMs": 60000, "maxUpdates": 3 }
35
124
  }
36
125
  }
37
126
  }
@@ -44,6 +133,20 @@
44
133
  "planner": {
45
134
  "enabled": true,
46
135
  "mode": "auto"
136
+ },
137
+ "responseValidation": {
138
+ "enabled": true,
139
+ "minPromptTokens": 400,
140
+ "minResponseTokens": 160
141
+ },
142
+ "tools": {
143
+ "progress": {
144
+ "enabled": true,
145
+ "minIntervalMs": 30000,
146
+ "notifyTools": ["WebSearch", "WebFetch", "Bash", "GitClone", "NpmInstall"],
147
+ "notifyOnStart": true,
148
+ "notifyOnError": true
149
+ }
47
150
  }
48
151
  }
49
152
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "dotclaw-agent-runner",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "dotclaw-agent-runner",
9
- "version": "1.2.0",
9
+ "version": "1.5.0",
10
10
  "dependencies": {
11
11
  "@openrouter/sdk": "^0.3.0",
12
12
  "cron-parser": "^5.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotclaw-agent-runner",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Container-side agent runner for DotClaw",
6
6
  "main": "dist/index.js",
@@ -60,6 +60,8 @@ export type AgentRuntimeConfig = {
60
60
  temperature: number;
61
61
  maxRetries: number;
62
62
  allowToolCalls: boolean;
63
+ minPromptTokens: number;
64
+ minResponseTokens: number;
63
65
  };
64
66
  tools: {
65
67
  maxToolSteps: number;
@@ -87,6 +89,13 @@ export type AgentRuntimeConfig = {
87
89
  maxBytes: number;
88
90
  httpTimeoutMs: number;
89
91
  };
92
+ progress: {
93
+ enabled: boolean;
94
+ minIntervalMs: number;
95
+ notifyTools: string[];
96
+ notifyOnStart: boolean;
97
+ notifyOnError: boolean;
98
+ };
90
99
  toolSummary: {
91
100
  enabled: boolean;
92
101
  maxBytes: number;
@@ -160,7 +169,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
160
169
  planner: {
161
170
  enabled: true,
162
171
  mode: 'auto',
163
- minTokens: 600,
172
+ minTokens: 800,
164
173
  triggerRegex: '(plan|steps|roadmap|research|design|architecture|spec|strategy)',
165
174
  maxOutputTokens: 200,
166
175
  temperature: 0.2
@@ -170,7 +179,9 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
170
179
  maxOutputTokens: 120,
171
180
  temperature: 0,
172
181
  maxRetries: 1,
173
- allowToolCalls: false
182
+ allowToolCalls: false,
183
+ minPromptTokens: 400,
184
+ minResponseTokens: 160
174
185
  },
175
186
  tools: {
176
187
  maxToolSteps: 24,
@@ -198,6 +209,13 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
198
209
  maxBytes: 800_000,
199
210
  httpTimeoutMs: 20_000
200
211
  },
212
+ progress: {
213
+ enabled: true,
214
+ minIntervalMs: 30_000,
215
+ notifyTools: ['WebSearch', 'WebFetch', 'Bash', 'GitClone', 'NpmInstall'],
216
+ notifyOnStart: true,
217
+ notifyOnError: true
218
+ },
201
219
  toolSummary: {
202
220
  enabled: true,
203
221
  maxBytes: 60_000,
@@ -40,6 +40,11 @@ export interface ContainerInput {
40
40
  modelContextTokens?: number;
41
41
  modelMaxOutputTokens?: number;
42
42
  modelTemperature?: number;
43
+ timezone?: string;
44
+ disablePlanner?: boolean;
45
+ disableResponseValidation?: boolean;
46
+ responseValidationMaxRetries?: number;
47
+ disableMemoryExtraction?: boolean;
43
48
  streaming?: {
44
49
  enabled?: boolean;
45
50
  draftId?: number;
@@ -63,6 +68,12 @@ export interface ContainerOutput {
63
68
  session_recall_count?: number;
64
69
  memory_items_upserted?: number;
65
70
  memory_items_extracted?: number;
71
+ timings?: {
72
+ planner_ms?: number;
73
+ response_validation_ms?: number;
74
+ memory_extraction_ms?: number;
75
+ tool_ms?: number;
76
+ };
66
77
  tool_calls?: Array<{
67
78
  name: string;
68
79
  args?: unknown;
@@ -28,6 +28,8 @@ import {
28
28
  } from './memory.js';
29
29
  import { loadPromptPackWithCanary, formatTaskExtractionPack, formatResponseQualityPack, formatToolCallingPack, formatToolOutcomePack, formatMemoryPolicyPack, formatMemoryRecallPack, PromptPack } from './prompt-packs.js';
30
30
 
31
+ type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
32
+
31
33
 
32
34
  const SESSION_ROOT = '/workspace/session';
33
35
  const GROUP_DIR = '/workspace/group';
@@ -69,6 +71,89 @@ function log(message: string): void {
69
71
  console.error(`[agent-runner] ${message}`);
70
72
  }
71
73
 
74
+ function coerceTextFromContent(content: unknown): string {
75
+ if (!content) return '';
76
+ if (typeof content === 'string') return content;
77
+ if (Array.isArray(content)) {
78
+ return content.map(part => {
79
+ if (!part) return '';
80
+ if (typeof part === 'string') return part;
81
+ if (typeof part === 'object') {
82
+ const record = part as { text?: unknown; content?: unknown; value?: unknown };
83
+ if (typeof record.text === 'string') return record.text;
84
+ if (typeof record.content === 'string') return record.content;
85
+ if (typeof record.value === 'string') return record.value;
86
+ }
87
+ return '';
88
+ }).join('');
89
+ }
90
+ if (typeof content === 'object') {
91
+ const record = content as { text?: unknown; content?: unknown; value?: unknown };
92
+ if (typeof record.text === 'string') return record.text;
93
+ if (typeof record.content === 'string') return record.content;
94
+ if (typeof record.value === 'string') return record.value;
95
+ }
96
+ return '';
97
+ }
98
+
99
+ function extractTextFallbackFromResponse(response: unknown): string {
100
+ if (!response || typeof response !== 'object') return '';
101
+ const record = response as {
102
+ outputText?: unknown;
103
+ output_text?: unknown;
104
+ output?: unknown;
105
+ choices?: unknown;
106
+ };
107
+
108
+ if (typeof record.outputText === 'string' && record.outputText.trim()) {
109
+ return record.outputText;
110
+ }
111
+ if (typeof record.output_text === 'string' && record.output_text.trim()) {
112
+ return record.output_text;
113
+ }
114
+
115
+ if (Array.isArray(record.output)) {
116
+ const messageItem = record.output.find(item => !!item && typeof item === 'object' && (item as { type?: unknown }).type === 'message');
117
+ if (messageItem && typeof messageItem === 'object') {
118
+ const content = (messageItem as { content?: unknown }).content;
119
+ const text = coerceTextFromContent(content);
120
+ if (text.trim()) return text;
121
+ }
122
+ }
123
+
124
+ if (Array.isArray(record.choices) && record.choices.length > 0) {
125
+ const choice = record.choices[0] as { message?: { content?: unknown }; text?: unknown } | null | undefined;
126
+ if (choice?.message) {
127
+ const text = coerceTextFromContent(choice.message.content);
128
+ if (text.trim()) return text;
129
+ }
130
+ if (typeof choice?.text === 'string' && choice.text.trim()) {
131
+ return choice.text;
132
+ }
133
+ }
134
+
135
+ return '';
136
+ }
137
+
138
+ async function getTextWithFallback(result: OpenRouterResult, context: string): Promise<string> {
139
+ const text = await result.getText();
140
+ if (text && text.trim()) {
141
+ return text;
142
+ }
143
+ try {
144
+ const response = await result.getResponse();
145
+ const fallbackText = extractTextFallbackFromResponse(response);
146
+ if (fallbackText && fallbackText.trim()) {
147
+ log(`Recovered empty response text from payload (${context})`);
148
+ return fallbackText;
149
+ }
150
+ log(`Model returned empty response and fallback extraction failed (${context})`);
151
+ } catch (err) {
152
+ log(`Failed to recover empty response text (${context}): ${err instanceof Error ? err.message : String(err)}`);
153
+ }
154
+ return text;
155
+ }
156
+
72
157
  function writeOutput(output: ContainerOutput): void {
73
158
  console.log(OUTPUT_START_MARKER);
74
159
  console.log(JSON.stringify(output));
@@ -318,6 +403,7 @@ function buildSystemInstructions(params: {
318
403
  taskId?: string;
319
404
  isBackgroundJob: boolean;
320
405
  jobId?: string;
406
+ timezone?: string;
321
407
  planBlock?: string;
322
408
  taskExtractionPack?: PromptPack | null;
323
409
  responseQualityPack?: PromptPack | null;
@@ -425,6 +511,10 @@ function buildSystemInstructions(params: {
425
511
  ? `Behavior overrides:\n${JSON.stringify(params.behaviorConfig, null, 2)}`
426
512
  : '';
427
513
 
514
+ const timezoneNote = params.timezone
515
+ ? `Timezone: ${params.timezone}. Use this timezone when interpreting or presenting timestamps unless the user specifies another.`
516
+ : '';
517
+
428
518
  const scheduledNote = params.isScheduledTask
429
519
  ? `You are running as a scheduled task${params.taskId ? ` (task id: ${params.taskId})` : ''}. If you need to communicate, use \`mcp__dotclaw__send_message\`.`
430
520
  : '';
@@ -432,7 +522,7 @@ function buildSystemInstructions(params: {
432
522
  ? 'You are running in the background for a user request. Focus on completing the task and return a complete response without asking follow-up questions unless strictly necessary.'
433
523
  : '';
434
524
  const jobNote = params.isBackgroundJob
435
- ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Return a complete result. Use \`mcp__dotclaw__job_update\` for progress if needed. Prefer writing large outputs to the job artifacts directory.`
525
+ ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Return a complete result. If the task will take more than a few minutes, send periodic \`mcp__dotclaw__job_update\` messages with milestones or intermediate findings (roughly every ~5 minutes or after major steps). Prefer writing large outputs to the job artifacts directory.`
436
526
  : '';
437
527
  const jobArtifactsNote = params.isBackgroundJob && params.jobId
438
528
  ? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
@@ -496,6 +586,7 @@ function buildSystemInstructions(params: {
496
586
  browserAutomation,
497
587
  groupNotes,
498
588
  globalNotes,
589
+ timezoneNote,
499
590
  params.planBlock || '',
500
591
  toolCallingBlock,
501
592
  toolOutcomeBlock,
@@ -615,7 +706,7 @@ async function updateMemorySummary(params: {
615
706
  maxOutputTokens: params.maxOutputTokens,
616
707
  temperature: 0.1
617
708
  });
618
- const text = await result.getText();
709
+ const text = await getTextWithFallback(result, 'summary');
619
710
  return parseSummaryResponse(text);
620
711
  }
621
712
 
@@ -756,7 +847,7 @@ async function validateResponseQuality(params: {
756
847
  maxOutputTokens: params.maxOutputTokens,
757
848
  temperature: params.temperature
758
849
  });
759
- const text = await result.getText();
850
+ const text = await getTextWithFallback(result, 'response_validation');
760
851
  return parseResponseValidation(text);
761
852
  }
762
853
 
@@ -835,26 +926,30 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
835
926
  const maxToolSteps = Number.isFinite(input.maxToolSteps)
836
927
  ? Math.max(1, Math.floor(input.maxToolSteps as number))
837
928
  : agent.tools.maxToolSteps;
838
- const memoryExtractionEnabled = agent.memory.extraction.enabled;
929
+ const memoryExtractionEnabled = agent.memory.extraction.enabled && !input.disableMemoryExtraction;
839
930
  const isDaemon = process.env.DOTCLAW_DAEMON === '1';
840
931
  const memoryExtractionAsync = agent.memory.extraction.async;
841
932
  const memoryExtractionMaxMessages = agent.memory.extraction.maxMessages;
842
933
  const memoryExtractionMaxOutputTokens = agent.memory.extraction.maxOutputTokens;
843
934
  const memoryExtractScheduled = agent.memory.extractScheduled;
844
935
  const memoryArchiveSync = agent.memory.archiveSync;
845
- const plannerEnabled = agent.planner.enabled;
936
+ const plannerEnabled = agent.planner.enabled && !input.disablePlanner;
846
937
  const plannerMode = String(agent.planner.mode || 'auto').toLowerCase();
847
938
  const plannerMinTokens = agent.planner.minTokens;
848
939
  const plannerTrigger = buildPlannerTrigger(agent.planner.triggerRegex);
849
940
  const plannerModel = agent.models.planner;
850
941
  const plannerMaxOutputTokens = agent.planner.maxOutputTokens;
851
942
  const plannerTemperature = agent.planner.temperature;
852
- const responseValidateEnabled = agent.responseValidation.enabled;
943
+ const responseValidateEnabled = agent.responseValidation.enabled && !input.disableResponseValidation;
853
944
  const responseValidateModel = agent.models.responseValidation;
854
945
  const responseValidateMaxOutputTokens = agent.responseValidation.maxOutputTokens;
855
946
  const responseValidateTemperature = agent.responseValidation.temperature;
856
- const responseValidateMaxRetries = agent.responseValidation.maxRetries;
947
+ const responseValidateMaxRetries = Number.isFinite(input.responseValidationMaxRetries)
948
+ ? Math.max(0, Math.floor(input.responseValidationMaxRetries as number))
949
+ : agent.responseValidation.maxRetries;
857
950
  const responseValidateAllowToolCalls = agent.responseValidation.allowToolCalls;
951
+ const responseValidateMinPromptTokens = agent.responseValidation.minPromptTokens || 0;
952
+ const responseValidateMinResponseTokens = agent.responseValidation.minResponseTokens || 0;
858
953
  const maxContextMessageTokens = agent.context.maxContextMessageTokens;
859
954
  const streamingEnabled = Boolean(input.streaming?.enabled && typeof input.streaming?.draftId === 'number');
860
955
  const streamingDraftId = streamingEnabled ? input.streaming?.draftId : undefined;
@@ -884,6 +979,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
884
979
  const toolCalls: ToolCallRecord[] = [];
885
980
  let memoryItemsUpserted = 0;
886
981
  let memoryItemsExtracted = 0;
982
+ const timings: { planner_ms?: number; response_validation_ms?: number; memory_extraction_ms?: number; tool_ms?: number } = {};
887
983
  const ipc = createIpcHandlers({
888
984
  chatJid: input.chatJid,
889
985
  groupFolder: input.groupFolder,
@@ -897,7 +993,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
897
993
  onToolCall: (call) => {
898
994
  toolCalls.push(call);
899
995
  },
900
- policy: input.toolPolicy
996
+ policy: input.toolPolicy,
997
+ jobProgress: {
998
+ jobId: input.jobId,
999
+ enabled: Boolean(input.isBackgroundJob)
1000
+ }
901
1001
  });
902
1002
 
903
1003
  let streamLastSentAt = 0;
@@ -1083,6 +1183,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1083
1183
  taskId: input.taskId,
1084
1184
  isBackgroundJob: !!input.isBackgroundJob,
1085
1185
  jobId: input.jobId,
1186
+ timezone: typeof input.timezone === 'string' ? input.timezone : undefined,
1086
1187
  planBlock: planBlockValue,
1087
1188
  taskExtractionPack: taskPackResult?.pack || null,
1088
1189
  responseQualityPack: responseQualityResult?.pack || null,
@@ -1109,6 +1210,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1109
1210
  trigger: plannerTrigger
1110
1211
  })) {
1111
1212
  try {
1213
+ const plannerStartedAt = Date.now();
1112
1214
  const plannerPrompt = buildPlannerPrompt(plannerContextMessages);
1113
1215
  const plannerResult = await openrouter.callModel({
1114
1216
  model: plannerModel,
@@ -1117,11 +1219,12 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1117
1219
  maxOutputTokens: plannerMaxOutputTokens,
1118
1220
  temperature: plannerTemperature
1119
1221
  });
1120
- const plannerText = await plannerResult.getText();
1222
+ const plannerText = await getTextWithFallback(plannerResult, 'planner');
1121
1223
  const plan = parsePlannerResponse(plannerText);
1122
1224
  if (plan) {
1123
1225
  planBlock = formatPlanBlock(plan);
1124
1226
  }
1227
+ timings.planner_ms = Date.now() - plannerStartedAt;
1125
1228
  } catch (err) {
1126
1229
  log(`Planner failed: ${err instanceof Error ? err.message : String(err)}`);
1127
1230
  }
@@ -1212,7 +1315,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1212
1315
  }
1213
1316
  }
1214
1317
  if (!streamed || !localResponseText || !localResponseText.trim()) {
1215
- localResponseText = await result.getText();
1318
+ localResponseText = await getTextWithFallback(result, 'completion');
1216
1319
  if (localResponseText && localResponseText.trim()) {
1217
1320
  sendStreamUpdate(localResponseText, true);
1218
1321
  }
@@ -1245,6 +1348,8 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1245
1348
  modelToolCalls = firstAttempt.modelToolCalls;
1246
1349
 
1247
1350
  const shouldValidate = responseValidateEnabled
1351
+ && promptTokens >= responseValidateMinPromptTokens
1352
+ && completionTokens >= responseValidateMinResponseTokens
1248
1353
  && (responseValidateAllowToolCalls || modelToolCalls.length === 0);
1249
1354
  if (shouldValidate) {
1250
1355
  let retriesLeft = responseValidateMaxRetries;
@@ -1257,6 +1362,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1257
1362
  validationResult = { verdict: 'fail', issues: ['Response was empty.'], missing: [] };
1258
1363
  } else {
1259
1364
  try {
1365
+ const validationStartedAt = Date.now();
1260
1366
  validationResult = await validateResponseQuality({
1261
1367
  openrouter,
1262
1368
  model: responseValidateModel,
@@ -1265,6 +1371,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1265
1371
  maxOutputTokens: responseValidateMaxOutputTokens,
1266
1372
  temperature: responseValidateTemperature
1267
1373
  });
1374
+ timings.response_validation_ms = (timings.response_validation_ms ?? 0) + (Date.now() - validationStartedAt);
1268
1375
  } catch (err) {
1269
1376
  log(`Response validation failed: ${err instanceof Error ? err.message : String(err)}`);
1270
1377
  }
@@ -1306,6 +1413,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1306
1413
  session_recall_count: sessionRecallCount,
1307
1414
  memory_items_upserted: memoryItemsUpserted,
1308
1415
  memory_items_extracted: memoryItemsExtracted,
1416
+ timings: Object.keys(timings).length > 0 ? timings : undefined,
1309
1417
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
1310
1418
  latency_ms: latencyMs
1311
1419
  };
@@ -1335,6 +1443,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1335
1443
  const runMemoryExtraction = async () => {
1336
1444
  const extractionMessages = history.slice(-memoryExtractionMaxMessages);
1337
1445
  if (extractionMessages.length === 0) return;
1446
+ const extractionStartedAt = Date.now();
1338
1447
  const extractionPrompt = buildMemoryExtractionPrompt({
1339
1448
  assistantName,
1340
1449
  userId: input.userId,
@@ -1349,7 +1458,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1349
1458
  maxOutputTokens: memoryExtractionMaxOutputTokens,
1350
1459
  temperature: 0.1
1351
1460
  });
1352
- const extractionText = await extractionResult.getText();
1461
+ const extractionText = await getTextWithFallback(extractionResult, 'memory_extraction');
1353
1462
  const extractedItems = parseMemoryExtraction(extractionText);
1354
1463
  if (extractedItems.length === 0) return;
1355
1464
 
@@ -1380,6 +1489,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1380
1489
  memoryItemsExtracted += normalizedItems.length;
1381
1490
  memoryItemsUpserted += normalizedItems.length;
1382
1491
  }
1492
+ timings.memory_extraction_ms = (timings.memory_extraction_ms ?? 0) + (Date.now() - extractionStartedAt);
1383
1493
  };
1384
1494
 
1385
1495
  if (memoryExtractionEnabled && (!input.isScheduledTask || memoryExtractScheduled)) {
@@ -1398,6 +1508,12 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1398
1508
 
1399
1509
  // Normalize empty/whitespace-only responses to null
1400
1510
  const finalResult = responseText && responseText.trim() ? responseText : null;
1511
+ if (toolCalls.length > 0) {
1512
+ const totalToolMs = toolCalls.reduce((sum, call) => sum + (call.duration_ms || 0), 0);
1513
+ if (totalToolMs > 0) {
1514
+ timings.tool_ms = totalToolMs;
1515
+ }
1516
+ }
1401
1517
 
1402
1518
  return {
1403
1519
  status: 'success',
@@ -1413,6 +1529,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1413
1529
  session_recall_count: sessionRecallCount,
1414
1530
  memory_items_upserted: memoryItemsUpserted,
1415
1531
  memory_items_extracted: memoryItemsExtracted,
1532
+ timings: Object.keys(timings).length > 0 ? timings : undefined,
1416
1533
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
1417
1534
  latency_ms: latencyMs
1418
1535
  };