feat(heartbeat): change default target to 'last' and remove target option from UI
This commit is contained in:
@@ -235,10 +235,7 @@ def _update_agent_heartbeat(
|
|||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
heartbeat.update(raw)
|
heartbeat.update(raw)
|
||||||
heartbeat["every"] = payload.every
|
heartbeat["every"] = payload.every
|
||||||
if payload.target is not None:
|
heartbeat["target"] = DEFAULT_HEARTBEAT_CONFIG.get("target", "last")
|
||||||
heartbeat["target"] = payload.target
|
|
||||||
elif "target" not in heartbeat:
|
|
||||||
heartbeat["target"] = DEFAULT_HEARTBEAT_CONFIG.get("target", "none")
|
|
||||||
agent.heartbeat_config = heartbeat
|
agent.heartbeat_config = heartbeat
|
||||||
agent.updated_at = utcnow()
|
agent.updated_at = utcnow()
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ class BoardGroupHeartbeatApply(SQLModel):
|
|||||||
# Heartbeat cadence string understood by the OpenClaw gateway
|
# Heartbeat cadence string understood by the OpenClaw gateway
|
||||||
# (e.g. "2m", "10m", "30m").
|
# (e.g. "2m", "10m", "30m").
|
||||||
every: str
|
every: str
|
||||||
# Optional heartbeat target (most deployments use "none").
|
|
||||||
target: str | None = None
|
|
||||||
include_board_leads: bool = False
|
include_board_leads: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ _GATEWAY_AGENT_SUFFIX = ":main"
|
|||||||
|
|
||||||
DEFAULT_HEARTBEAT_CONFIG: dict[str, Any] = {
|
DEFAULT_HEARTBEAT_CONFIG: dict[str, Any] = {
|
||||||
"every": "10m",
|
"every": "10m",
|
||||||
"target": "none",
|
"target": "last",
|
||||||
"includeReasoning": False,
|
"includeReasoning": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ async def test_control_plane_upsert_agent_create_then_update(monkeypatch):
|
|||||||
agent_id="board-agent-a",
|
agent_id="board-agent-a",
|
||||||
name="Board Agent A",
|
name="Board Agent A",
|
||||||
workspace_path="/tmp/workspace-board-agent-a",
|
workspace_path="/tmp/workspace-board-agent-a",
|
||||||
heartbeat={"every": "10m", "target": "none", "includeReasoning": False},
|
heartbeat={"every": "10m", "target": "last", "includeReasoning": False},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ async def test_control_plane_upsert_agent_handles_already_exists(monkeypatch):
|
|||||||
agent_id="board-agent-a",
|
agent_id="board-agent-a",
|
||||||
name="Board Agent A",
|
name="Board Agent A",
|
||||||
workspace_path="/tmp/workspace-board-agent-a",
|
workspace_path="/tmp/workspace-board-agent-a",
|
||||||
heartbeat={"every": "10m", "target": "none", "includeReasoning": False},
|
heartbeat={"every": "10m", "target": "last", "includeReasoning": False},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -52,11 +52,6 @@ const EMOJI_OPTIONS = [
|
|||||||
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
|
||||||
{ value: "none", label: "None (no outbound message)" },
|
|
||||||
{ value: "last", label: "Last channel" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] =>
|
const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] =>
|
||||||
boards.map((board) => ({
|
boards.map((board) => ({
|
||||||
value: board.id,
|
value: board.id,
|
||||||
@@ -111,9 +106,6 @@ export default function EditAgentPage() {
|
|||||||
const [heartbeatEvery, setHeartbeatEvery] = useState<string | undefined>(
|
const [heartbeatEvery, setHeartbeatEvery] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [heartbeatTarget, setHeartbeatTarget] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [identityProfile, setIdentityProfile] = useState<
|
const [identityProfile, setIdentityProfile] = useState<
|
||||||
IdentityProfile | undefined
|
IdentityProfile | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -166,13 +158,11 @@ export default function EditAgentPage() {
|
|||||||
if (heartbeat && typeof heartbeat === "object") {
|
if (heartbeat && typeof heartbeat === "object") {
|
||||||
const record = heartbeat as Record<string, unknown>;
|
const record = heartbeat as Record<string, unknown>;
|
||||||
const every = record.every;
|
const every = record.every;
|
||||||
const target = record.target;
|
|
||||||
return {
|
return {
|
||||||
every: typeof every === "string" && every.trim() ? every : "10m",
|
every: typeof every === "string" && every.trim() ? every : "10m",
|
||||||
target: typeof target === "string" && target.trim() ? target : "none",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { every: "10m", target: "none" };
|
return { every: "10m" };
|
||||||
}, [loadedAgent?.heartbeat_config]);
|
}, [loadedAgent?.heartbeat_config]);
|
||||||
|
|
||||||
const loadedIdentityProfile = useMemo(() => {
|
const loadedIdentityProfile = useMemo(() => {
|
||||||
@@ -200,7 +190,6 @@ export default function EditAgentPage() {
|
|||||||
const resolvedIsGatewayMain =
|
const resolvedIsGatewayMain =
|
||||||
isGatewayMain ?? Boolean(loadedAgent?.is_gateway_main);
|
isGatewayMain ?? Boolean(loadedAgent?.is_gateway_main);
|
||||||
const resolvedHeartbeatEvery = heartbeatEvery ?? loadedHeartbeat.every;
|
const resolvedHeartbeatEvery = heartbeatEvery ?? loadedHeartbeat.every;
|
||||||
const resolvedHeartbeatTarget = heartbeatTarget ?? loadedHeartbeat.target;
|
|
||||||
const resolvedIdentityProfile = identityProfile ?? loadedIdentityProfile;
|
const resolvedIdentityProfile = identityProfile ?? loadedIdentityProfile;
|
||||||
|
|
||||||
const resolvedBoardId = useMemo(() => {
|
const resolvedBoardId = useMemo(() => {
|
||||||
@@ -244,7 +233,7 @@ export default function EditAgentPage() {
|
|||||||
heartbeat_config: {
|
heartbeat_config: {
|
||||||
...existingHeartbeat,
|
...existingHeartbeat,
|
||||||
every: resolvedHeartbeatEvery.trim() || "10m",
|
every: resolvedHeartbeatEvery.trim() || "10m",
|
||||||
target: resolvedHeartbeatTarget,
|
target: "last",
|
||||||
includeReasoning:
|
includeReasoning:
|
||||||
typeof existingHeartbeat.includeReasoning === "boolean"
|
typeof existingHeartbeat.includeReasoning === "boolean"
|
||||||
? existingHeartbeat.includeReasoning
|
? existingHeartbeat.includeReasoning
|
||||||
@@ -449,7 +438,7 @@ export default function EditAgentPage() {
|
|||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Schedule & notifications
|
Schedule & notifications
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Interval
|
Interval
|
||||||
@@ -464,24 +453,6 @@ export default function EditAgentPage() {
|
|||||||
Set how often this agent runs HEARTBEAT.md.
|
Set how often this agent runs HEARTBEAT.md.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Target
|
|
||||||
</label>
|
|
||||||
<SearchableSelect
|
|
||||||
ariaLabel="Select heartbeat target"
|
|
||||||
value={resolvedHeartbeatTarget}
|
|
||||||
onValueChange={setHeartbeatTarget}
|
|
||||||
options={HEARTBEAT_TARGET_OPTIONS}
|
|
||||||
placeholder="Select target"
|
|
||||||
searchPlaceholder="Search targets..."
|
|
||||||
emptyMessage="No matching targets."
|
|
||||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
|
||||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
|
||||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,6 @@ const EMOJI_OPTIONS = [
|
|||||||
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
{ value: ":brain:", label: "Brain", glyph: "🧠" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HEARTBEAT_TARGET_OPTIONS: SearchableSelectOption[] = [
|
|
||||||
{ value: "none", label: "None (no outbound message)" },
|
|
||||||
{ value: "last", label: "Last channel" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] =>
|
const getBoardOptions = (boards: BoardRead[]): SearchableSelectOption[] =>
|
||||||
boards.map((board) => ({
|
boards.map((board) => ({
|
||||||
value: board.id,
|
value: board.id,
|
||||||
@@ -81,7 +76,6 @@ export default function NewAgentPage() {
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [boardId, setBoardId] = useState<string>("");
|
const [boardId, setBoardId] = useState<string>("");
|
||||||
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
const [heartbeatEvery, setHeartbeatEvery] = useState("10m");
|
||||||
const [heartbeatTarget, setHeartbeatTarget] = useState("none");
|
|
||||||
const [identityProfile, setIdentityProfile] = useState<IdentityProfile>({
|
const [identityProfile, setIdentityProfile] = useState<IdentityProfile>({
|
||||||
...DEFAULT_IDENTITY_PROFILE,
|
...DEFAULT_IDENTITY_PROFILE,
|
||||||
});
|
});
|
||||||
@@ -136,7 +130,7 @@ export default function NewAgentPage() {
|
|||||||
board_id: resolvedBoardId,
|
board_id: resolvedBoardId,
|
||||||
heartbeat_config: {
|
heartbeat_config: {
|
||||||
every: heartbeatEvery.trim() || "10m",
|
every: heartbeatEvery.trim() || "10m",
|
||||||
target: heartbeatTarget,
|
target: "last",
|
||||||
includeReasoning: false,
|
includeReasoning: false,
|
||||||
},
|
},
|
||||||
identity_profile: normalizeIdentityProfile(
|
identity_profile: normalizeIdentityProfile(
|
||||||
@@ -277,7 +271,7 @@ export default function NewAgentPage() {
|
|||||||
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||||
Schedule & notifications
|
Schedule & notifications
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 grid gap-6 md:grid-cols-2">
|
<div className="mt-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium text-slate-900">
|
<label className="text-sm font-medium text-slate-900">
|
||||||
Interval
|
Interval
|
||||||
@@ -292,24 +286,6 @@ export default function NewAgentPage() {
|
|||||||
How often this agent runs HEARTBEAT.md (10m, 30m, 2h).
|
How often this agent runs HEARTBEAT.md (10m, 30m, 2h).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium text-slate-900">
|
|
||||||
Target
|
|
||||||
</label>
|
|
||||||
<SearchableSelect
|
|
||||||
ariaLabel="Select heartbeat target"
|
|
||||||
value={heartbeatTarget}
|
|
||||||
onValueChange={setHeartbeatTarget}
|
|
||||||
options={HEARTBEAT_TARGET_OPTIONS}
|
|
||||||
placeholder="Select target"
|
|
||||||
searchPlaceholder="Search targets..."
|
|
||||||
emptyMessage="No matching targets."
|
|
||||||
triggerClassName="w-full h-11 rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-200"
|
|
||||||
contentClassName="rounded-xl border border-slate-200 shadow-lg"
|
|
||||||
itemClassName="px-4 py-3 text-sm text-slate-700 data-[selected=true]:bg-slate-50 data-[selected=true]:text-slate-900"
|
|
||||||
disabled={isLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user