mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-12 19:00:58 +08:00
b0c76ce4c5
Scheduled tasks move from MCP tools to a first-class ncl resource:
list / get / create / update / cancel / pause / resume / run / append-log,
with args declared on every verb (strict validation, fix-carrying errors)
and deep per-verb help with executable examples.
A series is CronJob-like: the live (pending/paused) row is the next
occurrence, completed rows are its run history. tasks list reads as a
compact run-history table, server-rendered so the container agent prints
the same aligned table. Short named ids (<slug>-<hex>) are
filesystem/thread-safe and copy-pasteable. Agents keep a per-series work
journal: the run log at tasks/<series>.md (append-log + auto-logged
final text).
Each series runs in its own isolated system session
(system:tasks:<series>); the live-task cap is dropped — isolation
replaces throttling. Spent task sessions are GC'd by the sweep.
--script on a task runs a bash gate BEFORE the agent wakes. It prints a
JSON verdict as its last stdout line: {"wakeAgent": bool, "data": {...}}.
wakeAgent:false handles the occurrence without waking the agent; data is
threaded into the fire's prompt, so a gate can fetch/inspect and hand the
agent exactly what changed. Failing scripts back off exponentially
(2·2^(n-1) min, cap 60) and auto-pause the series after 8 consecutive
failures with a host-written run-log note; manual runs are excluded from
the streak.
BREAKING: the schedule_task / list_tasks / update_task / cancel_task /
pause_task / resume_task MCP tools are removed. Agents schedule via
ncl tasks (available under cli_scope=group). Existing task rows keep
firing — storage (messages_in kind='task') is unchanged; only the
management surface moved. Migration: docs/ncl-tasks-migration.md.
Review round (gavrielc):
- Recurrence frequency guard: create/update refuse a recurrence more
frequent than 4 fires/day with a warning steering to gate scripts;
--dangerously-override-recurrence-limit bypasses after explicit user
confirmation. Counted over the next 24h in the instance TZ. Guarded on
update too, so create-slow-then-update cannot sneak past.
- ncl tasks help scripts: resource help topics (new helpTopics seam on
registerResource) carry the full gate-script guide — contract, examples,
state, failure/backoff semantics, testing directions.
- Agent-facing scheduling instructions slimmed: no recurring-frequency
prose in the fragment; one pointer to the help topic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
73 lines
2.8 KiB
TypeScript
73 lines
2.8 KiB
TypeScript
/**
|
|
* Container leg of the script-failure backoff chain, tested at unit level so
|
|
* the e2e suite doesn't need a live multi-sweep scenario for it:
|
|
*
|
|
* script error → applyPreTaskScripts skips with reason 'error'
|
|
* → markScriptSkipped acks `script-skip:error` in outbound.db
|
|
* (gated → plain 'completed': the monitor working as designed).
|
|
*
|
|
* The host leg (ack → FAILED run → streak backoff) is pinned in
|
|
* src/db/session-db.test.ts and src/modules/scheduling/recurrence.test.ts —
|
|
* both sides pin the literal 'script-skip:error'; if either renames it, its
|
|
* own test goes red.
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
|
|
import { initTestSessionDb, closeSessionDb, getInboundDb, getOutboundDb } from '../db/connection.js';
|
|
import { getPendingMessages, markScriptSkipped } from '../db/messages-in.js';
|
|
import { applyPreTaskScripts } from './task-script.js';
|
|
|
|
beforeEach(() => {
|
|
initTestSessionDb();
|
|
});
|
|
|
|
afterEach(() => {
|
|
closeSessionDb();
|
|
});
|
|
|
|
function insertTask(id: string, script: string) {
|
|
getInboundDb()
|
|
.prepare(
|
|
`INSERT INTO messages_in (id, kind, timestamp, status, trigger, content)
|
|
VALUES (?, 'task', datetime('now'), 'pending', 1, ?)`,
|
|
)
|
|
.run(id, JSON.stringify({ prompt: 'monitor', script }));
|
|
}
|
|
|
|
const ackStatus = (id: string): string | undefined =>
|
|
(getOutboundDb().prepare('SELECT status FROM processing_ack WHERE message_id = ?').get(id) as { status: string } | undefined)
|
|
?.status;
|
|
|
|
describe('script-skip ack chain (container leg)', () => {
|
|
it('an erroring script skips with reason "error" and acks script-skip:error', async () => {
|
|
insertTask('t-err', 'echo boom >&2; exit 1');
|
|
const { keep, skipped } = await applyPreTaskScripts(getPendingMessages());
|
|
|
|
expect(keep).toHaveLength(0);
|
|
expect(skipped).toEqual([{ id: 't-err', reason: 'error' }]);
|
|
|
|
markScriptSkipped(skipped);
|
|
expect(ackStatus('t-err')).toBe('script-skip:error');
|
|
});
|
|
|
|
it('a deliberate wakeAgent=false gate acks plain completed — never backs off', async () => {
|
|
insertTask('t-gated', 'echo \'{"wakeAgent": false}\'');
|
|
const { keep, skipped } = await applyPreTaskScripts(getPendingMessages());
|
|
|
|
expect(keep).toHaveLength(0);
|
|
expect(skipped).toEqual([{ id: 't-gated', reason: 'gated' }]);
|
|
|
|
markScriptSkipped(skipped);
|
|
expect(ackStatus('t-gated')).toBe('completed');
|
|
});
|
|
|
|
it('wakeAgent=true keeps the task and enriches the prompt with script data', async () => {
|
|
insertTask('t-wake', 'echo \'{"wakeAgent": true, "data": {"alerts": 2}}\'');
|
|
const { keep, skipped } = await applyPreTaskScripts(getPendingMessages());
|
|
|
|
expect(skipped).toHaveLength(0);
|
|
expect(keep).toHaveLength(1);
|
|
expect(JSON.parse(keep[0].content).scriptOutput).toEqual({ alerts: 2 });
|
|
});
|
|
});
|