mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(cron): clarify no-delivery previews
This commit is contained in:
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -103,10 +103,13 @@ struct SessionRow: Identifiable {
|
||||
}
|
||||
|
||||
enum SessionKind {
|
||||
case direct, group, global, unknown
|
||||
case cron, direct, group, global, unknown
|
||||
|
||||
static func from(key: String) -> SessionKind {
|
||||
if key == "global" { return .global }
|
||||
let parts = key.lowercased().split(separator: ":").filter { !$0.isEmpty }
|
||||
if parts.first == "cron" { return .cron }
|
||||
if parts.count >= 3, parts[0] == "agent", parts[2] == "cron" { return .cron }
|
||||
if key.hasPrefix("group:") { return .group }
|
||||
if key.contains(":group:") { return .group }
|
||||
if key.contains(":channel:") { return .group }
|
||||
@@ -116,6 +119,7 @@ enum SessionKind {
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .cron: "Cron"
|
||||
case .direct: "Direct"
|
||||
case .group: "Group"
|
||||
case .global: "Global"
|
||||
@@ -125,6 +129,7 @@ enum SessionKind {
|
||||
|
||||
var tint: Color {
|
||||
switch self {
|
||||
case .cron: .green
|
||||
case .direct: .accentColor
|
||||
case .group: .orange
|
||||
case .global: .purple
|
||||
|
||||
@@ -5,6 +5,8 @@ import Testing
|
||||
struct SessionDataTests {
|
||||
@Test func `session kind from key detects common kinds`() {
|
||||
#expect(SessionKind.from(key: "global") == .global)
|
||||
#expect(SessionKind.from(key: "cron:daily") == .cron)
|
||||
#expect(SessionKind.from(key: "agent:main:cron:daily") == .cron)
|
||||
#expect(SessionKind.from(key: "discord:group:engineering") == .group)
|
||||
#expect(SessionKind.from(key: "unknown") == .unknown)
|
||||
#expect(SessionKind.from(key: "user@example.com") == .direct)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { loadSessionStore, resolveSessionTotalTokens } from "../config/sessions.
|
||||
import { info } from "../globals.js";
|
||||
import { parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js";
|
||||
import { isCronSessionKey } from "../sessions/session-key-utils.js";
|
||||
import { createLazyImportLoader } from "../shared/lazy-promise.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import { resolveSessionStoreTargetsOrExit } from "./session-store-targets.js";
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
|
||||
type SessionRow = SessionDisplayRow & {
|
||||
agentId: string;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
kind: "cron" | "direct" | "group" | "global" | "unknown";
|
||||
agentRuntime: ReturnType<typeof resolveAgentRuntimeMetadata>;
|
||||
};
|
||||
|
||||
@@ -84,6 +85,9 @@ function classifySessionKey(key: string, entry?: { chatType?: string | null }):
|
||||
if (key === "unknown") {
|
||||
return "unknown";
|
||||
}
|
||||
if (isCronSessionKey(key)) {
|
||||
return "cron";
|
||||
}
|
||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||
return "group";
|
||||
}
|
||||
|
||||
@@ -70,6 +70,19 @@ describe("status.command-sections", () => {
|
||||
contextTokens: null,
|
||||
flags: [],
|
||||
},
|
||||
{
|
||||
key: "agent:main:cron:daily-digest",
|
||||
kind: "cron",
|
||||
updatedAt: 2,
|
||||
age: 7_000,
|
||||
model: "gpt-5.5",
|
||||
totalTokens: null,
|
||||
totalTokensFresh: false,
|
||||
remainingTokens: null,
|
||||
percentUsed: null,
|
||||
contextTokens: null,
|
||||
flags: [],
|
||||
},
|
||||
],
|
||||
verbose: true,
|
||||
shortenText: (value) => value.slice(0, 8),
|
||||
@@ -88,6 +101,14 @@ describe("status.command-sections", () => {
|
||||
Tokens: "12k",
|
||||
Cache: "cache ok",
|
||||
},
|
||||
{
|
||||
Key: "agent:ma",
|
||||
Kind: "cron",
|
||||
Age: "7000ms",
|
||||
Model: "gpt-5.5",
|
||||
Tokens: "12k",
|
||||
Cache: "cache ok",
|
||||
},
|
||||
]);
|
||||
|
||||
const emptyRows = buildStatusSessionsRows({
|
||||
|
||||
@@ -41,6 +41,15 @@ describe("statusSummaryRuntime.resolveContextTokensForModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("statusSummaryRuntime.classifySessionKey", () => {
|
||||
it("classifies cron history sessions distinctly", () => {
|
||||
expect(statusSummaryRuntime.classifySessionKey("agent:main:cron:daily-digest")).toBe("cron");
|
||||
expect(
|
||||
statusSummaryRuntime.classifySessionKey("agent:avery:cron:daily-digest:run:abc123"),
|
||||
).toBe("cron");
|
||||
});
|
||||
});
|
||||
|
||||
describe("statusSummaryRuntime.resolveSessionModelRef", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { isCronSessionKey } from "../sessions/session-key-utils.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -129,6 +130,9 @@ function classifySessionKey(key: string, entry?: SessionEntry) {
|
||||
if (key === "unknown") {
|
||||
return "unknown";
|
||||
}
|
||||
if (isCronSessionKey(key)) {
|
||||
return "cron";
|
||||
}
|
||||
if (entry?.chatType === "group" || entry?.chatType === "channel") {
|
||||
return "group";
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TaskRegistrySummary } from "../tasks/task-registry.types.js";
|
||||
export type SessionStatus = {
|
||||
agentId?: string;
|
||||
key: string;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
kind: "cron" | "direct" | "group" | "global" | "unknown";
|
||||
sessionId?: string;
|
||||
updatedAt: number | null;
|
||||
age: number | null;
|
||||
|
||||
@@ -51,4 +51,19 @@ describe("resolveCronDeliveryPreview", () => {
|
||||
"resolved from last, session agent:avery:telegram:direct:direct-123",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not resolve routes for explicit no-delivery jobs", async () => {
|
||||
const job = makeCronJob({
|
||||
delivery: { mode: "none" },
|
||||
sessionTarget: "isolated",
|
||||
});
|
||||
|
||||
const preview = await resolveCronDeliveryPreview({
|
||||
cfg: {} as never,
|
||||
job,
|
||||
});
|
||||
|
||||
expect(preview).toEqual({ label: "not requested", detail: "not requested" });
|
||||
expect(mocks.resolveDeliveryTarget).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function resolveCronDeliveryPreview(params: {
|
||||
job: CronJob;
|
||||
}): Promise<CronDeliveryPreview> {
|
||||
const plan = resolveCronDeliveryPlan(params.job);
|
||||
if (!plan.requested && plan.mode === "none" && !params.job.delivery) {
|
||||
if (plan.mode === "none") {
|
||||
return { label: "not requested", detail: "not requested" };
|
||||
}
|
||||
if (plan.mode === "webhook") {
|
||||
|
||||
@@ -2404,6 +2404,11 @@
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.data-table-badge--cron {
|
||||
color: var(--success);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.data-table-badge--global {
|
||||
color: var(--warn);
|
||||
background: var(--warn-subtle);
|
||||
|
||||
@@ -118,7 +118,11 @@ function hasOwn(record: Record<string, unknown>, key: string): boolean {
|
||||
}
|
||||
|
||||
function normalizeSessionKind(value: unknown): GatewaySessionRow["kind"] | undefined {
|
||||
return value === "direct" || value === "group" || value === "global" || value === "unknown"
|
||||
return value === "cron" ||
|
||||
value === "direct" ||
|
||||
value === "group" ||
|
||||
value === "global" ||
|
||||
value === "unknown"
|
||||
? value
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ export type SessionCompactionCheckpointPreview = Pick<
|
||||
export type GatewaySessionRow = {
|
||||
key: string;
|
||||
spawnedBy?: string;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
kind: "cron" | "direct" | "group" | "global" | "unknown";
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
surface?: string;
|
||||
|
||||
@@ -220,6 +220,26 @@ describe("sessions view", () => {
|
||||
expect(keyCell?.getAttribute("title")).toBe("agent:unknown-agent:telegram:abc123");
|
||||
});
|
||||
|
||||
it("renders cron session kind distinctly", async () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderSessions(
|
||||
buildProps(
|
||||
buildResult({
|
||||
key: "agent:main:cron:daily-digest",
|
||||
kind: "cron",
|
||||
updatedAt: Date.now(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
container,
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
const badge = container.querySelector(".data-table-badge--cron");
|
||||
expect(badge?.textContent?.trim()).toBe("cron");
|
||||
});
|
||||
|
||||
it("keeps raw keys for inherited identity object properties", async () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
|
||||
@@ -519,13 +519,15 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) {
|
||||
? `${pathForTab("chat", props.basePath)}?session=${encodeURIComponent(row.key)}`
|
||||
: null;
|
||||
const badgeClass =
|
||||
row.kind === "direct"
|
||||
? "data-table-badge--direct"
|
||||
: row.kind === "group"
|
||||
? "data-table-badge--group"
|
||||
: row.kind === "global"
|
||||
? "data-table-badge--global"
|
||||
: "data-table-badge--unknown";
|
||||
row.kind === "cron"
|
||||
? "data-table-badge--cron"
|
||||
: row.kind === "direct"
|
||||
? "data-table-badge--direct"
|
||||
: row.kind === "group"
|
||||
? "data-table-badge--group"
|
||||
: row.kind === "global"
|
||||
? "data-table-badge--global"
|
||||
: "data-table-badge--unknown";
|
||||
|
||||
return [
|
||||
html`<tr>
|
||||
|
||||
Reference in New Issue
Block a user