Skip to content

Commit 2f3b739

Browse files
authored
fix(message): validate targets before channel discovery
1 parent 50e7668 commit 2f3b739

41 files changed

Lines changed: 1658 additions & 152 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/openclaw-landable-bug-sweep/SKILL.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
---
22
name: openclaw-landable-bug-sweep
3-
description: "Find or repair small high-confidence non-SDK-boundary OpenClaw bugfix PRs until five are landable."
3+
description: "Find or repair a requested batch of small high-confidence non-SDK-boundary OpenClaw bugfix PRs until they are landable."
44
---
55

66
# OpenClaw Landable Bug Sweep
77

8-
Autonomous maintainer workflow for producing five landable OpenClaw bugfix PR URLs.
8+
Autonomous maintainer workflow for producing a requested batch of landable OpenClaw bugfix PR URLs.
99
Use for broad issue/PR sweeps where the bar is high and the output is PRs, not notes.
1010
Do not use for plugin SDK/API boundary work; those need separate architecture review.
1111

1212
## Target
1313

14-
Return exactly five PR URLs, each with:
14+
Use `batch_size` from the request, defaulting to `5` and capped at `20`.
15+
Return up to that many qualified PR URLs, each with:
1516

1617
- bug summary
1718
- why the fix is low-risk
@@ -20,9 +21,18 @@ Return exactly five PR URLs, each with:
2021
- CI green on the exact pushed PR head
2122
- issue/duplicate cleanup done or still pending
2223

23-
The five URLs may be existing PRs that were reviewed/fixed, or new PRs created from issues/clusters.
24+
The URLs may be existing PRs that were reviewed/fixed, or new PRs created from issues/clusters.
2425
Do not present a PR URL to the maintainer until it has been refreshed on current `main`, left-tested, autoreviewed clean, pushed, and verified green in live GitHub CI.
2526
If code, tests, changelog, PR body, or branch base changes after autoreview, rerun autoreview before showing the URL.
27+
Do not pad a batch when the bounded search yields fewer qualified PRs.
28+
29+
## Inputs
30+
31+
- `batch_size`: requested number of landable PRs; default `5`, maximum `20`.
32+
- `source_mode`: `discovery` or `provided-prs`; default `discovery`.
33+
- `provided_prs`: explicit PR refs when `source_mode=provided-prs`.
34+
35+
In `provided-prs` mode, inspect only the supplied PRs plus directly linked duplicate/canonical refs unless broader discovery is required to prove the best fix.
2636

2737
## Companion Skills
2838

@@ -103,7 +113,7 @@ Reject:
103113
- close duplicates and fixed-on-main issues/PRs with proof as soon as you notice them during the sweep
104114
- never mutate more than five associated items in one cluster without explicit confirmation
105115
- comments must be kind, concrete, and include proof/PR/commit links
106-
8. Repeat until five landable PR URLs are ready.
116+
8. Repeat until `batch_size` landable PR URLs are ready or the bounded qualified queue is exhausted.
107117

108118
## PR Body Proof
109119

@@ -151,7 +161,7 @@ closed:
151161

152162
Final answer:
153163

154-
- exactly five accepted PR URLs
164+
- the requested number of accepted PR URLs, or the smaller qualified count with the exhausted-search reason
155165
- 2-4 sentence explainer per PR
156166
- proof/CI state per PR
157167
- closed duplicates/fixed-on-main refs

apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import androidx.compose.ui.graphics.Color
2727
import androidx.compose.ui.text.font.FontWeight
2828
import androidx.compose.ui.unit.sp
2929
import androidx.core.view.WindowCompat
30+
import androidx.core.view.WindowInsetsCompat
31+
import androidx.core.view.WindowInsetsControllerCompat
3032
import androidx.lifecycle.Lifecycle
3133
import androidx.lifecycle.lifecycleScope
3234
import androidx.lifecycle.repeatOnLifecycle
@@ -87,11 +89,21 @@ class MainActivity : ComponentActivity() {
8789
}
8890

8991
private fun enterScreenshotMode(scene: AndroidScreenshotScene) {
92+
hideScreenshotModeStatusBar()
9093
setContent {
9194
AndroidScreenshotModeScreen(scene = scene)
9295
}
9396
}
9497

