Fix scheduled tasks and improve task scheduling UX
- Fix Apple Container mount issue: move groups/CLAUDE.md to groups/global/ directory (Apple Container only supports directory mounts, not file mounts) - Fix scheduled tasks for main group: properly detect isMain based on group_folder instead of always setting false - Add isScheduledTask flag so agent knows when running as scheduled task - Improve schedule_task tool description with clear format examples for cron, interval, and once schedule types - Update global CLAUDE.md with instructions for scheduled tasks to use mcp__nanoclaw__send_message when needed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2
SPEC.md
2
SPEC.md
@@ -53,7 +53,7 @@ A personal Claude assistant accessible via WhatsApp, with persistent memory per
|
|||||||
│ │ Working directory: /workspace/group (mounted from host) │ │
|
│ │ Working directory: /workspace/group (mounted from host) │ │
|
||||||
│ │ Volume mounts: │ │
|
│ │ Volume mounts: │ │
|
||||||
│ │ • groups/{name}/ → /workspace/group │ │
|
│ │ • groups/{name}/ → /workspace/group │ │
|
||||||
│ │ • groups/CLAUDE.md → /workspace/global/CLAUDE.md │ │
|
│ │ • groups/global/ → /workspace/global/ (non-main only) │ │
|
||||||
│ │ • ~/.claude/ → /home/node/.claude/ (sessions) │ │
|
│ │ • ~/.claude/ → /home/node/.claude/ (sessions) │ │
|
||||||
│ │ • Additional dirs → /workspace/extra/* │ │
|
│ │ • Additional dirs → /workspace/extra/* │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ContainerInput {
|
|||||||
groupFolder: string;
|
groupFolder: string;
|
||||||
chatJid: string;
|
chatJid: string;
|
||||||
isMain: boolean;
|
isMain: boolean;
|
||||||
|
isScheduledTask?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContainerOutput {
|
interface ContainerOutput {
|
||||||
@@ -219,11 +220,17 @@ async function main(): Promise<void> {
|
|||||||
let result: string | null = null;
|
let result: string | null = null;
|
||||||
let newSessionId: string | undefined;
|
let newSessionId: string | undefined;
|
||||||
|
|
||||||
|
// Add context for scheduled tasks
|
||||||
|
let prompt = input.prompt;
|
||||||
|
if (input.isScheduledTask) {
|
||||||
|
prompt = `[SCHEDULED TASK - You are running automatically, not in response to a user message. Use mcp__nanoclaw__send_message if needed to communicate with the user.]\n\n${input.prompt}`;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log('Starting agent...');
|
log('Starting agent...');
|
||||||
|
|
||||||
for await (const message of query({
|
for await (const message of query({
|
||||||
prompt: input.prompt,
|
prompt,
|
||||||
options: {
|
options: {
|
||||||
cwd: '/workspace/group',
|
cwd: '/workspace/group',
|
||||||
resume: input.sessionId,
|
resume: input.sessionId,
|
||||||
|
|||||||
@@ -67,11 +67,16 @@ export function createIpcMcp(ctx: IpcMcpContext) {
|
|||||||
|
|
||||||
tool(
|
tool(
|
||||||
'schedule_task',
|
'schedule_task',
|
||||||
'Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.',
|
`Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.
|
||||||
|
|
||||||
|
IMPORTANT - schedule_value format depends on schedule_type:
|
||||||
|
• cron: Standard cron expression (e.g., "*/5 * * * *" for every 5 minutes, "0 9 * * *" for daily at 9am)
|
||||||
|
• interval: Milliseconds between runs (e.g., "300000" for 5 minutes, "3600000" for 1 hour)
|
||||||
|
• once: ISO 8601 timestamp (e.g., "2026-02-01T15:30:00.000Z"). Calculate this from current time.`,
|
||||||
{
|
{
|
||||||
prompt: z.string().describe('What the agent should do when the task runs'),
|
prompt: z.string().describe('What the agent should do when the task runs'),
|
||||||
schedule_type: z.enum(['cron', 'interval', 'once']).describe('Type of schedule'),
|
schedule_type: z.enum(['cron', 'interval', 'once']).describe('cron=recurring at specific times, interval=recurring every N ms, once=run once at specific time'),
|
||||||
schedule_value: z.string().describe('Cron expression, interval in ms, or ISO timestamp'),
|
schedule_value: z.string().describe('cron: "*/5 * * * *" | interval: milliseconds like "300000" | once: ISO timestamp like "2026-02-01T15:30:00.000Z"'),
|
||||||
target_group: z.string().optional().describe('Target group folder (main only, defaults to current group)')
|
target_group: z.string().optional().describe('Target group folder (main only, defaults to current group)')
|
||||||
},
|
},
|
||||||
async (args) => {
|
async (args) => {
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ If a request requires significant work (research, multiple steps, file operation
|
|||||||
|
|
||||||
This keeps users informed instead of waiting in silence.
|
This keeps users informed instead of waiting in silence.
|
||||||
|
|
||||||
|
## Scheduled Tasks
|
||||||
|
|
||||||
|
When you run as a scheduled task (no direct user message), use `mcp__nanoclaw__send_message` if needed to communicate with the user. Your return value is only logged internally - it won't be sent to the user.
|
||||||
|
|
||||||
|
Example: If your task is "Share the weather forecast", you should:
|
||||||
|
1. Get the weather data
|
||||||
|
2. Call `mcp__nanoclaw__send_message` with the formatted forecast
|
||||||
|
3. Return a brief summary for the logs
|
||||||
|
|
||||||
## Your Workspace
|
## Your Workspace
|
||||||
|
|
||||||
Files you create are saved in `/workspace/group/`. Use this for notes, research, or anything that should persist.
|
Files you create are saved in `/workspace/group/`. Use this for notes, research, or anything that should persist.
|
||||||
@@ -147,7 +147,7 @@ Read `/workspace/project/data/registered_groups.json` and format it nicely.
|
|||||||
|
|
||||||
## Global Memory
|
## Global Memory
|
||||||
|
|
||||||
You can read and write to `/workspace/project/groups/CLAUDE.md` for facts that should apply to all groups. Only update global memory when explicitly asked to "remember this globally" or similar.
|
You can read and write to `/workspace/project/groups/global/CLAUDE.md` for facts that should apply to all groups. Only update global memory when explicitly asked to "remember this globally" or similar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface ContainerInput {
|
|||||||
groupFolder: string;
|
groupFolder: string;
|
||||||
chatJid: string;
|
chatJid: string;
|
||||||
isMain: boolean;
|
isMain: boolean;
|
||||||
|
isScheduledTask?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerOutput {
|
export interface ContainerOutput {
|
||||||
@@ -68,12 +69,13 @@ function buildVolumeMounts(group: RegisteredGroup, isMain: boolean): VolumeMount
|
|||||||
readonly: false
|
readonly: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global CLAUDE.md (read-only for non-main)
|
// Global memory directory (read-only for non-main)
|
||||||
const globalClaudeMd = path.join(GROUPS_DIR, 'CLAUDE.md');
|
// Apple Container only supports directory mounts, not file mounts
|
||||||
if (fs.existsSync(globalClaudeMd)) {
|
const globalDir = path.join(GROUPS_DIR, 'global');
|
||||||
|
if (fs.existsSync(globalDir)) {
|
||||||
mounts.push({
|
mounts.push({
|
||||||
hostPath: globalClaudeMd,
|
hostPath: globalDir,
|
||||||
containerPath: '/workspace/global/CLAUDE.md',
|
containerPath: '/workspace/global',
|
||||||
readonly: true
|
readonly: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import pino from 'pino';
|
|||||||
import { CronExpressionParser } from 'cron-parser';
|
import { CronExpressionParser } from 'cron-parser';
|
||||||
import { getDueTasks, updateTaskAfterRun, logTaskRun, getTaskById, getAllTasks } from './db.js';
|
import { getDueTasks, updateTaskAfterRun, logTaskRun, getTaskById, getAllTasks } from './db.js';
|
||||||
import { ScheduledTask, RegisteredGroup } from './types.js';
|
import { ScheduledTask, RegisteredGroup } from './types.js';
|
||||||
import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR } from './config.js';
|
import { GROUPS_DIR, SCHEDULER_POLL_INTERVAL, DATA_DIR, MAIN_GROUP_FOLDER } from './config.js';
|
||||||
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
|
import { runContainerAgent, writeTasksSnapshot } from './container-runner.js';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
@@ -56,11 +56,13 @@ async function runTask(task: ScheduledTask, deps: SchedulerDependencies): Promis
|
|||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const isMain = task.group_folder === MAIN_GROUP_FOLDER;
|
||||||
const output = await runContainerAgent(group, {
|
const output = await runContainerAgent(group, {
|
||||||
prompt: task.prompt,
|
prompt: task.prompt,
|
||||||
groupFolder: task.group_folder,
|
groupFolder: task.group_folder,
|
||||||
chatJid: task.chat_jid,
|
chatJid: task.chat_jid,
|
||||||
isMain: false // Scheduled tasks run in their group's context
|
isMain,
|
||||||
|
isScheduledTask: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (output.status === 'error') {
|
if (output.status === 'error') {
|
||||||
|
|||||||
Reference in New Issue
Block a user