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>
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
/**
|
|
* Compact aligned-table renderer for `ncl tasks list` (human mode only).
|
|
*
|
|
* A task series is a recurring job; each fire is a run. This surfaces the run history
|
|
* the raw row hides — run count, last/next fire, schedule — as an aligned table.
|
|
* Driven by the enriched rows from listTasks (tasks.ts); the --json path is
|
|
* untouched. `now` is injectable so the relative times are testable.
|
|
*/
|
|
|
|
interface TaskListRow {
|
|
series_id: string;
|
|
schedule?: string | null;
|
|
runs?: number;
|
|
failed_runs?: number;
|
|
last_run?: string | null;
|
|
next_run?: string | null;
|
|
status?: string;
|
|
log?: string | null;
|
|
created_at?: string | null;
|
|
prompt?: string | null;
|
|
}
|
|
|
|
const COLS = ['SERIES', 'SCHEDULE', 'RUNS', 'FAILED', 'LAST RUN', 'NEXT RUN', 'STATUS', 'AGE', 'PROMPT'] as const;
|
|
|
|
function parseMs(iso: string): number {
|
|
return Date.parse(/[Z+]|[+-]\d\d:\d\d$/.test(iso) ? iso : iso + 'Z');
|
|
}
|
|
|
|
/** "1m ago" / "in 30s" — coarse, human relative time. */
|
|
function duration(ms: number): string {
|
|
const s = Math.abs(ms) / 1000;
|
|
if (s < 60) return `${Math.round(s)}s`;
|
|
if (s < 3600) return `${Math.round(s / 60)}m`;
|
|
if (s < 86400) return `${Math.round(s / 3600)}h`;
|
|
return `${Math.round(s / 86400)}d`;
|
|
}
|
|
|
|
function lastRun(iso: string | null | undefined, now: number): string {
|
|
if (!iso) return '-';
|
|
const t = parseMs(iso);
|
|
if (Number.isNaN(t)) return iso;
|
|
return `${duration(now - t)} ago`;
|
|
}
|
|
|
|
function nextRun(iso: string | null | undefined, now: number): string {
|
|
if (!iso) return '-';
|
|
const t = parseMs(iso);
|
|
if (Number.isNaN(t)) return iso;
|
|
return t <= now ? 'due' : `in ${duration(t - now)}`;
|
|
}
|
|
|
|
/** AGE — how long since the series was created. */
|
|
function age(iso: string | null | undefined, now: number): string {
|
|
if (!iso) return '-';
|
|
const t = parseMs(iso);
|
|
return Number.isNaN(t) ? '-' : duration(now - t);
|
|
}
|
|
|
|
function clip(s: string | null | undefined, n: number): string {
|
|
const v = (s ?? '').replace(/\s+/g, ' ').trim();
|
|
return v.length > n ? v.slice(0, n - 1) + '…' : v;
|
|
}
|
|
|
|
export function formatTasksTable(rows: TaskListRow[], now: number = Date.now()): string {
|
|
if (!rows.length) return 'No tasks.';
|
|
const body = rows.map((r) => [
|
|
r.series_id, // full id, copy-pasteable into `ncl tasks get --id <…>`
|
|
r.schedule || 'once',
|
|
String(r.runs ?? 0),
|
|
String(r.failed_runs ?? 0),
|
|
lastRun(r.last_run, now),
|
|
nextRun(r.next_run, now),
|
|
r.status ?? '-',
|
|
age(r.created_at, now),
|
|
clip(r.prompt, 40),
|
|
]);
|
|
const widths = COLS.map((c, i) => Math.max(c.length, ...body.map((row) => row[i].length)));
|
|
const line = (cells: string[]) =>
|
|
cells
|
|
.map((c, i) => c.padEnd(widths[i]))
|
|
.join(' ')
|
|
.trimEnd();
|
|
return [line([...COLS]), ...body.map(line)].join('\n');
|
|
}
|