98+
private fun hideScreenshotModeStatusBar() {
99+
WindowCompat
100+
.getInsetsController(window, window.decorView)
101+
.apply {
102+
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
103+
hide(WindowInsetsCompat.Type.statusBars())
104+
}
105+
}
106+
95107
override fun onStart() {
96108
super.onStart()
97109
foreground = true

extensions/active-memory/index.test.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,16 +2883,16 @@ describe("active-memory plugin", () => {
28832883
);
28842884

28852885
const prependContext = requirePrependContext(result);
2886-
expect(prependContext).toContain("alpha beta gamma delta epsilon zeta");
2886+
expect(prependContext).toContain("alpha beta gamma delta epsilon zeta eta…");
28872887
expect(prependContext).toContain("<active_memory_plugin>");
28882888
expect(prependContext).not.toContain("theta");
28892889
expect(prependContext).not.toContain("ignore this user text");
28902890
const lines = getActiveMemoryLines(sessionKey);
28912891
expectLinesToContain(lines, "🧩 Active Memory: status=timeout_partial");
2892-
expectLinesToContain(lines, "summary=35 chars");
2892+
expectLinesToContain(lines, "summary=40 chars");
28932893
expectLinesToContain(
28942894
lines,
2895-
"🔎 Active Memory Debug: timeout_partial: 35 chars recovered (not persisted)",
2895+
"🔎 Active Memory Debug: timeout_partial: 40 chars recovered (not persisted)",
28962896
);
28972897
expect(lines.join("\n")).not.toContain("alpha beta gamma delta");
28982898
});
@@ -5358,11 +5358,28 @@ describe("active-memory plugin", () => {
53585358

53595359
const prependContext = requirePrependContext(result);
53605360
expect(prependContext).toContain("alpha beta gamma");
5361-
expect(prependContext).toContain("alpha beta gamma delta epsilon");
5361+
expect(prependContext).toContain("alpha beta gamma delta epsilon");
53625362
expect(prependContext).not.toContain("zetalo");
53635363
expect(prependContext).not.toContain("zetalongword");
53645364
});
53655365

