diff --git a/README.md b/README.md
index 3f980225..b849c20e 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,7 @@
## 📢 News
+- **2026-06-16** ⚠️ Breaking change: agent-turn automation jobs now require complete target chat or channel metadata. Cron jobs created by older versions without that metadata should be recreated from the target chat or channel; existing cron records remain in the cron store.
- **2026-06-01** 🚀 Released **v0.2.1** — **The Workbench Release** turns the packaged WebUI into a daily agent workbench: clearer Thought/response timelines, live file-edit activity, project workspaces, model and context controls, steadier sustained goals, CLI Apps + MCP extensions, and broader provider/channel support. Please see [release notes](https://github.com/HKUDS/nanobot/releases/tag/v0.2.1) for details.
- **2026-05-30** 🔐 Safer Matrix verification, bounded media downloads, clearer WebUI model timeline.
- **2026-05-29** 🧩 Extension registry, context-window tuning, document extraction controls.
diff --git a/nanobot/webui/session_automations.py b/nanobot/webui/session_automations.py
index 0780d781..0680dde7 100644
--- a/nanobot/webui/session_automations.py
+++ b/nanobot/webui/session_automations.py
@@ -122,16 +122,6 @@ def _serialize_job(
payload["created_at_ms"] = job.created_at_ms
payload["updated_at_ms"] = job.updated_at_ms
payload["payload"].update({"kind": job.payload.kind})
- if _expose_origin_identifiers(job):
- payload["payload"].update(
- {
- "session_key": job.payload.session_key,
- "origin_channel": job.payload.origin_channel,
- "origin_chat_id": job.payload.origin_chat_id,
- }
- )
- elif job.payload.origin_channel:
- payload["payload"]["origin_channel"] = job.payload.origin_channel
payload["state"].update(
{
"last_run_at_ms": job.state.last_run_at_ms,
@@ -183,12 +173,6 @@ def _origin_payload(
"preview": preview,
}
-
-def _expose_origin_identifiers(job: CronJob) -> bool:
- channel = job.payload.origin_channel
- return not channel or channel == "websocket"
-
-
def _session_preview(messages: Any) -> str:
if not isinstance(messages, list):
return ""
diff --git a/tests/channels/test_websocket_http_routes.py b/tests/channels/test_websocket_http_routes.py
index 885cc37a..4451f486 100644
--- a/tests/channels/test_websocket_http_routes.py
+++ b/tests/channels/test_websocket_http_routes.py
@@ -842,7 +842,7 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
origin_channel="websocket",
origin_chat_id="abc",
)
- legacy_job = cron.add_job(
+ incomplete_job = cron.add_job(
name="english-quiz",
schedule=CronSchedule(kind="every", every_ms=3_600_000),
message="Practice English",
@@ -899,6 +899,7 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
)
assert resp.status_code == 200
assert "wx-chat" not in resp.text
+ assert "unified:default" not in resp.text
body = resp.json()
by_id = {job["id"]: job for job in body["jobs"]}
assert by_id[user_job.id]["protected"] is False
@@ -906,10 +907,12 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
assert by_id[user_job.id]["state"]["run_history"] == []
assert by_id[user_job.id]["origin"]["session_key"] == "websocket:abc"
assert by_id[user_job.id]["origin"]["preview"] == "hi"
- assert by_id[legacy_job.id]["payload"]["session_key"] == "unified:default"
- assert by_id[legacy_job.id]["origin"] is None
- assert by_id[external_job.id]["payload"]["origin_channel"] == "weixin"
+ assert "session_key" not in by_id[incomplete_job.id]["payload"]
+ assert "origin_channel" not in by_id[incomplete_job.id]["payload"]
+ assert "origin_chat_id" not in by_id[incomplete_job.id]["payload"]
+ assert by_id[incomplete_job.id]["origin"] is None
assert "session_key" not in by_id[external_job.id]["payload"]
+ assert "origin_channel" not in by_id[external_job.id]["payload"]
assert "origin_chat_id" not in by_id[external_job.id]["payload"]
assert by_id[external_job.id]["origin"]["channel"] == "weixin"
assert "session_key" not in by_id[external_job.id]["origin"]
diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx
index c6efe0d0..71c5b58b 100644
--- a/webui/src/components/settings/SettingsView.tsx
+++ b/webui/src/components/settings/SettingsView.tsx
@@ -3698,7 +3698,6 @@ function AutomationDetailPanel({
const originHref = job.origin?.channel === "websocket" && job.origin.session_key
? `#/chat/${encodeURIComponent(job.origin.session_key)}`
: null;
- const needsRecreation = automationNeedsRecreation(job);
const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null;
const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null;
const message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
@@ -3787,15 +3786,6 @@ function AutomationDetailPanel({
- {needsRecreation ? (
-
- {tx(
- "settings.automations.legacyWarning",
- "This older automation is missing its target chat. Recreate it from the chat or channel where it should run.",
- )}
-
- ) : null}
-
{job.state.last_error ? (
{job.state.last_error}
@@ -4227,19 +4217,14 @@ function AutomationDeleteDialog({
);
}
-function automationNeedsRecreation(job: SessionAutomationJob): boolean {
- return !job.protected && !job.origin && job.payload.kind === "agent_turn";
-}
-
function automationNeedsAttention(job: SessionAutomationJob): boolean {
- return automationNeedsRecreation(job) || job.state.last_status === "error";
+ return job.state.last_status === "error";
}
function automationStatusKey(
job: SessionAutomationJob,
-): "active" | "running" | "paused" | "failed" | "system" | "needs_setup" | "completed" | "idle" {
+): "active" | "running" | "paused" | "failed" | "system" | "completed" | "idle" {
if (job.protected) return "system";
- if (automationNeedsRecreation(job)) return "needs_setup";
if (job.state.pending) return "running";
if (!job.enabled) return "paused";
if (job.state.last_status === "error") return "failed";
@@ -4412,9 +4397,6 @@ function automationStatus(
): { label: string; tone: "neutral" | "success" | "warning" } {
const status = automationStatusKey(job);
if (status === "system") return { label: tx("settings.automations.status.system", "System"), tone: "neutral" };
- if (status === "needs_setup") {
- return { label: tx("settings.automations.status.needsSetup", "Needs setup"), tone: "warning" };
- }
if (status === "running") {
return { label: tx("settings.automations.status.running", "Running now"), tone: "warning" };
}
@@ -4437,9 +4419,6 @@ function automationOriginLabel(
): string {
if (job.protected) return tx("settings.automations.origin.system", "System");
const origin = job.origin;
- if (!origin && job.payload.kind === "agent_turn") {
- return tx("settings.automations.origin.legacy", "Recreate in target chat");
- }
if (!origin) return tx("settings.automations.origin.unknown", "No linked chat");
if (origin.channel !== "websocket") return automationChannelLabel(origin.channel, tx);
return origin.title || origin.preview || origin.session_key || automationChannelLabel(origin.channel, tx);
@@ -4574,7 +4553,7 @@ function formatAutomationNextTitle(
function automationStatusDotClass(job: SessionAutomationJob): string {
const status = automationStatusKey(job);
if (status === "active" || status === "running") return "bg-emerald-500";
- if (status === "failed" || status === "needs_setup") return "bg-amber-500";
+ if (status === "failed") return "bg-amber-500";
if (status === "system") return "bg-blue-500";
return "bg-muted-foreground/45";
}
diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json
index aaa35a21..372173b6 100644
--- a/webui/src/i18n/locales/en/common.json
+++ b/webui/src/i18n/locales/en/common.json
@@ -525,18 +525,15 @@
"system": "System",
"pending": "Pending",
"running": "Running now",
- "needsSetup": "Needs setup",
"paused": "Paused",
"failed": "Failed",
"completed": "Completed",
"noSchedule": "No schedule",
"active": "Active"
},
- "legacyWarning": "This older automation is missing its target chat. Recreate it from the chat or channel where it should run.",
"origin": {
"system": "System",
- "unknown": "No linked chat",
- "legacy": "Recreate in target chat"
+ "unknown": "No linked chat"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json
index aba33564..cc27c365 100644
--- a/webui/src/i18n/locales/es/common.json
+++ b/webui/src/i18n/locales/es/common.json
@@ -525,18 +525,15 @@
"system": "Sistema",
"pending": "Pendiente",
"running": "Ejecutándose ahora",
- "needsSetup": "Requiere configuración",
"paused": "Pausada",
"failed": "Fallida",
"completed": "Completada",
"noSchedule": "Sin programación",
"active": "Activa"
},
- "legacyWarning": "Esta automatización antigua no tiene chat de destino. Vuelve a crearla desde el chat o canal donde debe ejecutarse.",
"origin": {
"system": "Sistema",
- "unknown": "Sin chat vinculado",
- "legacy": "Recréala en el chat de destino"
+ "unknown": "Sin chat vinculado"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json
index 5ce9b6dc..9e2c4268 100644
--- a/webui/src/i18n/locales/fr/common.json
+++ b/webui/src/i18n/locales/fr/common.json
@@ -525,18 +525,15 @@
"system": "Système",
"pending": "En attente",
"running": "En cours d’exécution",
- "needsSetup": "Configuration requise",
"paused": "En pause",
"failed": "Échouée",
"completed": "Terminée",
"noSchedule": "Aucun planning",
"active": "En cours"
},
- "legacyWarning": "Cette ancienne automatisation n’a pas de discussion cible. Recréez-la depuis la discussion ou le canal où elle doit s’exécuter.",
"origin": {
"system": "Système",
- "unknown": "Aucune discussion liée",
- "legacy": "Recréez-la dans la discussion cible"
+ "unknown": "Aucune discussion liée"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json
index adab95db..6885d3a1 100644
--- a/webui/src/i18n/locales/id/common.json
+++ b/webui/src/i18n/locales/id/common.json
@@ -525,18 +525,15 @@
"system": "Sistem",
"pending": "Menunggu",
"running": "Sedang berjalan",
- "needsSetup": "Perlu disiapkan",
"paused": "Dijeda",
"failed": "Gagal",
"completed": "Selesai",
"noSchedule": "Tanpa jadwal",
"active": "Aktif"
},
- "legacyWarning": "Otomasi lama ini tidak memiliki chat tujuan. Buat ulang dari chat atau channel tempat otomasi akan berjalan.",
"origin": {
"system": "Sistem",
- "unknown": "Tidak ada chat tertaut",
- "legacy": "Buat ulang di chat tujuan"
+ "unknown": "Tidak ada chat tertaut"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json
index 632b4f42..e5be1c4d 100644
--- a/webui/src/i18n/locales/ja/common.json
+++ b/webui/src/i18n/locales/ja/common.json
@@ -525,18 +525,15 @@
"system": "システム",
"pending": "待機中",
"running": "実行中",
- "needsSetup": "再設定が必要",
"paused": "一時停止",
"failed": "失敗",
"completed": "完了",
"noSchedule": "スケジュールなし",
"active": "実行中"
},
- "legacyWarning": "この古い自動タスクには対象チャットがありません。実行先のチャットまたは外部 channel から作り直してください。",
"origin": {
"system": "システム",
- "unknown": "関連チャットなし",
- "legacy": "対象チャットで作り直してください"
+ "unknown": "関連チャットなし"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json
index 15e1611b..e8741c83 100644
--- a/webui/src/i18n/locales/ko/common.json
+++ b/webui/src/i18n/locales/ko/common.json
@@ -525,18 +525,15 @@
"system": "시스템",
"pending": "대기 중",
"running": "실행 중",
- "needsSetup": "설정 필요",
"paused": "일시 중지",
"failed": "실패",
"completed": "완료",
"noSchedule": "일정 없음",
"active": "활성"
},
- "legacyWarning": "이전 자동화에 대상 채팅이 없습니다. 실행되어야 하는 채팅 또는 외부 channel에서 다시 만드세요.",
"origin": {
"system": "시스템",
- "unknown": "연결된 채팅 없음",
- "legacy": "대상 채팅에서 다시 만드세요"
+ "unknown": "연결된 채팅 없음"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json
index 2e549e1f..4132f093 100644
--- a/webui/src/i18n/locales/vi/common.json
+++ b/webui/src/i18n/locales/vi/common.json
@@ -525,18 +525,15 @@
"system": "Hệ thống",
"pending": "Đang chờ",
"running": "Đang chạy",
- "needsSetup": "Cần thiết lập",
"paused": "Đã tạm dừng",
"failed": "Thất bại",
"completed": "Hoàn tất",
"noSchedule": "Không có lịch",
"active": "Đang chạy"
},
- "legacyWarning": "Tự động hóa cũ này thiếu cuộc trò chuyện đích. Hãy tạo lại từ cuộc trò chuyện hoặc channel nơi nó sẽ chạy.",
"origin": {
"system": "Hệ thống",
- "unknown": "Chưa liên kết cuộc trò chuyện",
- "legacy": "Tạo lại trong cuộc trò chuyện đích"
+ "unknown": "Chưa liên kết cuộc trò chuyện"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json
index f5e62e90..df54b609 100644
--- a/webui/src/i18n/locales/zh-CN/common.json
+++ b/webui/src/i18n/locales/zh-CN/common.json
@@ -525,18 +525,15 @@
"system": "系统",
"pending": "等待中",
"running": "正在运行",
- "needsSetup": "需重新设置",
"paused": "已暂停",
"failed": "失败",
"completed": "已完成",
"noSchedule": "无计划",
"active": "运行中"
},
- "legacyWarning": "这个旧版自动任务缺少目标会话。请从它应该运行的聊天或外部 channel 中重新创建。",
"origin": {
"system": "系统",
- "unknown": "未关联会话",
- "legacy": "请在目标会话中重新创建"
+ "unknown": "未关联会话"
},
"channels": {
"api": "API",
diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json
index c787de5a..2c6f438f 100644
--- a/webui/src/i18n/locales/zh-TW/common.json
+++ b/webui/src/i18n/locales/zh-TW/common.json
@@ -525,18 +525,15 @@
"system": "系統",
"pending": "等待中",
"running": "正在執行",
- "needsSetup": "需重新設定",
"paused": "已暫停",
"failed": "失敗",
"completed": "已完成",
"noSchedule": "無排程",
"active": "執行中"
},
- "legacyWarning": "這個舊版自動任務缺少目標會話。請從它應該執行的聊天或外部 channel 中重新建立。",
"origin": {
"system": "系統",
- "unknown": "未關聯會話",
- "legacy": "請在目標會話中重新建立"
+ "unknown": "未關聯會話"
},
"channels": {
"api": "API",
diff --git a/webui/src/lib/types.ts b/webui/src/lib/types.ts
index 436aa8d6..88de7e38 100644
--- a/webui/src/lib/types.ts
+++ b/webui/src/lib/types.ts
@@ -114,9 +114,6 @@ export interface SessionAutomationJob {
payload: {
message: string;
kind?: "agent_turn" | "system_event" | string;
- session_key?: string | null;
- origin_channel?: string | null;
- origin_chat_id?: string | null;
};
state: {
next_run_at_ms?: number | null;
diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx
index ff3e95d1..46657c28 100644
--- a/webui/src/tests/app-layout.test.tsx
+++ b/webui/src/tests/app-layout.test.tsx
@@ -372,7 +372,6 @@ describe("App layout", () => {
payload: {
message: "Check the repo status",
kind: "agent_turn",
- session_key: "websocket:chat-a",
},
state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
@@ -388,26 +387,6 @@ describe("App layout", () => {
preview: "Check release blockers",
},
},
- {
- id: "legacy-quiz",
- name: "english-quiz",
- enabled: true,
- protected: false,
- delete_after_run: false,
- schedule: { kind: "cron", expr: "30 9-23 * * *", tz: "Asia/Shanghai" },
- payload: {
- message: "Practice English",
- kind: "agent_turn",
- session_key: "unified:default",
- },
- state: {
- next_run_at_ms: Date.UTC(2026, 3, 17, 11, 0, 0),
- last_status: "ok",
- pending: false,
- run_history: [],
- },
- origin: null,
- },
{
id: "external-quiz",
name: "WeChat quiz",
@@ -418,7 +397,6 @@ describe("App layout", () => {
payload: {
message: "Send a quiz",
kind: "agent_turn",
- origin_channel: "weixin",
},
state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 11, 30, 0),
@@ -464,9 +442,6 @@ describe("App layout", () => {
expect(screen.getAllByText("Daily repo check").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Check the repo status").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Release prep").length).toBeGreaterThanOrEqual(1);
- expect(screen.getByText("english-quiz")).toBeInTheDocument();
- expect(screen.getByText("Recreate in target chat")).toBeInTheDocument();
- expect(screen.queryByText("unified:default")).not.toBeInTheDocument();
expect(screen.getByText("WeChat quiz")).toBeInTheDocument();
expect(screen.getByText("WeChat")).toBeInTheDocument();
expect(screen.queryByText("weixin:wx-chat")).not.toBeInTheDocument();
@@ -490,7 +465,6 @@ describe("App layout", () => {
payload: {
message: "Old one-shot message",
kind: "agent_turn",
- session_key: "websocket:chat-a",
},
state: {
next_run_at_ms: null,
@@ -589,7 +563,6 @@ describe("App layout", () => {
payload: {
message: longMessage,
kind: "agent_turn",
- session_key: "websocket:chat-a",
},
state: {
next_run_at_ms: Date.UTC(2026, 3, 18, 10, 0, 0),
@@ -650,7 +623,6 @@ describe("App layout", () => {
payload: {
message: "检查仓库状态",
kind: "agent_turn",
- session_key: "websocket:chat-a",
},
state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),