Skip to content

Commit b3268d8

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents a129401 + 125db80 commit b3268d8

18 files changed

Lines changed: 824 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ Docs: https://docs.openclaw.ai
8989
- iMessage (imsg): strip an accidental protobuf length-delimited UTF-8 field wrapper from inbound `text` and `reply_to_text` when it fully consumes the field, fixing leading garbage before the real message. (#63868) Thanks @neeravmakwana.
9090
- Gateway/pairing: fail closed for paired device records that have no device tokens, and reject pairing approvals whose requested scopes do not match the requested device roles.
9191
- ACP/gateway chat: classify lifecycle errors before forwarding them to ACP clients so refusals use ACP's refusal stop reason while transient backend errors continue to finish as normal turns.
92+
- Commands/btw: keep tool-less side questions from sending injected empty `tools` arrays on strict OpenAI-compatible providers, so `/btw` continues working after prior tool-call history. (#64219) Thanks @ngutman.
93+
- Agents/Bedrock: let `/btw` side questions use `auth: "aws-sdk"` without a static API key so Bedrock IAM and instance-role sessions stop failing before the side question runs. (#64218) Thanks @SnowSky1.
9294

9395
## 2026.4.9
9496

@@ -136,6 +138,8 @@ Docs: https://docs.openclaw.ai
136138
- Plugins/contracts: keep test-only helpers out of production contract barrels, load shared contract harnesses through bundled test surfaces, and harden guardrails so indirect re-exports and canonical `*.test.ts` files stay blocked. (#63311) Thanks @altaywtf.
137139
- Control UI/models: preserve provider-qualified refs for OpenRouter catalog models whose ids already contain slashes so picker selections submit allowlist-compatible model refs instead of dropping the `openrouter/` prefix. (#63416) Thanks @sallyom.
138140
- Plugin SDK/command auth: split command status builders onto the lightweight `openclaw/plugin-sdk/command-status` subpath while preserving deprecated `command-auth` compatibility exports, so auth-only plugin imports no longer pull status/context warmup into CLI onboarding paths. (#63174) Thanks @hxy91819.
141+
- Wizard/plugin config: coerce integer-typed plugin config fields from interactive text input so integer schema values persist as numbers instead of failing validation. (#63346) Thanks @jalehman.
142+
- Dreaming/narrative: harden request-scoped diary fallback so scheduled dreaming only falls back on the dedicated subagent-runtime error, stop trusting spoofable raw error-code objects, and avoid leaking workspace paths when local fallback writes fail. (#64156) Thanks @mbelinky.
139143

140144
## 2026.4.8
141145

apps/macos/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4272,6 +4272,7 @@ public struct ChatEvent: Codable, Sendable {
42724272
public let state: AnyCodable
42734273
public let message: AnyCodable?
42744274
public let errormessage: String?
4275+
public let errorkind: AnyCodable?
42754276
public let usage: AnyCodable?
42764277
public let stopreason: String?
42774278

@@ -4282,6 +4283,7 @@ public struct ChatEvent: Codable, Sendable {
42824283
state: AnyCodable,
42834284
message: AnyCodable?,
42844285
errormessage: String?,
4286+
errorkind: AnyCodable?,
42854287
usage: AnyCodable?,
42864288
stopreason: String?)
42874289
{
@@ -4291,6 +4293,7 @@ public struct ChatEvent: Codable, Sendable {
42914293
self.state = state
42924294
self.message = message
42934295
self.errormessage = errormessage
4296+
self.errorkind = errorkind
42944297
self.usage = usage
42954298
self.stopreason = stopreason
42964299
}
@@ -4302,6 +4305,7 @@ public struct ChatEvent: Codable, Sendable {
43024305
case state
43034306
case message
43044307
case errormessage = "errorMessage"
4308+
case errorkind = "errorKind"
43054309
case usage
43064310
case stopreason = "stopReason"
43074311
}

apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4272,6 +4272,7 @@ public struct ChatEvent: Codable, Sendable {
42724272
public let state: AnyCodable
42734273
public let message: AnyCodable?
42744274
public let errormessage: String?
4275+
public let errorkind: AnyCodable?
42754276
public let usage: AnyCodable?
42764277
public let stopreason: String?
42774278

@@ -4282,6 +4283,7 @@ public struct ChatEvent: Codable, Sendable {
42824283
state: AnyCodable,
42834284
message: AnyCodable?,
42844285
errormessage: String?,
4286+
errorkind: AnyCodable?,
42854287
usage: AnyCodable?,
42864288
stopreason: String?)
42874289
{
@@ -4291,6 +4293,7 @@ public struct ChatEvent: Codable, Sendable {
42914293
self.state = state
42924294
self.message = message
42934295
self.errormessage = errormessage
4296+
self.errorkind = errorkind
42944297
self.usage = usage
42954298
self.stopreason = stopreason
42964299
}
@@ -4302,6 +4305,7 @@ public struct ChatEvent: Codable, Sendable {
43024305
case state
43034306
case message
43044307
case errormessage = "errorMessage"
4308+
case errorkind = "errorKind"
43054309
case usage
43064310
case stopreason = "stopReason"
43074311
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
087dc7fe9759330c953a00130ea20242b3d7f460eaa530d631cfb2a9f96e0370 plugin-sdk-api-baseline.json
2-
a84765a726e0493dc87d2799020fd454407b1fe2c4d3ad69e8c3cc3a0cde834b plugin-sdk-api-baseline.jsonl
1+
268aca42eaae8b4dd37d7eddb7202d002db16a4a27830cd90d98b5c4413cbbe7 plugin-sdk-api-baseline.json
2+
4fe4fc194bec72a58bdd5566c4b31c00b2c0a520941fdcdd0f42bdf02b683ea5 plugin-sdk-api-baseline.jsonl

docs/help/testing.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ Single-provider Docker recipes:
316316

317317
```bash
318318
pnpm test:docker:live-cli-backend:claude
319+
pnpm test:docker:live-cli-backend:claude-subscription
319320
pnpm test:docker:live-cli-backend:codex
320321
pnpm test:docker:live-cli-backend:gemini
321322
```
@@ -325,6 +326,7 @@ Notes:
325326
- The Docker runner lives at `scripts/test-live-cli-backend-docker.sh`.
326327
- It runs the live CLI-backend smoke inside the repo Docker image as the non-root `node` user.
327328
- It resolves CLI smoke metadata from the owning extension, then installs the matching Linux CLI package (`@anthropic-ai/claude-code`, `@openai/codex`, or `@google/gemini-cli`) into a cached writable prefix at `OPENCLAW_DOCKER_CLI_TOOLS_DIR` (default: `~/.cache/openclaw/docker-cli-tools`).
329+
- `pnpm test:docker:live-cli-backend:claude-subscription` requires portable Claude Code subscription OAuth through either `~/.claude/.credentials.json` with `claudeAiOauth.subscriptionType` or `CLAUDE_CODE_OAUTH_TOKEN` from `claude setup-token`. It first proves direct `claude -p` in Docker, then runs two Gateway CLI-backend turns without preserving Anthropic API-key env vars. This subscription lane disables the Claude MCP/tool and image probes by default because Claude currently routes third-party app usage through extra-usage billing instead of normal subscription plan limits.
328330
- The live CLI-backend smoke now exercises the same end-to-end flow for Claude, Codex, and Gemini: text turn, image classification turn, then MCP `cron` tool call verified through the gateway CLI.
329331
- Claude's default smoke also patches the session from Sonnet to Opus and verifies the resumed session still remembers an earlier note.
330332

@@ -669,6 +671,7 @@ Useful env vars:
669671
- Override manually with `OPENCLAW_DOCKER_AUTH_DIRS=all`, `OPENCLAW_DOCKER_AUTH_DIRS=none`, or a comma list like `OPENCLAW_DOCKER_AUTH_DIRS=.claude,.codex`
670672
- `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run
671673
- `OPENCLAW_LIVE_GATEWAY_PROVIDERS=...` / `OPENCLAW_LIVE_PROVIDERS=...` to filter providers in-container
674+
- `OPENCLAW_SKIP_DOCKER_BUILD=1` to reuse an existing `openclaw:local-live` image for reruns that do not need a rebuild
672675
- `OPENCLAW_LIVE_REQUIRE_PROFILE_KEYS=1` to ensure creds come from the profile store (not env)
673676
- `OPENCLAW_OPENWEBUI_MODEL=...` to choose the model exposed by the gateway for the Open WebUI smoke
674677
- `OPENCLAW_OPENWEBUI_PROMPT=...` to override the nonce-check prompt used by the Open WebUI smoke

extensions/memory-core/src/dreaming-narrative.test.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3+
import {
4+
RequestScopedSubagentRuntimeError,
5+
SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE,
6+
} from "openclaw/plugin-sdk/error-runtime";
37
import { afterEach, describe, expect, it, vi } from "vitest";
48
import {
59
appendNarrativeEntry,
@@ -477,7 +481,11 @@ describe("generateAndAppendDreamNarrative", () => {
477481
it("handles subagent error gracefully", async () => {
478482
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
479483
const subagent = createMockSubagent("");
480-
subagent.run.mockRejectedValue(new Error("connection failed"));
484+
subagent.run.mockRejectedValue(
485+
new Error("connection failed", {
486+
cause: new RequestScopedSubagentRuntimeError(),
487+
}),
488+
);
481489
const logger = createMockLogger();
482490

483491
await generateAndAppendDreamNarrative({
@@ -489,6 +497,80 @@ describe("generateAndAppendDreamNarrative", () => {
489497

490498
// Should not throw.
491499
expect(logger.warn).toHaveBeenCalled();
500+
await expect(fs.access(path.join(workspaceDir, "DREAMS.md"))).rejects.toMatchObject({
501+
code: "ENOENT",
502+
});
503+
});
504+
505+
it("falls back to a local narrative when subagent runtime is request-scoped", async () => {
506+
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
507+
const subagent = createMockSubagent("");
508+
subagent.run.mockRejectedValue(new RequestScopedSubagentRuntimeError());
509+
const logger = createMockLogger();
510+
511+
await generateAndAppendDreamNarrative({
512+
subagent,
513+
workspaceDir,
514+
data: { phase: "light", snippets: ["API endpoints need authentication"] },
515+
nowMs: Date.parse("2026-04-05T03:00:00Z"),
516+
timezone: "UTC",
517+
logger,
518+
});
519+
520+
const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
521+
expect(content).toContain("API endpoints need authentication");
522+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining("request-scoped"));
523+
expect(logger.warn).not.toHaveBeenCalledWith(expect.stringContaining(workspaceDir));
524+
expect(subagent.deleteSession).toHaveBeenCalledOnce();
525+
});
526+
527+
it("falls back when the request-scoped runtime error is detected by stable code", async () => {
528+
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
529+
const subagent = createMockSubagent("");
530+
const crossBoundaryError = new Error("different wrapper text");
531+
crossBoundaryError.name = "RequestScopedSubagentRuntimeError";
532+
Object.assign(crossBoundaryError, {
533+
code: SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE,
534+
});
535+
subagent.run.mockRejectedValue(crossBoundaryError);
536+
const logger = createMockLogger();
537+
538+
await generateAndAppendDreamNarrative({
539+
subagent,
540+
workspaceDir,
541+
data: { phase: "deep", snippets: [], promotions: ["A durable candidate surfaced."] },
542+
nowMs: Date.parse("2026-04-05T03:00:00Z"),
543+
timezone: "UTC",
544+
logger,
545+
});
546+
547+
const content = await fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8");
548+
expect(content).toContain("A durable candidate surfaced.");
549+
});
550+
551+
it("does not fall back for non-Error objects that only spoof the stable code", async () => {
552+
const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-");
553+
const subagent = createMockSubagent("");
554+
subagent.run.mockRejectedValue({
555+
code: SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE,
556+
name: "RequestScopedSubagentRuntimeError",
557+
message: "spoofed",
558+
});
559+
const logger = createMockLogger();
560+
561+
await generateAndAppendDreamNarrative({
562+
subagent,
563+
workspaceDir,
564+
data: { phase: "deep", snippets: ["should not persist"] },
565+
logger,
566+
});
567+
568+
await expect(fs.access(path.join(workspaceDir, "DREAMS.md"))).rejects.toMatchObject({
569+
code: "ENOENT",
570+
});
571+
expect(logger.warn).toHaveBeenCalledWith(
572+
expect.stringContaining("narrative generation failed"),
573+
);
492574
});
493575

494576
it("cleans up session even on failure", async () => {

extensions/memory-core/src/dreaming-narrative.ts

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3-
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3+
import {
4+
extractErrorCode,
5+
formatErrorMessage,
6+
RequestScopedSubagentRuntimeError,
7+
readErrorName,
8+
SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE,
9+
} from "openclaw/plugin-sdk/error-runtime";
410

511
// ── Types ──────────────────────────────────────────────────────────────
612

@@ -73,6 +79,80 @@ const DIARY_START_MARKER = "<!-- openclaw:dreaming:diary:start -->";
7379
const DIARY_END_MARKER = "<!-- openclaw:dreaming:diary:end -->";
7480
const BACKFILL_ENTRY_MARKER = "openclaw:dreaming:backfill-entry";
7581

82+
function isRequestScopedSubagentRuntimeError(err: unknown): boolean {
83+
return (
84+
err instanceof RequestScopedSubagentRuntimeError ||
85+
(err instanceof Error &&
86+
err.name === "RequestScopedSubagentRuntimeError" &&
87+
extractErrorCode(err) === SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE)
88+
);
89+
}
90+
91+
function formatFallbackWriteFailure(err: unknown): string {
92+
const code = extractErrorCode(err);
93+
const name = readErrorName(err);
94+
if (code && name) {
95+
return `code=${code} name=${name}`;
96+
}
97+
if (code) {
98+
return `code=${code}`;
99+
}
100+
if (name) {
101+
return `name=${name}`;
102+
}
103+
return "unknown error";
104+
}
105+
106+
function buildRequestScopedFallbackNarrative(data: NarrativePhaseData): string {
107+
return (
108+
data.snippets.map((value) => value.trim()).find((value) => value.length > 0) ??
109+
(data.promotions ?? []).map((value) => value.trim()).find((value) => value.length > 0) ??
110+
"A memory trace surfaced, but details were unavailable in this run."
111+
);
112+
}
113+
114+
async function startNarrativeRunOrFallback(params: {
115+
subagent: SubagentSurface;
116+
sessionKey: string;
117+
message: string;
118+
data: NarrativePhaseData;
119+
workspaceDir: string;
120+
nowMs: number;
121+
timezone?: string;
122+
logger: Logger;
123+
}): Promise<string | null> {
124+
try {
125+
const run = await params.subagent.run({
126+
idempotencyKey: params.sessionKey,
127+
sessionKey: params.sessionKey,
128+
message: params.message,
129+
extraSystemPrompt: NARRATIVE_SYSTEM_PROMPT,
130+
deliver: false,
131+
});
132+
return run.runId;
133+
} catch (runErr) {
134+
if (!isRequestScopedSubagentRuntimeError(runErr)) {
135+
throw runErr;
136+
}
137+
try {
138+
await appendNarrativeEntry({
139+
workspaceDir: params.workspaceDir,
140+
narrative: buildRequestScopedFallbackNarrative(params.data),
141+
nowMs: params.nowMs,
142+
timezone: params.timezone,
143+
});
144+
params.logger.warn(
145+
`memory-core: narrative generation used fallback for ${params.data.phase} phase because subagent runtime is request-scoped.`,
146+
);
147+
} catch (fallbackErr) {
148+
params.logger.warn(
149+
`memory-core: narrative fallback failed for ${params.data.phase} phase (${formatFallbackWriteFailure(fallbackErr)})`,
150+
);
151+
}
152+
return null;
153+
}
154+
}
155+
76156
// ── Prompt building ────────────────────────────────────────────────────
77157

78158
export function buildNarrativePrompt(data: NarrativePhaseData): string {
@@ -449,13 +529,19 @@ export async function generateAndAppendDreamNarrative(params: {
449529
const message = buildNarrativePrompt(params.data);
450530

451531
try {
452-
const { runId } = await params.subagent.run({
453-
idempotencyKey: sessionKey,
532+
const runId = await startNarrativeRunOrFallback({
533+
subagent: params.subagent,
454534
sessionKey,
455535
message,
456-
extraSystemPrompt: NARRATIVE_SYSTEM_PROMPT,
457-
deliver: false,
536+
data: params.data,
537+
workspaceDir: params.workspaceDir,
538+
nowMs,
539+
timezone: params.timezone,
540+
logger: params.logger,
458541
});
542+
if (!runId) {
543+
return;
544+
}
459545

460546
const result = await params.subagent.waitForRun({
461547
runId,

extensions/qqbot/src/config.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe("qqbot config", () => {
113113
expect(parsed.success).toBe(true);
114114
});
115115

116-
it("rejects account-level speech overrides that runtime does not consume", () => {
116+
it("accepts account-level speech overrides as forward-compatible config", () => {
117117
const parsed = QQBotConfigSchema.safeParse({
118118
accounts: {
119119
bot2: {
@@ -125,7 +125,7 @@ describe("qqbot config", () => {
125125
},
126126
});
127127

128-
expect(parsed.success).toBe(false);
128+
expect(parsed.success).toBe(true);
129129
});
130130

131131
it("preserves top-level media and upgrade config on the default account", () => {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,7 @@
12321232
"test:docker:live-build": "bash scripts/test-live-build-docker.sh",
12331233
"test:docker:live-cli-backend": "bash scripts/test-live-cli-backend-docker.sh",
12341234
"test:docker:live-cli-backend:claude": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 bash scripts/test-live-cli-backend-docker.sh",
1235+
"test:docker:live-cli-backend:claude-subscription": "OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6 OPENCLAW_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG=1 OPENCLAW_LIVE_CLI_BACKEND_MODEL_SWITCH_PROBE=0 OPENCLAW_LIVE_CLI_BACKEND_RESUME_PROBE=1 OPENCLAW_LIVE_CLI_BACKEND_IMAGE_PROBE=0 OPENCLAW_LIVE_CLI_BACKEND_MCP_PROBE=0 bash scripts/test-live-cli-backend-docker.sh",
12351236
"test:docker:live-cli-backend:codex": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4 bash scripts/test-live-cli-backend-docker.sh",
12361237
"test:docker:live-cli-backend:gemini": "OPENCLAW_LIVE_CLI_BACKEND_MODEL=google-gemini-cli/gemini-3-flash-preview bash scripts/test-live-cli-backend-docker.sh",
12371238
"test:docker:live-gateway": "bash scripts/test-live-gateway-models-docker.sh",

scripts/lib/live-docker-stage.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
openclaw_live_stage_source_tree() {
44
local dest_dir="${1:?destination directory required}"
55

6+
set +e
67
tar -C /src \
8+
--warning=no-file-changed \
9+
--ignore-failed-read \
710
--exclude=.git \
811
--exclude=node_modules \
912
--exclude=dist \
@@ -23,6 +26,11 @@ openclaw_live_stage_source_tree() {
2326
--exclude='apps/*/.kotlin' \
2427
--exclude='apps/*/build' \
2528
-cf - . | tar -C "$dest_dir" -xf -
29+
local status=$?
30+
set -e
31+
if [ "$status" -gt 1 ]; then
32+
return "$status"
33+
fi
2634
}
2735

2836
openclaw_live_link_runtime_tree() {

0 commit comments

Comments
 (0)