mirror of https://github.com/openclaw/openclaw.git
fix(heartbeat): pin HEARTBEAT.md reads to workspace path
This commit is contained in:
parent
1efa7a88c4
commit
604f22c42a
|
|
@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
|
|||
- OpenAI Codex OAuth/scope request parity: augment the OAuth authorize URL with required API scopes (`api.responses.write`, `model.request`, `api.model.read`) before browser handoff so OAuth tokens include runtime model/request permissions expected by OpenAI API calls. (#24720) Thanks @Skippy-Gunboat.
|
||||
- Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.
|
||||
- Kimi Coding/Anthropic tools compatibility: normalize `anthropic-messages` tool payloads to OpenAI-style `tools[].function` + compatible `tool_choice` when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.
|
||||
- Heartbeat/workspace-path guardrails: append explicit workspace `HEARTBEAT.md` path guidance (and `docs/heartbeat.md` avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.
|
||||
- Gateway/remote WS break-glass hostname support: honor `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` for `ws://` hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.
|
||||
- Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second `resolveAgentRoute` stalls in large binding configurations. (#36915) Thanks @songchenghao.
|
||||
- Browser/session cleanup: track browser tabs opened by session-scoped browser tool runs and close tracked tabs during `sessions.reset`/`sessions.delete` runtime cleanup, preventing orphaned tabs and unbounded browser memory growth after session teardown. (#36666) Thanks @Harnoor6693.
|
||||
|
|
|
|||
|
|
@ -1080,9 +1080,28 @@ describe("runHeartbeatOnce", () => {
|
|||
reason: params.reason,
|
||||
deps: createHeartbeatDeps(sendWhatsApp),
|
||||
});
|
||||
return { res, replySpy, sendWhatsApp };
|
||||
return { res, replySpy, sendWhatsApp, workspaceDir };
|
||||
}
|
||||
|
||||
it("adds explicit workspace HEARTBEAT.md path guidance to heartbeat prompts", async () => {
|
||||
const { res, replySpy, sendWhatsApp, workspaceDir } = await runHeartbeatFileScenario({
|
||||
fileState: "actionable",
|
||||
reason: "interval",
|
||||
replyText: "Checked logs and PRs",
|
||||
});
|
||||
try {
|
||||
expect(res.status).toBe("ran");
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const calledCtx = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||
const expectedPath = path.join(workspaceDir, "HEARTBEAT.md").replace(/\\/g, "/");
|
||||
expect(calledCtx.Body).toContain(`use workspace file ${expectedPath} (exact case)`);
|
||||
expect(calledCtx.Body).toContain("Do not read docs/heartbeat.md.");
|
||||
} finally {
|
||||
replySpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("applies HEARTBEAT.md gating rules across file states and triggers", async () => {
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -560,11 +560,24 @@ type HeartbeatPromptResolution = {
|
|||
hasCronEvents: boolean;
|
||||
};
|
||||
|
||||
function appendHeartbeatWorkspacePathHint(prompt: string, workspaceDir: string): string {
|
||||
if (!/heartbeat\.md/i.test(prompt)) {
|
||||
return prompt;
|
||||
}
|
||||
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME).replace(/\\/g, "/");
|
||||
const hint = `When reading HEARTBEAT.md, use workspace file ${heartbeatFilePath} (exact case). Do not read docs/heartbeat.md.`;
|
||||
if (prompt.includes(hint)) {
|
||||
return prompt;
|
||||
}
|
||||
return `${prompt}\n${hint}`;
|
||||
}
|
||||
|
||||
function resolveHeartbeatRunPrompt(params: {
|
||||
cfg: OpenClawConfig;
|
||||
heartbeat?: HeartbeatConfig;
|
||||
preflight: HeartbeatPreflight;
|
||||
canRelayToUser: boolean;
|
||||
workspaceDir: string;
|
||||
}): HeartbeatPromptResolution {
|
||||
const pendingEventEntries = params.preflight.pendingEventEntries;
|
||||
const pendingEvents = params.preflight.shouldInspectPendingEvents
|
||||
|
|
@ -579,11 +592,12 @@ function resolveHeartbeatRunPrompt(params: {
|
|||
.map((event) => event.text);
|
||||
const hasExecCompletion = pendingEvents.some(isExecCompletionEvent);
|
||||
const hasCronEvents = cronEvents.length > 0;
|
||||
const prompt = hasExecCompletion
|
||||
const basePrompt = hasExecCompletion
|
||||
? buildExecEventPrompt({ deliverToUser: params.canRelayToUser })
|
||||
: hasCronEvents
|
||||
? buildCronEventPrompt(cronEvents, { deliverToUser: params.canRelayToUser })
|
||||
: resolveHeartbeatPrompt(params.cfg, params.heartbeat);
|
||||
const prompt = appendHeartbeatWorkspacePathHint(basePrompt, params.workspaceDir);
|
||||
|
||||
return { prompt, hasExecCompletion, hasCronEvents };
|
||||
}
|
||||
|
|
@ -668,11 +682,13 @@ export async function runHeartbeatOnce(opts: {
|
|||
const canRelayToUser = Boolean(
|
||||
delivery.channel !== "none" && delivery.to && visibility.showAlerts,
|
||||
);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const { prompt, hasExecCompletion, hasCronEvents } = resolveHeartbeatRunPrompt({
|
||||
cfg,
|
||||
heartbeat,
|
||||
preflight,
|
||||
canRelayToUser,
|
||||
workspaceDir,
|
||||
});
|
||||
const ctx = {
|
||||
Body: appendCronStyleCurrentTimeLine(prompt, cfg, startedAt),
|
||||
|
|
|
|||
Loading…
Reference in New Issue