fix(cron): clarify no-delivery previews · openclaw/openclaw@0872b50
steipete
·
2026-05-04
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
37 | 37 | - Channels/streaming: expose `streaming.progress.label`, `labels`, `maxLines`, and `toolProgress` in bundled channel config metadata so progress draft settings appear in config, docs, and control surfaces. Thanks @vincentkoc. |
38 | 38 | - Channels/streaming: normalize whitespace and case for `streaming.progress.label: "auto"` so progress draft labels keep using the built-in label pool instead of rendering a literal `auto` title. Thanks @vincentkoc. |
39 | 39 | - Gateway/install: prefer supported system Node over nvm/fnm/volta/asdf/mise when regenerating managed gateway services, so `gateway install --force` no longer recreates service definitions that doctor immediately flags as version-manager-backed. Fixes #76339. Thanks @brokemac79. |
| 40 | +- Cron/status: render explicit `delivery.mode: "none"` jobs as no-delivery previews and label cron session history distinctly instead of showing fallback delivery or direct-session rows. Fixes #76945. |
40 | 41 | - Gateway/usage: serve `usage.cost` and `sessions.usage` from a durable transcript aggregate cache with lock-safe background refreshes and localized stale-cache status, so large usage views avoid repeated full scans. (#76650) Thanks @Marvinthebored. |
41 | 42 | - Plugins/hooks: let `plugins.entries.<id>.hooks.timeoutMs` and `plugins.entries.<id>.hooks.timeouts` bound plugin typed hooks from operator config, so slow hooks can be tuned without patching installed plugin code. Fixes #76778. Thanks @vincentkoc. |
42 | 43 | - Telegram: add `channels.telegram.mediaGroupFlushMs` at the top level and per account so operators can tune album buffering instead of being stuck with the hard-coded 500ms media-group flush window. Fixes #76149. Thanks @vincentkoc. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -103,10 +103,13 @@ struct SessionRow: Identifiable {
|
103 | 103 | } |
104 | 104 | |
105 | 105 | enum SessionKind { |
106 | | -case direct, group, global, unknown |
| 106 | +case cron, direct, group, global, unknown |
107 | 107 | |
108 | 108 | static func from(key: String) -> SessionKind { |
109 | 109 | if key == "global" { return .global } |
| 110 | +let parts = key.lowercased().split(separator: ":").filter { !$0.isEmpty } |
| 111 | +if parts.first == "cron" { return .cron } |
| 112 | +if parts.count >= 3, parts[0] == "agent", parts[2] == "cron" { return .cron } |
110 | 113 | if key.hasPrefix("group:") { return .group } |
111 | 114 | if key.contains(":group:") { return .group } |
112 | 115 | if key.contains(":channel:") { return .group } |
@@ -116,6 +119,7 @@ enum SessionKind {
|
116 | 119 | |
117 | 120 | var label: String { |
118 | 121 | switch self { |
| 122 | +case .cron: "Cron" |
119 | 123 | case .direct: "Direct" |
120 | 124 | case .group: "Group" |
121 | 125 | case .global: "Global" |
@@ -125,6 +129,7 @@ enum SessionKind {
|
125 | 129 | |
126 | 130 | var tint: Color { |
127 | 131 | switch self { |
| 132 | +case .cron: .green |
128 | 133 | case .direct: .accentColor |
129 | 134 | case .group: .orange |
130 | 135 | case .global: .purple |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,6 +5,8 @@ import Testing
|
5 | 5 | struct SessionDataTests { |
6 | 6 | @Test func `session kind from key detects common kinds`() { |
7 | 7 | #expect(SessionKind.from(key: "global") == .global) |
| 8 | + #expect(SessionKind.from(key: "cron:daily") == .cron) |
| 9 | + #expect(SessionKind.from(key: "agent:main:cron:daily") == .cron) |
8 | 10 | #expect(SessionKind.from(key: "discord:group:engineering") == .group) |
9 | 11 | #expect(SessionKind.from(key: "unknown") == .unknown) |
10 | 12 | #expect(SessionKind.from(key: "user@example.com") == .direct) |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,6 +5,7 @@ import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.
|
5 | 5 | import { info } from "../globals.js"; |
6 | 6 | import { parseAgentSessionKey } from "../routing/session-key.js"; |
7 | 7 | import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; |
| 8 | +import { isCronSessionKey } from "../sessions/session-key-utils.js"; |
8 | 9 | import { createLazyImportLoader } from "../shared/lazy-promise.js"; |
9 | 10 | import { isRich, theme } from "../terminal/theme.js"; |
10 | 11 | import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js"; |
@@ -27,7 +28,7 @@ import {
|
27 | 28 | |
28 | 29 | type SessionRow = SessionDisplayRow & { |
29 | 30 | agentId: string; |
30 | | -kind: "direct" | "group" | "global" | "unknown"; |
| 31 | +kind: "cron" | "direct" | "group" | "global" | "unknown"; |
31 | 32 | agentRuntime: ReturnType<typeof resolveAgentRuntimeMetadata>; |
32 | 33 | }; |
33 | 34 | |
@@ -84,6 +85,9 @@ function classifySessionKey(key: string, entry?: { chatType?: string | null }):
|
84 | 85 | if (key === "unknown") { |
85 | 86 | return "unknown"; |
86 | 87 | } |
| 88 | +if (isCronSessionKey(key)) { |
| 89 | +return "cron"; |
| 90 | +} |
87 | 91 | if (entry?.chatType === "group" || entry?.chatType === "channel") { |
88 | 92 | return "group"; |
89 | 93 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -70,6 +70,19 @@ describe("status.command-sections", () => {
|
70 | 70 | contextTokens: null, |
71 | 71 | flags: [], |
72 | 72 | }, |
| 73 | +{ |
| 74 | +key: "agent:main:cron:daily-digest", |
| 75 | +kind: "cron", |
| 76 | +updatedAt: 2, |
| 77 | +age: 7_000, |
| 78 | +model: "gpt-5.5", |
| 79 | +totalTokens: null, |
| 80 | +totalTokensFresh: false, |
| 81 | +remainingTokens: null, |
| 82 | +percentUsed: null, |
| 83 | +contextTokens: null, |
| 84 | +flags: [], |
| 85 | +}, |
73 | 86 | ], |
74 | 87 | verbose: true, |
75 | 88 | shortenText: (value) => value.slice(0, 8), |
@@ -88,6 +101,14 @@ describe("status.command-sections", () => {
|
88 | 101 | Tokens: "12k", |
89 | 102 | Cache: "cache ok", |
90 | 103 | }, |
| 104 | +{ |
| 105 | +Key: "agent:ma", |
| 106 | +Kind: "cron", |
| 107 | +Age: "7000ms", |
| 108 | +Model: "gpt-5.5", |
| 109 | +Tokens: "12k", |
| 110 | +Cache: "cache ok", |
| 111 | +}, |
91 | 112 | ]); |
92 | 113 | |
93 | 114 | const emptyRows = buildStatusSessionsRows({ |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -41,6 +41,15 @@ describe("statusSummaryRuntime.resolveContextTokensForModel", () => {
|
41 | 41 | }); |
42 | 42 | }); |
43 | 43 | |
| 44 | +describe("statusSummaryRuntime.classifySessionKey", () => { |
| 45 | +it("classifies cron history sessions distinctly", () => { |
| 46 | +expect(statusSummaryRuntime.classifySessionKey("agent:main:cron:daily-digest")).toBe("cron"); |
| 47 | +expect( |
| 48 | +statusSummaryRuntime.classifySessionKey("agent:avery:cron:daily-digest:run:abc123"), |
| 49 | +).toBe("cron"); |
| 50 | +}); |
| 51 | +}); |
| 52 | + |
44 | 53 | describe("statusSummaryRuntime.resolveSessionModelRef", () => { |
45 | 54 | const cfg = { |
46 | 55 | agents: { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,6 +5,7 @@ import { normalizeProviderId } from "../agents/provider-id.js";
|
5 | 5 | import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; |
6 | 6 | import type { SessionEntry } from "../config/sessions/types.js"; |
7 | 7 | import type { OpenClawConfig } from "../config/types.js"; |
| 8 | +import { isCronSessionKey } from "../sessions/session-key-utils.js"; |
8 | 9 | import { |
9 | 10 | normalizeLowercaseStringOrEmpty, |
10 | 11 | normalizeOptionalString, |
@@ -129,6 +130,9 @@ function classifySessionKey(key: string, entry?: SessionEntry) {
|
129 | 130 | if (key === "unknown") { |
130 | 131 | return "unknown"; |
131 | 132 | } |
| 133 | +if (isCronSessionKey(key)) { |
| 134 | +return "cron"; |
| 135 | +} |
132 | 136 | if (entry?.chatType === "group" || entry?.chatType === "channel") { |
133 | 137 | return "group"; |
134 | 138 | } |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,7 +5,7 @@ import type { TaskRegistrySummary } from "../tasks/task-registry.types.js";
|
5 | 5 | export type SessionStatus = { |
6 | 6 | agentId?: string; |
7 | 7 | key: string; |
8 | | -kind: "direct" | "group" | "global" | "unknown"; |
| 8 | +kind: "cron" | "direct" | "group" | "global" | "unknown"; |
9 | 9 | sessionId?: string; |
10 | 10 | updatedAt: number | null; |
11 | 11 | age: number | null; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -51,4 +51,19 @@ describe("resolveCronDeliveryPreview", () => {
|
51 | 51 | "resolved from last, session agent:avery:telegram:direct:direct-123", |
52 | 52 | ); |
53 | 53 | }); |
| 54 | + |
| 55 | +it("does not resolve routes for explicit no-delivery jobs", async () => { |
| 56 | +const job = makeCronJob({ |
| 57 | +delivery: { mode: "none" }, |
| 58 | +sessionTarget: "isolated", |
| 59 | +}); |
| 60 | + |
| 61 | +const preview = await resolveCronDeliveryPreview({ |
| 62 | +cfg: {} as never, |
| 63 | + job, |
| 64 | +}); |
| 65 | + |
| 66 | +expect(preview).toEqual({ label: "not requested", detail: "not requested" }); |
| 67 | +expect(mocks.resolveDeliveryTarget).not.toHaveBeenCalled(); |
| 68 | +}); |
54 | 69 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -40,7 +40,7 @@ export async function resolveCronDeliveryPreview(params: {
|
40 | 40 | job: CronJob; |
41 | 41 | }): Promise<CronDeliveryPreview> { |
42 | 42 | const plan = resolveCronDeliveryPlan(params.job); |
43 | | -if (!plan.requested && plan.mode === "none" && !params.job.delivery) { |
| 43 | +if (plan.mode === "none") { |
44 | 44 | return { label: "not requested", detail: "not requested" }; |
45 | 45 | } |
46 | 46 | if (plan.mode === "webhook") { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。