Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/473-schedule-last-run-retry.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Each scheduled task in Settings → Scheduled tasks now says when it last ran and how it went (still running, finished, stopped, or failed with the reason). A run fails only when DSH could not deliver it or the Bot's turn errored; a failed run gets **Retry**, which runs the task once now with the same prompt, and the failure also shows up in notifications ([#473](https://github.com/ThinkFlowLab/FlyBot/issues/473)).
43 changes: 42 additions & 1 deletion core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,13 @@ var ZH = {
"Every weekend at {time}": "每个周末 {time}",
"On the schedule {expression}": "按计划 {expression}",
"Next: {time}": "下次 {time}",
"Running since {time}": "运行中,{time} 开始",
"Last run {time}": "上次运行 {time}",
"Last run {time} · Stopped": "上次运行 {time} · 已停止",
"Last run {time} · Failed: {reason}": "上次运行 {time} · 失败:{reason}",
"Run the task once now": "现在运行一次这个任务",
"DSH could not deliver it": "DSH 没能送达",
"Resume the task first": "请先继续这个任务",
Finished: "已结束",
Resume: "继续",
"in {name}": "在 {name}",
Expand Down Expand Up @@ -9541,6 +9548,20 @@ function timingText(task) {
}
}
var nextText = (task) => task.status === "paused" ? t("Paused") : task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished");
function lastRunText(run) {
if (run === void 0 || !Number.isFinite(run.at)) return "";
const time = dayAndTime(run.at);
switch (run.result) {
case "running":
return t("Running since {time}", { time });
case "stopped":
return t("Last run {time} · Stopped", { time });
case "failed":
return t("Last run {time} · Failed: {reason}", { time, reason: hostText(run.reason || "Unknown error") });
default:
return t("Last run {time}", { time });
}
}
function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
const [confirming, setConfirming] = (0, import_react19.useState)(false);
const [busy, setBusy] = (0, import_react19.useState)(false);
Expand All @@ -9565,8 +9586,22 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
onView(next);
}, () => setBusy(false));
};
const [retryError, setRetryError] = (0, import_react19.useState)("");
const retry = () => {
setBusy(true);
setRetryError("");
void actions.scheduleRetry(task.sessionId, task.id).then((next) => {
setBusy(false);
onView(next);
}, (error) => {
setBusy(false);
setRetryError(hostText(error?.message ?? String(error)));
});
};
const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0;
const stoppable = task.status === "active" || task.status === "paused";
const failed = task.lastRun?.result === "failed";
const last = lastRunText(task.lastRun);
return (0, import_react19.createElement)(
"div",
{ className: "bt-set-row bt-task", "data-done": stoppable ? void 0 : "", "data-paused": task.status === "paused" || void 0, "data-busy": busy || void 0 },
Expand All @@ -9576,9 +9611,12 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
{ className: "bt-set-copy" },
(0, import_react19.createElement)("span", { className: "bt-task-title" }, task.title),
(0, import_react19.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")),
task.prompt && task.prompt !== task.title ? (0, import_react19.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null
task.prompt && task.prompt !== task.title ? (0, import_react19.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null,
last ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": failed || void 0 }, last) : null,
retryError ? (0, import_react19.createElement)("span", { className: "bt-task-last", "data-failed": "", role: "alert" }, retryError) : null
),
(0, import_react19.createElement)("span", { className: "bt-task-next" }, nextText(task)),
failed && task.status !== "paused" && actions.scheduleRetry ? (0, import_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, title: t("Run the task once now"), onClick: retry }, t("Retry")) : null,
onEdit ? (0, import_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-tool", disabled: busy, "aria-label": t("Edit"), title: t("Edit"), onClick: () => onEdit(task) }, (0, import_react19.createElement)(PencilIcon)) : null,
stoppable ? (0, import_react19.createElement)("button", { type: "button", className: "bt-mini bt-task-pause", disabled: busy, onClick: toggle }, task.status === "paused" ? t("Resume") : t("Pause")) : null,
(0, import_react19.createElement)("button", {
Expand Down Expand Up @@ -9846,6 +9884,8 @@ var SCHEDULE_CSS = `
.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)}
.bt-task-tool svg{width:14px;height:14px}
.bt-task-pause{height:26px;color:var(--bt-ink-2)}
.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere}
.bt-task-last[data-failed]{color:#c21d2e}
.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)}
.bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135}
.bt-task-head{display:flex;align-items:center;gap:6px}
Expand Down Expand Up @@ -17466,6 +17506,7 @@ function apply(ctx) {
scheduleUpdate: (task) => call("schedule-update", task),
schedulePause: (sessionId, id) => call("schedule-pause", { sessionId, id }),
scheduleResume: (sessionId, id) => call("schedule-resume", { sessionId, id }),
scheduleRetry: (sessionId, id) => call("schedule-retry", { sessionId, id }),
scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }),
exchange: (a, b) => call("exchange", { a, b }).catch(() => []),
roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []),
Expand Down
7 changes: 7 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,13 @@ const ZH = {
'Every weekend at {time}': '每个周末 {time}',
'On the schedule {expression}': '按计划 {expression}',
'Next: {time}': '下次 {time}',
'Running since {time}': '运行中,{time} 开始',
'Last run {time}': '上次运行 {time}',
'Last run {time} · Stopped': '上次运行 {time} · 已停止',
'Last run {time} · Failed: {reason}': '上次运行 {time} · 失败:{reason}',
'Run the task once now': '现在运行一次这个任务',
'DSH could not deliver it': 'DSH 没能送达',
'Resume the task first': '请先继续这个任务',
Finished: '已结束',
Resume: '继续',
'in {name}': '在 {name}',
Expand Down
1 change: 1 addition & 0 deletions core/src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ export function apply(ctx) {
scheduleUpdate: task => call('schedule-update', task),
schedulePause: (sessionId, id) => call('schedule-pause', { sessionId, id }),
scheduleResume: (sessionId, id) => call('schedule-resume', { sessionId, id }),
scheduleRetry: (sessionId, id) => call('schedule-retry', { sessionId, id }),
scheduleDelete: (sessionId, id) => call('schedule-delete', { sessionId, id }),
exchange: (a, b) => call('exchange', { a, b }).catch(() => []),
roomProgress: roomId => call('room-progress', { roomId }).catch(() => []),
Expand Down
30 changes: 28 additions & 2 deletions core/src/client/schedules.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Scheduled tasks: Settings → Scheduled tasks, a Bot's card in its settings, and the
// line in its details panel. DSH runs the tasks (see src/host/schedules.js); a task
// sends its prompt to the Bot's chat when it is due. A row edits its task in the same
// form that creates one, and pauses or resumes it (#354).
// form that creates one, and pauses or resumes it (#354). Each row says how its last run
// went, and a failed run has Retry, which runs the task once now (#473).
import { createElement as h, Fragment, useEffect, useState } from 'react'
import { BotAvatar, RoomAvatar } from './mark.js'
import { ClockIcon, PencilIcon, PlusIcon, TrashIcon } from './icons.js'
Expand Down Expand Up @@ -73,6 +74,18 @@ export function timingText(task) {
const nextText = task => (task.status === 'paused' ? t('Paused')
: task.status === 'active' ? t('Next: {time}', { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t('Finished'))

// The last run, in one line: when, and how it went (src/host/schedules.js).
export function lastRunText(run) {
if (run === undefined || !Number.isFinite(run.at)) return ''
const time = dayAndTime(run.at)
switch (run.result) {
case 'running': return t('Running since {time}', { time })
case 'stopped': return t('Last run {time} · Stopped', { time })
case 'failed': return t('Last run {time} · Failed: {reason}', { time, reason: hostText(run.reason || 'Unknown error') })
default: return t('Last run {time}', { time })
}
}

function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
const [confirming, setConfirming] = useState(false)
const [busy, setBusy] = useState(false)
Expand All @@ -91,15 +104,26 @@ function TaskRow({ task, actions, onView, onEdit, showBot, roster }) {
const step = task.status === 'paused' ? actions.scheduleResume : actions.schedulePause
void step(task.sessionId, task.id).then((next) => { setBusy(false); onView(next) }, () => setBusy(false))
}
const [retryError, setRetryError] = useState('')
const retry = () => {
setBusy(true)
setRetryError('')
void actions.scheduleRetry(task.sessionId, task.id).then((next) => { setBusy(false); onView(next) }, (error) => { setBusy(false); setRetryError(hostText(error?.message ?? String(error))) })
}
const room = task.roomId ? roster.roomsById[task.roomId] : undefined
const stoppable = task.status === 'active' || task.status === 'paused'
const failed = task.lastRun?.result === 'failed'
const last = lastRunText(task.lastRun)
return h('div', { className: 'bt-set-row bt-task', 'data-done': stoppable ? undefined : '', 'data-paused': task.status === 'paused' || undefined, 'data-busy': busy || undefined },
showBot ? h(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null,
h('span', { className: 'bt-set-copy' },
h('span', { className: 'bt-task-title' }, task.title),
h('span', { className: 'bt-set-hint' }, [timingText(task), room ? t('in {name}', { name: room.name }) : ''].filter(Boolean).join(' · ')),
task.prompt && task.prompt !== task.title ? h('span', { className: 'bt-task-prompt' }, task.prompt) : null),
task.prompt && task.prompt !== task.title ? h('span', { className: 'bt-task-prompt' }, task.prompt) : null,
last ? h('span', { className: 'bt-task-last', 'data-failed': failed || undefined }, last) : null,
retryError ? h('span', { className: 'bt-task-last', 'data-failed': '', role: 'alert' }, retryError) : null),
h('span', { className: 'bt-task-next' }, nextText(task)),
failed && task.status !== 'paused' && actions.scheduleRetry ? h('button', { type: 'button', className: 'bt-mini bt-task-pause', disabled: busy, title: t('Run the task once now'), onClick: retry }, t('Retry')) : null,
onEdit ? h('button', { type: 'button', className: 'bt-mini bt-task-tool', disabled: busy, 'aria-label': t('Edit'), title: t('Edit'), onClick: () => onEdit(task) }, h(PencilIcon)) : null,
stoppable ? h('button', { type: 'button', className: 'bt-mini bt-task-pause', disabled: busy, onClick: toggle }, task.status === 'paused' ? t('Resume') : t('Pause')) : null,
h('button', {
Expand Down Expand Up @@ -303,6 +327,8 @@ export const SCHEDULE_CSS = `
.bt-task-tool{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)}
.bt-task-tool svg{width:14px;height:14px}
.bt-task-pause{height:26px;color:var(--bt-ink-2)}
.bt-task-last{font-size:12px;line-height:16px;color:var(--bt-ink-3);overflow-wrap:anywhere}
.bt-task-last[data-failed]{color:#c21d2e}
.bt-task[data-paused] .bt-task-title,.bt-task[data-paused] .bt-task-prompt{color:var(--bt-ink-3)}
.bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135}
.bt-task-head{display:flex;align-items:center;gap:6px}
Expand Down
2 changes: 1 addition & 1 deletion core/src/host/authz.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const SHARED_BLOCKED = new Set([
const BOT_TARGETED = new Set([
'update-bot', 'delete-bot', 'duplicate-bot', 'set-flags', 'bot-capability',
'memory-view', 'memory-ask', 'memory-forget',
'schedules', 'schedule-create', 'schedule-update', 'schedule-pause', 'schedule-resume', 'schedule-delete',
'schedules', 'schedule-create', 'schedule-update', 'schedule-pause', 'schedule-resume', 'schedule-retry', 'schedule-delete',
'agent-session', 'agent-adopt', 'agent-archive', 'agent-rename', 'remove-agent',
'stop-member-work', 'set-main', 'test-model',
])
Expand Down
1 change: 1 addition & 0 deletions core/src/host/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export function install(rt) {
case 'schedules':
case 'schedule-pause':
case 'schedule-resume':
case 'schedule-retry':
case 'schedule-delete':
await rt.load()
return ok(await rt.scheduleEndpoint(endpoint, body))
Expand Down
2 changes: 2 additions & 0 deletions core/src/host/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,8 @@ export function install(rt) {
rt.roomProgress.set(exec.callId, round)
try {
await runRound(room, round, exec.signal, exec.agent)
// A scheduled task's run in this group fails with its members' failed turns (#473).
if (round.failed.length > 0) rt.scheduleRoundFailed?.(room.id, round.failed)
} catch (error) {
rt.warn('ds-bot: group round in %s stopped: %s', room.name, error)
} finally {
Expand Down
Loading
Loading