create-walle 0.9.28 → 0.9.30
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 +2 -2
- package/bin/create-walle.js +166 -6
- package/package.json +1 -1
- package/template/bin/ctm-launch.sh +70 -18
- package/template/bin/dev.sh +18 -0
- package/template/bin/ensure-stable-node.js +11 -0
- package/template/bin/node-bin.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +214 -23
- package/template/claude-task-manager/db.js +884 -50
- package/template/claude-task-manager/docs/backfill-incremental-no-main-fallback.md +48 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +21 -0
- package/template/claude-task-manager/docs/conversation-log-redesign.html +587 -0
- package/template/claude-task-manager/docs/session-title-authority.md +8 -3
- package/template/claude-task-manager/lib/auth-rules.js +13 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +63 -0
- package/template/claude-task-manager/lib/codex-config-guard.js +124 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +93 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +5 -4
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +114 -0
- package/template/claude-task-manager/lib/db-owner-task-queue.js +67 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +5 -1
- package/template/claude-task-manager/lib/desktop-fork.js +81 -0
- package/template/claude-task-manager/lib/headless-term-service.js +251 -4
- package/template/claude-task-manager/lib/message-identity.js +115 -0
- package/template/claude-task-manager/lib/mirror-feed-guards.js +25 -0
- package/template/claude-task-manager/lib/mirror-feed-sanitize.js +45 -0
- package/template/claude-task-manager/lib/path-suggest.js +77 -0
- package/template/claude-task-manager/lib/prompt-index-inputs.js +136 -0
- package/template/claude-task-manager/lib/real-node.js +36 -4
- package/template/claude-task-manager/lib/restore-auto-resume-policy.js +67 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +20 -0
- package/template/claude-task-manager/lib/restore-terminal-dims.js +109 -0
- package/template/claude-task-manager/lib/resume-cwd.js +124 -3
- package/template/claude-task-manager/lib/runtime-approval-recorder.js +152 -0
- package/template/claude-task-manager/lib/runtime-context-truth.js +236 -0
- package/template/claude-task-manager/lib/runtime-contract.js +195 -0
- package/template/claude-task-manager/lib/runtime-history-builder.js +205 -0
- package/template/claude-task-manager/lib/runtime-hook-bus.js +98 -0
- package/template/claude-task-manager/lib/runtime-input-queue.js +114 -0
- package/template/claude-task-manager/lib/runtime-input-recorder.js +156 -0
- package/template/claude-task-manager/lib/runtime-lineage.js +189 -0
- package/template/claude-task-manager/lib/runtime-registry.js +263 -0
- package/template/claude-task-manager/lib/runtime-session-history.js +41 -0
- package/template/claude-task-manager/lib/scrollback-snapshot-policy.js +37 -0
- package/template/claude-task-manager/lib/server-phase-conditions.js +103 -0
- package/template/claude-task-manager/lib/session-content-backfill.js +55 -8
- package/template/claude-task-manager/lib/session-db-read-contract.js +67 -0
- package/template/claude-task-manager/lib/session-history.js +93 -5
- package/template/claude-task-manager/lib/session-host-manager.js +154 -2
- package/template/claude-task-manager/lib/session-messages-defer.js +50 -0
- package/template/claude-task-manager/lib/session-messages-page.js +13 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +48 -29
- package/template/claude-task-manager/lib/session-stream.js +80 -17
- package/template/claude-task-manager/lib/session-title-signals.js +54 -0
- package/template/claude-task-manager/lib/session-token-usage.js +13 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +41 -0
- package/template/claude-task-manager/lib/state-sync/frame-emitter.js +214 -0
- package/template/claude-task-manager/lib/state-sync/frame-rate.js +75 -0
- package/template/claude-task-manager/lib/state-sync/row-serializer.js +166 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +19 -3
- package/template/claude-task-manager/lib/transcript-ingest-chunker.js +41 -0
- package/template/claude-task-manager/lib/transcript-store.js +99 -7
- package/template/claude-task-manager/lib/wal-checkpoint-policy.js +40 -0
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +100 -9
- package/template/claude-task-manager/lib/worktree-output-binding.js +93 -0
- package/template/claude-task-manager/lib/write-coalescer.js +83 -0
- package/template/claude-task-manager/public/css/walle-session.css +4 -0
- package/template/claude-task-manager/public/css/walle.css +0 -66
- package/template/claude-task-manager/public/index.html +1707 -266
- package/template/claude-task-manager/public/js/feedback.js +8 -1
- package/template/claude-task-manager/public/js/message-renderer.js +72 -2
- package/template/claude-task-manager/public/js/session-phase.js +4 -0
- package/template/claude-task-manager/public/js/session-status-precedence.js +7 -173
- package/template/claude-task-manager/public/js/setup.js +46 -3
- package/template/claude-task-manager/public/js/state-sync-client.js +257 -0
- package/template/claude-task-manager/public/js/state-sync-predictor.js +41 -0
- package/template/claude-task-manager/public/js/stream-view.js +113 -9
- package/template/claude-task-manager/public/js/terminal-reconciler.js +24 -4
- package/template/claude-task-manager/public/js/walle-session.js +239 -19
- package/template/claude-task-manager/public/js/walle.js +32 -119
- package/template/claude-task-manager/queue-engine.js +140 -0
- package/template/claude-task-manager/server.js +2802 -416
- package/template/claude-task-manager/session-integrity.js +16 -1
- package/template/claude-task-manager/workers/db-owner-worker.js +23 -6
- package/template/claude-task-manager/workers/read-pool-worker.js +55 -1
- package/template/claude-task-manager/workers/session-host-pool-process.js +193 -0
- package/template/claude-task-manager/workers/session-host-process.js +47 -11
- package/template/claude-task-manager/workers/state-detectors/codex.js +33 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +191 -31
- package/template/wall-e/api-walle.js +97 -52
- package/template/wall-e/auth/flow-manager.js +78 -1
- package/template/wall-e/auth/provider-flows.js +56 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +138 -5
- package/template/wall-e/brain.js +175 -13
- package/template/wall-e/chat.js +46 -1
- package/template/wall-e/embeddings.js +70 -0
- package/template/wall-e/events/event-bus.js +11 -1
- package/template/wall-e/http/auth.js +3 -1
- package/template/wall-e/http/model-admin.js +22 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +36 -4
- package/template/wall-e/lib/diagnostics-flags.js +9 -0
- package/template/wall-e/lib/event-loop-monitor.js +84 -5
- package/template/wall-e/lib/mcp-scan-lifecycle.js +247 -0
- package/template/wall-e/lib/parent-brain-owner-client.js +109 -0
- package/template/wall-e/lib/runtime-process-inventory.js +114 -0
- package/template/wall-e/lib/runtime-worker-pool.js +214 -23
- package/template/wall-e/lib/scheduler-worker-jobs.js +49 -4
- package/template/wall-e/lib/scheduler.js +320 -35
- package/template/wall-e/lib/slack-identity.js +120 -0
- package/template/wall-e/lib/slack-permalink.js +107 -0
- package/template/wall-e/lib/slack-web.js +174 -0
- package/template/wall-e/lib/worker-thread-pool.js +55 -4
- package/template/wall-e/llm/claude-cli.js +21 -3
- package/template/wall-e/llm/cli-binary.js +90 -0
- package/template/wall-e/llm/codex-cli.js +113 -49
- package/template/wall-e/llm/default-fallback.js +10 -4
- package/template/wall-e/llm/mlx.js +46 -8
- package/template/wall-e/llm/model-catalog.js +129 -17
- package/template/wall-e/llm/provider-detector.js +112 -22
- package/template/wall-e/loops/backfill.js +32 -16
- package/template/wall-e/loops/ingest.js +50 -16
- package/template/wall-e/loops/tasks.js +521 -25
- package/template/wall-e/mcp-server.js +215 -6
- package/template/wall-e/memory/ctm-session-context.js +93 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +15 -23
- package/template/wall-e/skills/_bundled/gws-workspace/gws-router +237 -0
- package/template/wall-e/skills/_bundled/gws-workspace/setup.js +112 -1
- package/template/wall-e/skills/_bundled/mcp-scan/run.js +265 -41
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +434 -93
- package/template/wall-e/skills/internal-skill-registry.js +27 -5
- package/template/wall-e/skills/mcp-client.js +18 -3
- package/template/wall-e/skills/script-skill-runner.js +53 -5
- package/template/wall-e/skills/skill-planner.js +5 -26
- package/template/wall-e/training/real-trajectory-miner.js +24 -114
- package/template/wall-e/utils/dedup.js +165 -66
- package/template/wall-e/weather-runtime.js +12 -4
- package/template/wall-e/workers/brain-owner-worker.js +68 -0
- package/template/wall-e/workers/runtime-worker.js +4 -0
- package/template/website/index.html +3 -0
|
@@ -7,10 +7,22 @@ const runtimeHealth = require('../lib/runtime-health');
|
|
|
7
7
|
const { retryOnWriteLockBusy } = require('../shared/sqlite-write-lock');
|
|
8
8
|
|
|
9
9
|
const WALL_E_DIR = path.join(__dirname, '..');
|
|
10
|
+
const SLACK_AUTO_RESUME_KV = 'tasks:slack_auto_resume_next_at';
|
|
11
|
+
const DEFAULT_SLACK_AUTO_RESUME_COOLDOWN_MS = 15 * 60 * 1000;
|
|
12
|
+
const DEFAULT_RECURRING_SCHEDULE_REPAIR_INTERVAL_MS = 5 * 60 * 1000;
|
|
13
|
+
const DEFAULT_MAX_TASKS_PER_TICK = 1;
|
|
14
|
+
const DEFAULT_MAX_TASK_TICK_MS = 5000;
|
|
15
|
+
const DEFAULT_TASK_CONTINUE_DELAY_MS = 2000;
|
|
16
|
+
const DEFAULT_TASK_MAINTENANCE_MIN_REMAINING_MS = 750;
|
|
17
|
+
const DEFAULT_TASK_EXECUTION_TIMEOUT_MS = 120000;
|
|
18
|
+
const DEFAULT_SCRIPT_TASK_EXECUTION_TIMEOUT_MS = 30000;
|
|
19
|
+
const DEFAULT_TASK_KILL_GRACE_MS = 1500;
|
|
20
|
+
const CALENDAR_ACCESS_QUESTION = 'Calendar access denied — WALL-E cannot sync Calendar until you grant macOS Calendar permission.';
|
|
10
21
|
|
|
11
22
|
// ── Live log buffer — in-memory, keyed by task ID ──
|
|
12
23
|
const taskLogs = new Map(); // task_id → { lines: string[], startedAt, status }
|
|
13
24
|
const taskProcesses = new Map(); // task_id → child process (for stopping)
|
|
25
|
+
let lastRecurringScheduleRepairMs = 0;
|
|
14
26
|
|
|
15
27
|
function stopTask(taskId) {
|
|
16
28
|
const child = taskProcesses.get(taskId);
|
|
@@ -77,6 +89,137 @@ async function safeUpdateTask(taskId, updates, options = {}) {
|
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
function positiveIntEnv(name, fallback) {
|
|
93
|
+
const n = Number.parseInt(process.env[name] || '', 10);
|
|
94
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function positiveIntValue(value, fallback, max = Number.POSITIVE_INFINITY) {
|
|
98
|
+
const n = Number.parseInt(String(value || ''), 10);
|
|
99
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
100
|
+
return Math.min(max, Math.max(1, n));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nonNegativeIntValue(value, fallback, max = Number.POSITIVE_INFINITY) {
|
|
104
|
+
const n = Number.parseInt(String(value ?? ''), 10);
|
|
105
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
106
|
+
return Math.min(max, Math.max(0, n));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createTaskTimeoutError(task, timeoutMs) {
|
|
110
|
+
const title = task?.title ? ` "${String(task.title).slice(0, 120)}"` : '';
|
|
111
|
+
const err = new Error(`Task${title} timed out after ${timeoutMs}ms`);
|
|
112
|
+
err.code = 'WALL_E_TASK_TIMEOUT';
|
|
113
|
+
err.timeoutMs = timeoutMs;
|
|
114
|
+
err.taskId = task?.id || null;
|
|
115
|
+
err.taskTitle = task?.title || null;
|
|
116
|
+
return err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function slackAutoResumeCooldownMs() {
|
|
120
|
+
return positiveIntEnv('WALL_E_SLACK_AUTO_RESUME_COOLDOWN_MS', DEFAULT_SLACK_AUTO_RESUME_COOLDOWN_MS);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function recurringScheduleRepairIntervalMs() {
|
|
124
|
+
return positiveIntEnv('WALL_E_RECURRING_TASK_REPAIR_INTERVAL_MS', DEFAULT_RECURRING_SCHEDULE_REPAIR_INTERVAL_MS);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getSlackAutoResumeNextProbeMs() {
|
|
128
|
+
try {
|
|
129
|
+
const raw = brain.getKv(SLACK_AUTO_RESUME_KV);
|
|
130
|
+
const next = Date.parse(raw || '');
|
|
131
|
+
return Number.isFinite(next) ? next : 0;
|
|
132
|
+
} catch {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function shouldRunSlackAutoResumeProbe(nowMs = Date.now()) {
|
|
138
|
+
const nextProbeAt = getSlackAutoResumeNextProbeMs();
|
|
139
|
+
return !nextProbeAt || nowMs >= nextProbeAt;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setSlackAutoResumeCooldown(nowMs = Date.now()) {
|
|
143
|
+
const next = new Date(nowMs + slackAutoResumeCooldownMs()).toISOString();
|
|
144
|
+
try {
|
|
145
|
+
brain.setKv(SLACK_AUTO_RESUME_KV, next);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`[tasks] Failed to persist Slack auto-resume cooldown: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
return next;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clearSlackAutoResumeCooldown() {
|
|
153
|
+
try {
|
|
154
|
+
brain.setKv(SLACK_AUTO_RESUME_KV, null);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(`[tasks] Failed to clear Slack auto-resume cooldown: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isCalendarAccessDeniedError(task = {}, errMsg = '') {
|
|
161
|
+
const skill = String(task.skill || '').toLowerCase();
|
|
162
|
+
const title = String(task.title || '').toLowerCase();
|
|
163
|
+
const text = String(errMsg || '');
|
|
164
|
+
const isCalendarTask = skill.includes('calendar') || title.includes('calendar');
|
|
165
|
+
return isCalendarTask && /access_denied|calendar access(?: (?:was|still))? denied/i.test(text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function classifyNonRetryableTaskBlocker(task = {}) {
|
|
169
|
+
if (task.type === 'recurring' && isCalendarAccessDeniedError(task, task.error || '')) {
|
|
170
|
+
return {
|
|
171
|
+
type: 'calendar_permission',
|
|
172
|
+
question: CALENDAR_ACCESS_QUESTION,
|
|
173
|
+
error: 'Calendar access denied. Grant macOS Calendar access to Wall-E, then resume this task.',
|
|
174
|
+
instructions: 'Grant Calendar access to the Wall-E/terminal process in System Settings > Privacy & Security > Calendars, then resume the Calendar task.',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function pauseBlockedRecurringTask(task, blocker, reason = 'non-retryable-task-blocker') {
|
|
181
|
+
if (!task || !blocker) return false;
|
|
182
|
+
const pausedAt = new Date().toISOString();
|
|
183
|
+
ensureActionQuestionOnce({
|
|
184
|
+
question: blocker.question,
|
|
185
|
+
match: q => String(q.question || '').includes('Calendar access denied'),
|
|
186
|
+
context: {
|
|
187
|
+
action: blocker.type,
|
|
188
|
+
failed_task: task.title,
|
|
189
|
+
error: task.error || blocker.error,
|
|
190
|
+
instructions: blocker.instructions,
|
|
191
|
+
},
|
|
192
|
+
priority: 'high',
|
|
193
|
+
});
|
|
194
|
+
const ok = await safeUpdateTask(task.id, {
|
|
195
|
+
status: 'paused',
|
|
196
|
+
error: blocker.error,
|
|
197
|
+
completed_at: pausedAt,
|
|
198
|
+
last_run_at: task.last_run_at || pausedAt,
|
|
199
|
+
next_run_at: null,
|
|
200
|
+
}, { reason });
|
|
201
|
+
if (ok) console.log(`[tasks] Auto-paused: ${task.title} (${blocker.type})`);
|
|
202
|
+
return ok;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ensureActionQuestionOnce({ question, context, priority = 'high', match }) {
|
|
206
|
+
try {
|
|
207
|
+
const existing = brain
|
|
208
|
+
.listQuestions({ status: 'pending', question_type: 'action_required' })
|
|
209
|
+
.find(q => typeof match === 'function' ? match(q) : q.question === question);
|
|
210
|
+
if (existing) return existing;
|
|
211
|
+
return brain.insertQuestion({
|
|
212
|
+
question_type: 'action_required',
|
|
213
|
+
question,
|
|
214
|
+
context: context ? JSON.stringify(context) : null,
|
|
215
|
+
priority,
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error('[tasks] Failed to create action question:', err.message);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
80
223
|
/**
|
|
81
224
|
* On daemon startup, recover tasks that were interrupted (stuck in 'running').
|
|
82
225
|
* Resets them to 'pending' so they get re-picked up.
|
|
@@ -98,6 +241,64 @@ function recoverInterruptedTasks() {
|
|
|
98
241
|
return stuck.length;
|
|
99
242
|
}
|
|
100
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Recurring tasks must have an explicit next_run_at. A null next_run_at on a
|
|
246
|
+
* recurring task used to make it eligible every scheduler tick, so a blocked
|
|
247
|
+
* external sync (Calendar/Gmail/Slack) could create a permanent 10s task loop.
|
|
248
|
+
* Repair the malformed rows on a low-frequency cadence; unparseable schedules
|
|
249
|
+
* are paused so the UI can surface them instead of spinning silently.
|
|
250
|
+
*/
|
|
251
|
+
async function repairRecurringTaskSchedules(nowMs = Date.now()) {
|
|
252
|
+
const intervalMs = recurringScheduleRepairIntervalMs();
|
|
253
|
+
if (intervalMs > 0 && nowMs - lastRecurringScheduleRepairMs < intervalMs) {
|
|
254
|
+
return { repaired: 0, paused: 0, skipped: true };
|
|
255
|
+
}
|
|
256
|
+
lastRecurringScheduleRepairMs = nowMs;
|
|
257
|
+
|
|
258
|
+
let rows = [];
|
|
259
|
+
try {
|
|
260
|
+
rows = brain.getDb().prepare(`
|
|
261
|
+
SELECT id, title, schedule
|
|
262
|
+
FROM tasks
|
|
263
|
+
WHERE status = 'pending'
|
|
264
|
+
AND type = 'recurring'
|
|
265
|
+
AND (next_run_at IS NULL OR trim(next_run_at) = '')
|
|
266
|
+
ORDER BY updated_at ASC
|
|
267
|
+
LIMIT 25
|
|
268
|
+
`).all();
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.warn(`[tasks] Recurring schedule repair skipped: ${err.message}`);
|
|
271
|
+
return { repaired: 0, paused: 0, error: err.message };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let repaired = 0;
|
|
275
|
+
let paused = 0;
|
|
276
|
+
for (const row of rows) {
|
|
277
|
+
const nextRunAt = computeNextDue(row.schedule);
|
|
278
|
+
if (nextRunAt) {
|
|
279
|
+
const ok = await safeUpdateTask(row.id, {
|
|
280
|
+
next_run_at: nextRunAt,
|
|
281
|
+
error: null,
|
|
282
|
+
}, { bestEffort: true, reason: 'recurring-schedule-repair' });
|
|
283
|
+
if (ok) repaired++;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const ok = await safeUpdateTask(row.id, {
|
|
288
|
+
status: 'paused',
|
|
289
|
+
error: 'Recurring task has no next run time and its schedule could not be parsed. Edit the schedule or run it manually.',
|
|
290
|
+
completed_at: new Date(nowMs).toISOString(),
|
|
291
|
+
next_run_at: null,
|
|
292
|
+
}, { bestEffort: true, reason: 'recurring-schedule-invalid' });
|
|
293
|
+
if (ok) paused++;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (repaired > 0 || paused > 0) {
|
|
297
|
+
console.log(`[tasks] Repaired recurring task schedule state: ${repaired} rescheduled, ${paused} paused`);
|
|
298
|
+
}
|
|
299
|
+
return { repaired, paused, skipped: false };
|
|
300
|
+
}
|
|
301
|
+
|
|
101
302
|
/**
|
|
102
303
|
* Try to auto-resume Slack tasks that were paused due to token expiry.
|
|
103
304
|
* Runs at the start of each task loop cycle.
|
|
@@ -110,10 +311,22 @@ async function tryResumeSlackTasks() {
|
|
|
110
311
|
(t.error && t.error.includes('Slack token expired'))
|
|
111
312
|
));
|
|
112
313
|
if (pausedSlack.length === 0) return;
|
|
314
|
+
if (!shouldRunSlackAutoResumeProbe()) return;
|
|
113
315
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
316
|
+
// Token refresh can hit network/auth paths. Cool it down persistently so
|
|
317
|
+
// paused Slack tasks do not make every scheduler tick expensive.
|
|
318
|
+
setSlackAutoResumeCooldown();
|
|
319
|
+
const op = runtimeHealth.beginOperation('tasks.slackAutoResumeProbe', { paused: pausedSlack.length });
|
|
320
|
+
let valid = false;
|
|
321
|
+
try {
|
|
322
|
+
const slackMcp = require('../tools/slack-mcp');
|
|
323
|
+
valid = await slackMcp.ensureValidToken();
|
|
324
|
+
op.end({ meta: { valid } });
|
|
325
|
+
} catch (err) {
|
|
326
|
+
op.end({ ok: false, error: err, meta: { valid: false } });
|
|
327
|
+
console.warn(`[tasks] Slack auto-resume probe failed; retrying after cooldown: ${err.message}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
117
330
|
if (!valid) return;
|
|
118
331
|
|
|
119
332
|
// Token is valid — un-pause all Slack tasks
|
|
@@ -125,23 +338,189 @@ async function tryResumeSlackTasks() {
|
|
|
125
338
|
}, { reason: 'slack-auto-resume' });
|
|
126
339
|
console.log(`[tasks] Auto-resumed: ${t.title} (Slack token refreshed)`);
|
|
127
340
|
}
|
|
128
|
-
|
|
341
|
+
clearSlackAutoResumeCooldown();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn(`[tasks] Slack auto-resume skipped: ${err.message}`);
|
|
344
|
+
}
|
|
129
345
|
}
|
|
130
346
|
|
|
131
347
|
/**
|
|
132
348
|
* Task executor loop — picks up due tasks and runs them.
|
|
133
349
|
*/
|
|
134
|
-
async function runDueTasks() {
|
|
135
|
-
|
|
136
|
-
|
|
350
|
+
async function runDueTasks(options = {}) {
|
|
351
|
+
const maxTasks = positiveIntValue(
|
|
352
|
+
options.maxTasks ?? process.env.WALL_E_TASKS_MAX_PER_TICK ?? process.env.WALLE_TASKS_MAX_PER_TICK,
|
|
353
|
+
DEFAULT_MAX_TASKS_PER_TICK,
|
|
354
|
+
25,
|
|
355
|
+
);
|
|
356
|
+
const maxTickMs = positiveIntValue(
|
|
357
|
+
options.maxTickMs ?? process.env.WALL_E_TASKS_MAX_TICK_MS ?? process.env.WALLE_TASKS_MAX_TICK_MS,
|
|
358
|
+
DEFAULT_MAX_TASK_TICK_MS,
|
|
359
|
+
120000,
|
|
360
|
+
);
|
|
361
|
+
const continueDelayMs = positiveIntValue(
|
|
362
|
+
options.continueDelayMs ?? process.env.WALL_E_TASKS_CONTINUE_DELAY_MS ?? process.env.WALLE_TASKS_CONTINUE_DELAY_MS,
|
|
363
|
+
DEFAULT_TASK_CONTINUE_DELAY_MS,
|
|
364
|
+
60000,
|
|
365
|
+
);
|
|
366
|
+
const maintenanceMinRemainingMs = nonNegativeIntValue(
|
|
367
|
+
options.maintenanceMinRemainingMs
|
|
368
|
+
?? process.env.WALL_E_TASKS_MAINTENANCE_MIN_REMAINING_MS
|
|
369
|
+
?? process.env.WALLE_TASKS_MAINTENANCE_MIN_REMAINING_MS,
|
|
370
|
+
DEFAULT_TASK_MAINTENANCE_MIN_REMAINING_MS,
|
|
371
|
+
30000,
|
|
372
|
+
);
|
|
373
|
+
const taskExecutionTimeoutMs = nonNegativeIntValue(
|
|
374
|
+
options.taskExecutionTimeoutMs
|
|
375
|
+
?? process.env.WALL_E_TASK_EXECUTION_TIMEOUT_MS
|
|
376
|
+
?? process.env.WALLE_TASK_EXECUTION_TIMEOUT_MS,
|
|
377
|
+
DEFAULT_TASK_EXECUTION_TIMEOUT_MS,
|
|
378
|
+
240000,
|
|
379
|
+
);
|
|
380
|
+
const scriptTimeoutFallbackMs = taskExecutionTimeoutMs === 0
|
|
381
|
+
? 0
|
|
382
|
+
: Math.min(taskExecutionTimeoutMs, DEFAULT_SCRIPT_TASK_EXECUTION_TIMEOUT_MS);
|
|
383
|
+
const scriptTaskExecutionTimeoutMs = nonNegativeIntValue(
|
|
384
|
+
options.scriptTaskExecutionTimeoutMs
|
|
385
|
+
?? process.env.WALL_E_SCRIPT_TASK_EXECUTION_TIMEOUT_MS
|
|
386
|
+
?? process.env.WALLE_SCRIPT_TASK_EXECUTION_TIMEOUT_MS,
|
|
387
|
+
scriptTimeoutFallbackMs,
|
|
388
|
+
120000,
|
|
389
|
+
);
|
|
390
|
+
const chatTaskExecutionTimeoutMs = nonNegativeIntValue(
|
|
391
|
+
options.chatTaskExecutionTimeoutMs
|
|
392
|
+
?? process.env.WALL_E_CHAT_TASK_EXECUTION_TIMEOUT_MS
|
|
393
|
+
?? process.env.WALLE_CHAT_TASK_EXECUTION_TIMEOUT_MS,
|
|
394
|
+
taskExecutionTimeoutMs,
|
|
395
|
+
240000,
|
|
396
|
+
);
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
const phaseDurations = [];
|
|
399
|
+
const maintenance = {};
|
|
400
|
+
const elapsedMs = () => Date.now() - startedAt;
|
|
401
|
+
const remainingMs = () => Math.max(0, maxTickMs - elapsedMs());
|
|
402
|
+
const runPhase = async (name, fn, extraMeta = {}) => {
|
|
403
|
+
const phaseStart = Date.now();
|
|
404
|
+
try {
|
|
405
|
+
const value = await fn();
|
|
406
|
+
const durationMs = Date.now() - phaseStart;
|
|
407
|
+
phaseDurations.push({ name, durationMs, ok: true, ...extraMeta });
|
|
408
|
+
runtimeHealth.recordOperation(`tasks.phase.${name}`, durationMs, {
|
|
409
|
+
meta: { remainingMs: remainingMs(), maxTickMs, ...extraMeta },
|
|
410
|
+
});
|
|
411
|
+
return value;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
const durationMs = Date.now() - phaseStart;
|
|
414
|
+
phaseDurations.push({
|
|
415
|
+
name,
|
|
416
|
+
durationMs,
|
|
417
|
+
ok: false,
|
|
418
|
+
error: String(err?.message || err).slice(0, 120),
|
|
419
|
+
...extraMeta,
|
|
420
|
+
});
|
|
421
|
+
runtimeHealth.recordOperation(`tasks.phase.${name}`, durationMs, {
|
|
422
|
+
ok: false,
|
|
423
|
+
error: err?.code || err?.name || 'error',
|
|
424
|
+
meta: { remainingMs: remainingMs(), maxTickMs, ...extraMeta },
|
|
425
|
+
});
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (options.runMaintenance !== false) {
|
|
431
|
+
maintenance.repair = await runPhase(
|
|
432
|
+
'maintenance.repairRecurringSchedules',
|
|
433
|
+
() => repairRecurringTaskSchedules(),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (remainingMs() >= maintenanceMinRemainingMs) {
|
|
437
|
+
maintenance.slackAutoResume = await runPhase(
|
|
438
|
+
'maintenance.slackAutoResume',
|
|
439
|
+
() => tryResumeSlackTasks(),
|
|
440
|
+
);
|
|
441
|
+
} else {
|
|
442
|
+
maintenance.slackAutoResume = { skipped: true, reason: 'tick_budget_low' };
|
|
443
|
+
phaseDurations.push({
|
|
444
|
+
name: 'maintenance.slackAutoResume',
|
|
445
|
+
durationMs: 0,
|
|
446
|
+
ok: true,
|
|
447
|
+
skipped: true,
|
|
448
|
+
reason: 'tick_budget_low',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (elapsedMs() >= maxTickMs) {
|
|
454
|
+
return {
|
|
455
|
+
processed: 0,
|
|
456
|
+
reconciled: 0,
|
|
457
|
+
considered: 0,
|
|
458
|
+
fetched: 0,
|
|
459
|
+
maxTasks,
|
|
460
|
+
maxTickMs,
|
|
461
|
+
taskExecutionTimeoutMs,
|
|
462
|
+
scriptTaskExecutionTimeoutMs,
|
|
463
|
+
chatTaskExecutionTimeoutMs,
|
|
464
|
+
maintenance,
|
|
465
|
+
phaseDurations,
|
|
466
|
+
budgetExhausted: true,
|
|
467
|
+
_continue: true,
|
|
468
|
+
_continueDelayMs: continueDelayMs,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const fetched = await runPhase('scanDueTasks', () => brain.getDueTasks({ limit: maxTasks + 1 }));
|
|
473
|
+
if (fetched.length === 0) {
|
|
474
|
+
return {
|
|
475
|
+
processed: 0,
|
|
476
|
+
reconciled: 0,
|
|
477
|
+
considered: 0,
|
|
478
|
+
fetched: 0,
|
|
479
|
+
maxTasks,
|
|
480
|
+
maxTickMs,
|
|
481
|
+
taskExecutionTimeoutMs,
|
|
482
|
+
scriptTaskExecutionTimeoutMs,
|
|
483
|
+
chatTaskExecutionTimeoutMs,
|
|
484
|
+
maintenance,
|
|
485
|
+
phaseDurations,
|
|
486
|
+
budgetExhausted: false,
|
|
487
|
+
_continue: false,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
137
490
|
|
|
138
|
-
const tasks =
|
|
139
|
-
|
|
491
|
+
const tasks = fetched.slice(0, maxTasks);
|
|
492
|
+
const hadMore = fetched.length > maxTasks;
|
|
140
493
|
|
|
141
494
|
let processed = 0;
|
|
495
|
+
let reconciled = 0;
|
|
496
|
+
let budgetExhausted = false;
|
|
142
497
|
for (const task of tasks) {
|
|
498
|
+
if (elapsedMs() >= maxTickMs && (processed > 0 || reconciled > 0)) {
|
|
499
|
+
budgetExhausted = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const taskPhaseStart = Date.now();
|
|
504
|
+
const taskPhase = {
|
|
505
|
+
name: `task.${task.execution || (task.skill ? 'skill' : 'chat')}`,
|
|
506
|
+
durationMs: 0,
|
|
507
|
+
ok: true,
|
|
508
|
+
taskId: task.id,
|
|
509
|
+
taskType: task.type || '',
|
|
510
|
+
};
|
|
511
|
+
|
|
143
512
|
if (task.status === 'running') continue;
|
|
144
513
|
|
|
514
|
+
const blocker = classifyNonRetryableTaskBlocker(task);
|
|
515
|
+
if (blocker) {
|
|
516
|
+
await pauseBlockedRecurringTask(task, blocker, 'stored-task-blocker-reconcile');
|
|
517
|
+
reconciled++;
|
|
518
|
+
taskPhase.name = 'task.reconcileBlocked';
|
|
519
|
+
taskPhase.durationMs = Date.now() - taskPhaseStart;
|
|
520
|
+
phaseDurations.push(taskPhase);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
145
524
|
const now = new Date().toISOString();
|
|
146
525
|
const hasCheckpoint = task.checkpoint ? ' (resuming from checkpoint)' : '';
|
|
147
526
|
await safeUpdateTask(task.id, { status: 'running', started_at: now }, { reason: 'task-start' });
|
|
@@ -151,11 +530,18 @@ async function runDueTasks() {
|
|
|
151
530
|
let resultText;
|
|
152
531
|
|
|
153
532
|
if (task.skill) {
|
|
154
|
-
resultText = await executeSkill(task.id, task
|
|
533
|
+
resultText = await executeSkill(task.id, task, {
|
|
534
|
+
timeoutMs: taskExecutionTimeoutMs,
|
|
535
|
+
scriptTimeoutMs: scriptTaskExecutionTimeoutMs,
|
|
536
|
+
chatTimeoutMs: chatTaskExecutionTimeoutMs,
|
|
537
|
+
});
|
|
155
538
|
} else if (task.execution === 'script' && task.script) {
|
|
156
|
-
resultText = await executeScript(task.id, task.script, task.checkpoint
|
|
539
|
+
resultText = await executeScript(task.id, task.script, task.checkpoint, null, {
|
|
540
|
+
timeoutMs: scriptTaskExecutionTimeoutMs,
|
|
541
|
+
taskTitle: task.title,
|
|
542
|
+
});
|
|
157
543
|
} else {
|
|
158
|
-
resultText = await executeChat(task.id, task);
|
|
544
|
+
resultText = await executeChat(task.id, task, { timeoutMs: chatTaskExecutionTimeoutMs });
|
|
159
545
|
}
|
|
160
546
|
|
|
161
547
|
const completedAt = new Date().toISOString();
|
|
@@ -230,7 +616,13 @@ async function runDueTasks() {
|
|
|
230
616
|
|
|
231
617
|
processed++;
|
|
232
618
|
console.log(`[tasks] Completed: ${task.title} (${task.execution || 'chat'})`);
|
|
619
|
+
taskPhase.durationMs = Date.now() - taskPhaseStart;
|
|
620
|
+
phaseDurations.push(taskPhase);
|
|
233
621
|
} catch (err) {
|
|
622
|
+
taskPhase.ok = false;
|
|
623
|
+
taskPhase.durationMs = Date.now() - taskPhaseStart;
|
|
624
|
+
taskPhase.error = String(err?.message || err).slice(0, 120);
|
|
625
|
+
phaseDurations.push(taskPhase);
|
|
234
626
|
console.error('[tasks] Failed:', task.title, err.message);
|
|
235
627
|
appendLog(task.id, `[ERROR] ${err.message}`);
|
|
236
628
|
|
|
@@ -252,6 +644,7 @@ async function runDueTasks() {
|
|
|
252
644
|
// Detect actionable errors and create pending questions
|
|
253
645
|
const errMsg = err.message || '';
|
|
254
646
|
const isSlackAuth = errMsg.includes('Slack token expired') || errMsg.includes('invalid_auth') || errMsg.includes('401') || errMsg.includes('not authenticated');
|
|
647
|
+
const isCalendarDenied = isCalendarAccessDeniedError(task, errMsg);
|
|
255
648
|
|
|
256
649
|
// For Slack auth errors, try auto-refresh before pausing
|
|
257
650
|
let slackRefreshSucceeded = false;
|
|
@@ -301,6 +694,22 @@ async function runDueTasks() {
|
|
|
301
694
|
}
|
|
302
695
|
}
|
|
303
696
|
|
|
697
|
+
if (isCalendarDenied && task.type === 'recurring') {
|
|
698
|
+
await pauseBlockedRecurringTask({
|
|
699
|
+
...task,
|
|
700
|
+
error: errMsg,
|
|
701
|
+
last_run_at: new Date().toISOString(),
|
|
702
|
+
}, classifyNonRetryableTaskBlocker({
|
|
703
|
+
...task,
|
|
704
|
+
error: errMsg,
|
|
705
|
+
}), 'calendar-access-denied-pause');
|
|
706
|
+
await safeUpdateTask(task.id, {
|
|
707
|
+
run_count: (task.run_count || 0) + 1,
|
|
708
|
+
}, { bestEffort: true, reason: 'calendar-access-denied-count' });
|
|
709
|
+
appendLog(task.id, 'Paused recurring task until Calendar access is granted.');
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
304
713
|
await safeUpdateTask(task.id, {
|
|
305
714
|
status: task.type === 'recurring' ? 'pending' : 'failed',
|
|
306
715
|
error: isSlackAuth
|
|
@@ -319,12 +728,28 @@ async function runDueTasks() {
|
|
|
319
728
|
}
|
|
320
729
|
}
|
|
321
730
|
|
|
322
|
-
|
|
731
|
+
const shouldContinue = hadMore || budgetExhausted;
|
|
732
|
+
return {
|
|
733
|
+
processed,
|
|
734
|
+
reconciled,
|
|
735
|
+
considered: tasks.length,
|
|
736
|
+
fetched: fetched.length,
|
|
737
|
+
maxTasks,
|
|
738
|
+
maxTickMs,
|
|
739
|
+
taskExecutionTimeoutMs,
|
|
740
|
+
scriptTaskExecutionTimeoutMs,
|
|
741
|
+
chatTaskExecutionTimeoutMs,
|
|
742
|
+
maintenance,
|
|
743
|
+
phaseDurations,
|
|
744
|
+
budgetExhausted,
|
|
745
|
+
_continue: shouldContinue,
|
|
746
|
+
...(shouldContinue ? { _continueDelayMs: continueDelayMs } : {}),
|
|
747
|
+
};
|
|
323
748
|
}
|
|
324
749
|
|
|
325
750
|
// ── Skill execution ──
|
|
326
751
|
|
|
327
|
-
async function executeSkill(taskId, task) {
|
|
752
|
+
async function executeSkill(taskId, task, options = {}) {
|
|
328
753
|
const skill = findSkill(task.skill);
|
|
329
754
|
if (!skill) throw new Error(`Skill "${task.skill}" not found`);
|
|
330
755
|
|
|
@@ -334,6 +759,8 @@ async function executeSkill(taskId, task) {
|
|
|
334
759
|
const { runScriptSkill } = require('../skills/script-skill-runner');
|
|
335
760
|
return runScriptSkill(skill, task, {
|
|
336
761
|
log: line => appendLog(taskId, line),
|
|
762
|
+
timeoutMs: options.scriptTimeoutMs ?? options.timeoutMs,
|
|
763
|
+
killGraceMs: options.killGraceMs,
|
|
337
764
|
onCheckpoint: checkpoint => {
|
|
338
765
|
void safeUpdateTask(taskId, { checkpoint }, {
|
|
339
766
|
bestEffort: true,
|
|
@@ -355,7 +782,10 @@ async function executeSkill(taskId, task) {
|
|
|
355
782
|
description: skill.instructions || skill.description,
|
|
356
783
|
};
|
|
357
784
|
appendLog(taskId, `Agent skill: sending instructions to Claude...`);
|
|
358
|
-
return executeChat(taskId, agentTask
|
|
785
|
+
return executeChat(taskId, agentTask, {
|
|
786
|
+
...options,
|
|
787
|
+
timeoutMs: options.chatTimeoutMs ?? options.timeoutMs,
|
|
788
|
+
});
|
|
359
789
|
}
|
|
360
790
|
|
|
361
791
|
throw new Error(`Unknown skill execution mode: ${skill.execution}`);
|
|
@@ -363,7 +793,7 @@ async function executeSkill(taskId, task) {
|
|
|
363
793
|
|
|
364
794
|
// ── Script execution with live log streaming ──
|
|
365
795
|
|
|
366
|
-
function executeScript(taskId, script, checkpoint, extraEnv) {
|
|
796
|
+
function executeScript(taskId, script, checkpoint, extraEnv, options = {}) {
|
|
367
797
|
return new Promise((resolve, reject) => {
|
|
368
798
|
// Pass checkpoint as env var so scripts can resume from where they left off
|
|
369
799
|
const env = { ...process.env, HOME: process.env.HOME };
|
|
@@ -379,7 +809,37 @@ function executeScript(taskId, script, checkpoint, extraEnv) {
|
|
|
379
809
|
|
|
380
810
|
let stdout = '';
|
|
381
811
|
let stderr = '';
|
|
382
|
-
|
|
812
|
+
let timedOut = false;
|
|
813
|
+
let settled = false;
|
|
814
|
+
let timeoutTimer = null;
|
|
815
|
+
let killTimer = null;
|
|
816
|
+
const timeoutMs = nonNegativeIntValue(options.timeoutMs, 0, 240000);
|
|
817
|
+
const killGraceMs = nonNegativeIntValue(options.killGraceMs, DEFAULT_TASK_KILL_GRACE_MS, 10000);
|
|
818
|
+
const clearTimers = () => {
|
|
819
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
820
|
+
if (killTimer) clearTimeout(killTimer);
|
|
821
|
+
timeoutTimer = null;
|
|
822
|
+
killTimer = null;
|
|
823
|
+
};
|
|
824
|
+
const finish = (fn, value) => {
|
|
825
|
+
if (settled) return;
|
|
826
|
+
settled = true;
|
|
827
|
+
clearTimers();
|
|
828
|
+
taskProcesses.delete(taskId);
|
|
829
|
+
fn(value);
|
|
830
|
+
};
|
|
831
|
+
if (timeoutMs > 0) {
|
|
832
|
+
timeoutTimer = setTimeout(() => {
|
|
833
|
+
timedOut = true;
|
|
834
|
+
appendLog(taskId, `[timeout] Task exceeded ${timeoutMs}ms; stopping script process...`);
|
|
835
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
836
|
+
killTimer = setTimeout(() => {
|
|
837
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
838
|
+
}, killGraceMs);
|
|
839
|
+
killTimer.unref?.();
|
|
840
|
+
}, timeoutMs);
|
|
841
|
+
timeoutTimer.unref?.();
|
|
842
|
+
}
|
|
383
843
|
|
|
384
844
|
child.stdout.on('data', (chunk) => {
|
|
385
845
|
const text = chunk.toString();
|
|
@@ -405,28 +865,31 @@ function executeScript(taskId, script, checkpoint, extraEnv) {
|
|
|
405
865
|
});
|
|
406
866
|
|
|
407
867
|
child.on('close', (code) => {
|
|
408
|
-
|
|
868
|
+
if (timedOut) {
|
|
869
|
+
finish(reject, createTaskTimeoutError({ id: taskId, title: options.taskTitle || 'script task' }, timeoutMs));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
409
872
|
if (code !== 0 && code !== null) {
|
|
410
|
-
reject
|
|
873
|
+
finish(reject, new Error(`Exit code ${code}: ${stderr.slice(0, 500)}`));
|
|
411
874
|
} else {
|
|
412
875
|
let output = stdout.trim();
|
|
413
876
|
if (stderr.trim()) output += '\n[stderr] ' + stderr.trim();
|
|
414
|
-
resolve
|
|
877
|
+
finish(resolve, output);
|
|
415
878
|
}
|
|
416
879
|
});
|
|
417
880
|
|
|
418
881
|
child.on('error', (err) => {
|
|
419
|
-
reject
|
|
882
|
+
finish(reject, err);
|
|
420
883
|
});
|
|
421
884
|
});
|
|
422
885
|
}
|
|
423
886
|
|
|
424
887
|
// ── Chat execution with log streaming ──
|
|
425
888
|
|
|
426
|
-
async function executeChat(taskId, task) {
|
|
889
|
+
async function executeChat(taskId, task, options = {}) {
|
|
427
890
|
// Use multi-turn for complex tasks
|
|
428
891
|
if (task.execution === 'multi-turn') {
|
|
429
|
-
return executeMultiTurnChat(taskId, task);
|
|
892
|
+
return executeMultiTurnChat(taskId, task, options);
|
|
430
893
|
}
|
|
431
894
|
|
|
432
895
|
// Single-turn (existing behavior)
|
|
@@ -438,6 +901,8 @@ async function executeChat(taskId, task) {
|
|
|
438
901
|
const result = await chatModule.chat(prompt, {
|
|
439
902
|
channel: 'task',
|
|
440
903
|
session_id: `task-${task.id}`,
|
|
904
|
+
...(options.timeoutMs > 0 ? { timeoutMs: options.timeoutMs } : {}),
|
|
905
|
+
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
|
|
441
906
|
onProgress: (event) => {
|
|
442
907
|
if (event.type === 'tool_call') appendLog(taskId, event.summary);
|
|
443
908
|
else if (event.type === 'tool_done') appendLog(taskId, `Done: ${event.summary}`);
|
|
@@ -452,13 +917,19 @@ async function executeChat(taskId, task) {
|
|
|
452
917
|
* Execute a task using multiple chat turns, maintaining session state.
|
|
453
918
|
* Each turn can use tools, and progress is checkpointed between turns.
|
|
454
919
|
*/
|
|
455
|
-
async function executeMultiTurnChat(taskId, task) {
|
|
920
|
+
async function executeMultiTurnChat(taskId, task, options = {}) {
|
|
456
921
|
const chatModule = require('../chat');
|
|
457
922
|
const sessionId = `task-${task.id}`;
|
|
458
923
|
const MAX_TASK_TURNS = 5;
|
|
924
|
+
const timeoutMs = nonNegativeIntValue(options.timeoutMs, 0, 240000);
|
|
925
|
+
const deadlineMs = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
459
926
|
let lastReply = '';
|
|
460
927
|
|
|
461
928
|
for (let turn = 0; turn < MAX_TASK_TURNS; turn++) {
|
|
929
|
+
const remainingMs = deadlineMs > 0 ? deadlineMs - Date.now() : 0;
|
|
930
|
+
if (deadlineMs > 0 && remainingMs <= 0) {
|
|
931
|
+
throw createTaskTimeoutError(task, timeoutMs);
|
|
932
|
+
}
|
|
462
933
|
const prompt = turn === 0
|
|
463
934
|
? buildTaskPrompt(task)
|
|
464
935
|
: 'Continue working on this task. Review your progress so far and take the next step. If you are done, include [TASK COMPLETE] in your response.';
|
|
@@ -468,6 +939,8 @@ async function executeMultiTurnChat(taskId, task) {
|
|
|
468
939
|
const result = await chatModule.chat(prompt, {
|
|
469
940
|
channel: 'task',
|
|
470
941
|
session_id: sessionId,
|
|
942
|
+
...(remainingMs > 0 ? { timeoutMs: remainingMs } : {}),
|
|
943
|
+
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
|
|
471
944
|
onProgress: (event) => {
|
|
472
945
|
if (event.type === 'tool_call') appendLog(taskId, event.summary);
|
|
473
946
|
else if (event.type === 'tool_done') appendLog(taskId, `Done: ${event.summary}`);
|
|
@@ -762,4 +1235,27 @@ async function retrySlackDelivery(task, resultText) {
|
|
|
762
1235
|
}
|
|
763
1236
|
}
|
|
764
1237
|
|
|
765
|
-
module.exports = {
|
|
1238
|
+
module.exports = {
|
|
1239
|
+
runDueTasks,
|
|
1240
|
+
runTaskById,
|
|
1241
|
+
recoverInterruptedTasks,
|
|
1242
|
+
buildTaskPrompt,
|
|
1243
|
+
computeNextDue,
|
|
1244
|
+
getTaskLogs,
|
|
1245
|
+
clearTaskLogs,
|
|
1246
|
+
stopTask,
|
|
1247
|
+
executeSkill,
|
|
1248
|
+
executeMultiTurnChat,
|
|
1249
|
+
consolidateBriefingItems,
|
|
1250
|
+
taskLogs,
|
|
1251
|
+
_internals: {
|
|
1252
|
+
isCalendarAccessDeniedError,
|
|
1253
|
+
repairRecurringTaskSchedules,
|
|
1254
|
+
tryResumeSlackTasks,
|
|
1255
|
+
shouldRunSlackAutoResumeProbe,
|
|
1256
|
+
setSlackAutoResumeCooldown,
|
|
1257
|
+
clearSlackAutoResumeCooldown,
|
|
1258
|
+
positiveIntValue,
|
|
1259
|
+
nonNegativeIntValue,
|
|
1260
|
+
},
|
|
1261
|
+
};
|