5366+
it("asks recall subagents to mark mutable operational facts stale unless source status is current", async () => {
5367+
await hooks.before_prompt_build(
5368+
{ prompt: "is autonomous pickup running?", messages: [] },
5369+
{
5370+
agentId: "main",
5371+
trigger: "user",
5372+
sessionKey: "agent:main:main",
5373+
messageProvider: "webchat",
5374+
},
5375+
);
5376+
5377+
const prompt = lastEmbeddedPrompt();
5378+
expect(prompt).toContain("Mutable operational facts");
5379+
expect(prompt).toContain("source timestamp");
5380+
expect(prompt).toContain("verify live");
5381+
});
5382+
53665383
it("uses the configured maxSummaryChars value in the subagent prompt", async () => {
53675384
api.pluginConfig = {
53685385
agents: ["main"],

extensions/active-memory/index.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,8 @@ function buildRecallPrompt(params: {
10901090
"Questions like 'what is my favorite food', 'do you remember my flight preferences', or 'what do i usually get' should normally return memory when relevant results exist.",
10911091
"If the provided conversation context already contains recalled-memory summaries, debug output, or prior memory/tool traces, ignore that surfaced text unless the latest user message clearly requires re-checking it.",
10921092
"Return memory only when it would materially help the other model answer the user's latest message.",
1093+
"Mutable operational facts (cron/job health, automation status, deployments, incidents, service availability) go stale quickly: include the source timestamp when available, say when the memory may be stale, and tell the answering model to verify live before relying on it.",
1094+
"Do not summarize mutable operational facts as simply current/running/healthy unless the memory result itself contains a current source timestamp and matching health state.",
10931095
"If the connection is weak, broad, or only vaguely related, reply with NONE.",
10941096
"If nothing clearly useful is found, reply with NONE.",
10951097
"Return exactly one of these two forms:",
@@ -2563,18 +2565,23 @@ function truncateSummary(summary: string, maxSummaryChars: number): string {
25632565
return trimmed;
25642566
}
25652567

2566-
const bounded = trimmed.slice(0, maxSummaryChars).trimEnd();
2567-
const nextChar = trimmed.charAt(maxSummaryChars);
2568+
const ellipsis = "…";
2569+
if (maxSummaryChars <= ellipsis.length) {
2570+
return ellipsis.slice(0, Math.max(0, maxSummaryChars));
2571+
}
2572+
const contentMaxChars = maxSummaryChars - ellipsis.length;
2573+
const bounded = trimmed.slice(0, contentMaxChars).trimEnd();
2574+
const nextChar = trimmed.charAt(contentMaxChars);
25682575
if (!nextChar || /\s/.test(nextChar)) {
2569-
return bounded;
2576+
return `${bounded}${ellipsis}`;
25702577
}
25712578

25722579
const lastBoundary = bounded.search(/\s\S*$/);
25732580
if (lastBoundary > 0) {
2574-
return bounded.slice(0, lastBoundary).trimEnd();
2581+
return `${bounded.slice(0, lastBoundary).trimEnd()}${ellipsis}`;
25752582
}
25762583

2577-
return bounded;
2584+
return `${bounded}${ellipsis}`;
25782585
}
25792586

25802587
function buildMetadata(summary: string | null): string | undefined {

src/agents/agent-project-settings.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,29 @@ describe("createPreparedEmbeddedAgentSettingsManager", () => {
186186
await fs.rm(baseDir, { recursive: true, force: true });
187187
}
188188
});
189+
190+
it("keeps compaction reserve overrides after disabling runtime retry", () => {
191+
const settingsManager = createPreparedEmbeddedAgentSettingsManager({
192+
cwd: "/tmp/workspace",
193+
agentDir: "/tmp/agent",
194+
cfg: {
195+
agents: {
196+
defaults: {
197+
compaction: {
198+
reserveTokensFloor: 50_000,
199+
keepRecentTokens: 16_000,
200+
},
201+
},
202+
},
203+
},
204+
contextTokenBudget: 200_000,
205+
});
206+
207+
expect(settingsManager.getRetryEnabled()).toBe(false);
208+
expect(settingsManager.getCompactionSettings()).toEqual({
209+
enabled: true,
210+
reserveTokens: 50_000,
211+
keepRecentTokens: 16_000,
212+
});
213+
});
189214
});

src/agents/agent-settings.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isSilentOverflowProneModel,
99
resolveEffectiveCompactionMode,
1010
} from "./agent-settings.js";
11+
import { SettingsManager } from "./sessions/settings-manager.js";
1112

1213
describe("applyAgentCompactionSettingsFromConfig", () => {
1314
it("bumps reserveTokens when below floor", () => {
@@ -518,6 +519,29 @@ describe("applyAgentAutoCompactionGuard", () => {
518519
expect(setCompactionEnabled).toHaveBeenCalledWith(false);
519520
});
520521

522+
it("preserves configured reserve tokens when disabling embedded auto-compaction", () => {
523+
const settingsManager = SettingsManager.inMemory({
524+
compaction: { enabled: true, reserveTokens: 16_384 },
525+
});
526+
527+
applyAgentCompactionSettingsFromConfig({
528+
settingsManager,
529+
cfg: { agents: { defaults: { compaction: { reserveTokensFloor: 50_000 } } } },
530+
contextTokenBudget: 200_000,
531+
});
532+
const result = applyAgentAutoCompactionGuard({
533+
settingsManager,
534+
compactionMode: "safeguard",
535+
});
536+
537+
expect(result).toEqual({ supported: true, disabled: true });
538+
expect(settingsManager.getCompactionSettings()).toEqual({
539+
enabled: false,
540+
reserveTokens: 50_000,
541+
keepRecentTokens: 20_000,
542+
});
543+
});
544+
521545
// Default-mode runs against ordinary providers must keep OpenClaw runtime's auto-compaction
522546
// enabled. Disabling it across the board would silently remove OpenClaw runtime's
523547
// overflow-recovery path inside Session.prompt() for users who are not
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
import type { SessionEntry } from "../../config/sessions/types.js";
4+
5+
const hoisted = vi.hoisted(() => ({
6+
store: {} as Record<string, SessionEntry>,
7+
}));
8+
9+
vi.mock("../../config/sessions/store-load.js", () => ({
10+
loadSessionStore: () => hoisted.store,
11+
}));
12+
13+
vi.mock("../../config/sessions/paths.js", () => ({
14+
resolveStorePath: () => "/stores/main.json",
15+
}));
16+
17+
const { resolveSession } = await import("./session.js");
18+
19+
const DAY_MS = 24 * 60 * 60 * 1000;
20+
21+
function seedProviderOwned(sessionKey: string): void {
22+
const startedAt = Date.now() - DAY_MS;
23+
hoisted.store = {
24+
[sessionKey]: {
25+
sessionId: "old-session-id",
26+
updatedAt: startedAt,
27+
sessionStartedAt: startedAt,
28+
lastInteractionAt: startedAt,
29+
model: "claude-opus-4-6",
30+
modelProvider: "claude-cli",
31+
cliSessionBindings: { "claude-cli": { sessionId: "cli-conversation-xyz" } },
32+
},
33+
};
34+
}
35+
36+
describe("command resolveSession provider-owned daily reset", () => {
37+
it("keeps a provider-owned CLI session across the default daily boundary", () => {
38+
const sessionKey = "agent:main:cli";
39+
seedProviderOwned(sessionKey);
40+
41+
const result = resolveSession({
42+
cfg: { session: {} } as OpenClawConfig,
43+
sessionKey,
44+
agentId: "main",
45+
});
46+
47+
expect(result.isNewSession).toBe(false);
48+
expect(result.sessionId).toBe("old-session-id");
49+
});
50+
51+
it("still rotates a non-provider-owned session across the daily boundary", () => {
52+
const sessionKey = "agent:main:cli";
53+
const startedAt = Date.now() - DAY_MS;
54+
hoisted.store = {
55+
[sessionKey]: {
56+
sessionId: "old-session-id",
57+
updatedAt: startedAt,
58+
sessionStartedAt: startedAt,
59+
lastInteractionAt: startedAt,
60+
},
61+
};
62+
63+
const result = resolveSession({
64+
cfg: { session: {} } as OpenClawConfig,
65+
sessionKey,
66+
agentId: "main",
67+
});
68+
69+
expect(result.isNewSession).toBe(true);
70+
expect(result.sessionId).not.toBe("old-session-id");
71+
});
72+
});

src/agents/command/session.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type ThinkLevel,
1010
type VerboseLevel,
1111
} from "../../auto-reply/thinking.js";
12+
import { hasProviderOwnedSession } from "../../config/sessions/entry-freshness.js";
1213
import {
1314
hasTerminalMainSessionTranscriptNewerThanRegistrySync,
1415
resolveSessionLifecycleTimestamps,
@@ -386,18 +387,21 @@ export function resolveSession(opts: {
386387
storePath,
387388
})
388389
: false;
390+
const skipImplicitExpiry =
391+
resetPolicy.configured !== true && hasProviderOwnedSession(sessionEntry);
389392
const fresh = sessionEntry
390393
? !terminalMainTranscriptNewerThanRegistry &&
391-
evaluateSessionFreshness({
392-
updatedAt: sessionEntry.updatedAt,
393-
...resolveSessionLifecycleTimestamps({
394-
entry: sessionEntry,
395-
agentId: sessionAgentId,
396-
storePath,
397-
}),
398-
now,
399-
policy: resetPolicy,
400-
}).fresh
394+
(skipImplicitExpiry ||
395+
evaluateSessionFreshness({
396+
updatedAt: sessionEntry.updatedAt,
397+
...resolveSessionLifecycleTimestamps({
398+
entry: sessionEntry,
399+
agentId: sessionAgentId,
400+
storePath,
401+
}),
402+
now,
403+
policy: resetPolicy,
404+
}).fresh)
401405
: false;
402406
const sessionId =
403407
requestedSessionId || (fresh ? sessionEntry?.sessionId : undefined) || crypto.randomUUID();

src/agents/embedded-agent-runner/thinking.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,18 @@ describe("wrapAnthropicStreamWithRecovery", () => {
737737
errorMessage: terminalThinkingSignatureError,
738738
}),
739739
},
740+
{
741+
name: "ProviderHttpError errorBody",
742+
createError: () =>
743+
Object.assign(new Error(genericizedProviderError), {
744+
errorBody: JSON.stringify({
745+
error: {
746+
message: terminalThinkingSignatureError,
747+
type: "invalid_request_error",
748+
},
749+
}),
750+
}),
751+
},
740752
{
741753
name: "cyclic cause graph",
742754
createError: () => {

src/agents/embedded-agent-runner/thinking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ function shouldRecoverAnthropicThinkingError(
545545
current.error,
546546
current.rawError,
547547
current.errorMessage,
548+
current.errorBody,
548549
current.message,
549550
]);
550551
for (const candidate of candidates) {

0 commit comments

Comments
 (0)