Skip to content

Commit 6bb6812

Browse files
authored
Merge branch 'main' into vincentkoc-code/fix-gateway-watch-stale-build
2 parents 764960f + 53462b9 commit 6bb6812

File tree

68 files changed

+2977
-248
lines changed

Some content is hidden

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

68 files changed

+2977
-248
lines changed

.agent/workflows/update_clawdbot.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
2-
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
2+
description: Update OpenClaw from upstream when branch has diverged (ahead/behind)
33
---
44

5-
# Clawdbot Upstream Sync Workflow
5+
# OpenClaw Upstream Sync Workflow
66

77
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
88

@@ -132,16 +132,16 @@ pnpm mac:package
132132

133133
```bash
134134
# Kill running app
135-
pkill -x "Clawdbot" || true
135+
pkill -x "OpenClaw" || true
136136

137137
# Move old version
138-
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
138+
mv /Applications/OpenClaw.app /tmp/OpenClaw-backup.app
139139

140140
# Install new build
141-
cp -R dist/Clawdbot.app /Applications/
141+
cp -R dist/OpenClaw.app /Applications/
142142

143143
# Launch
144-
open /Applications/Clawdbot.app
144+
open /Applications/OpenClaw.app
145145
```
146146

147147
---
@@ -235,7 +235,7 @@ If upstream introduced new model configurations:
235235
# Check for OpenRouter API key requirements
236236
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
237237

238-
# Update clawdbot.json with fallback chains
238+
# Update openclaw.json with fallback chains
239239
# Add model fallback configurations as needed
240240
```
241241

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
1313
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior.
1414
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
15+
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
1516

1617
### Fixes
1718

@@ -26,12 +27,24 @@ Docs: https://docs.openclaw.ai
2627
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
2728
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
2829
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
30+
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. Thanks @vincentkoc.
31+
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
32+
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
33+
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
34+
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
35+
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
36+
37+
### Fixes
38+
39+
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
2940
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
3041
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
3142
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
3243
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
3344
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
3445
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
46+
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
47+
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
3548

3649
## 2026.3.13
3750

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
<p align="center">
44
<picture>
5-
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
6-
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" width="500">
5+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.svg">
6+
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.svg" alt="OpenClaw" width="500">
77
</picture>
88
</p>
99

apps/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
android:maxSdkVersion="32" />
2020
<uses-permission android:name="android.permission.READ_CONTACTS" />
2121
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
22+
<uses-permission android:name="android.permission.READ_CALL_LOG" />
2223
<uses-permission android:name="android.permission.READ_CALENDAR" />
2324
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
2425
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ class NodeRuntime(context: Context) {
110110
appContext = appContext,
111111
)
112112

113+
private val callLogHandler: CallLogHandler = CallLogHandler(
114+
appContext = appContext,
115+
)
116+
113117
private val motionHandler: MotionHandler = MotionHandler(
114118
appContext = appContext,
115119
)
@@ -151,6 +155,7 @@ class NodeRuntime(context: Context) {
151155
smsHandler = smsHandlerImpl,
152156
a2uiHandler = a2uiHandler,
153157
debugHandler = debugHandler,
158+
callLogHandler = callLogHandler,
154159
isForeground = { _isForeground.value },
155160
cameraEnabled = { cameraEnabled.value },
156161
locationEnabled = { locationMode.value != LocationMode.Off },
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package ai.openclaw.app.node
2+
3+
import android.Manifest
4+
import android.content.Context
5+
import android.provider.CallLog
6+
import androidx.core.content.ContextCompat
7+
import ai.openclaw.app.gateway.GatewaySession
8+
import kotlinx.serialization.json.Json
9+
import kotlinx.serialization.json.JsonArray
10+
import kotlinx.serialization.json.JsonObject
11+
import kotlinx.serialization.json.JsonPrimitive
12+
import kotlinx.serialization.json.buildJsonObject
13+
import kotlinx.serialization.json.buildJsonArray
14+
import kotlinx.serialization.json.put
15+
16+
private const val DEFAULT_CALL_LOG_LIMIT = 25
17+
18+
internal data class CallLogRecord(
19+
val number: String?,
20+
val cachedName: String?,
21+
val date: Long,
22+
val duration: Long,
23+
val type: Int,
24+
)
25+
26+
internal data class CallLogSearchRequest(
27+
val limit: Int, // Number of records to return
28+
val offset: Int, // Offset value
29+
val cachedName: String?, // Search by contact name
30+
val number: String?, // Search by phone number
31+
val date: Long?, // Search by time (timestamp, deprecated, use dateStart/dateEnd)
32+
val dateStart: Long?, // Query start time (timestamp)
33+
val dateEnd: Long?, // Query end time (timestamp)
34+
val duration: Long?, // Search by duration (seconds)
35+
val type: Int?, // Search by call log type
36+
)
37+
38+
internal interface CallLogDataSource {
39+
fun hasReadPermission(context: Context): Boolean
40+
41+
fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord>
42+
}
43+
44+
private object SystemCallLogDataSource : CallLogDataSource {
45+
override fun hasReadPermission(context: Context): Boolean {
46+
return ContextCompat.checkSelfPermission(
47+
context,
48+
Manifest.permission.READ_CALL_LOG
49+
) == android.content.pm.PackageManager.PERMISSION_GRANTED
50+
}
51+
52+
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
53+
val resolver = context.contentResolver
54+
val projection = arrayOf(
55+
CallLog.Calls.NUMBER,
56+
CallLog.Calls.CACHED_NAME,
57+
CallLog.Calls.DATE,
58+
CallLog.Calls.DURATION,
59+
CallLog.Calls.TYPE,
60+
)
61+
62+
// Build selection and selectionArgs for filtering
63+
val selections = mutableListOf<String>()
64+
val selectionArgs = mutableListOf<String>()
65+
66+
request.cachedName?.let {
67+
selections.add("${CallLog.Calls.CACHED_NAME} LIKE ?")
68+
selectionArgs.add("%$it%")
69+
}
70+
71+
request.number?.let {
72+
selections.add("${CallLog.Calls.NUMBER} LIKE ?")
73+
selectionArgs.add("%$it%")
74+
}
75+
76+
// Support time range query
77+
if (request.dateStart != null && request.dateEnd != null) {
78+
selections.add("${CallLog.Calls.DATE} >= ? AND ${CallLog.Calls.DATE} <= ?")
79+
selectionArgs.add(request.dateStart.toString())
80+
selectionArgs.add(request.dateEnd.toString())
81+
} else if (request.dateStart != null) {
82+
selections.add("${CallLog.Calls.DATE} >= ?")
83+
selectionArgs.add(request.dateStart.toString())
84+
} else if (request.dateEnd != null) {
85+
selections.add("${CallLog.Calls.DATE} <= ?")
86+
selectionArgs.add(request.dateEnd.toString())
87+
} else if (request.date != null) {
88+
// Compatible with the old date parameter (exact match)
89+
selections.add("${CallLog.Calls.DATE} = ?")
90+
selectionArgs.add(request.date.toString())
91+
}
92+
93+
request.duration?.let {
94+
selections.add("${CallLog.Calls.DURATION} = ?")
95+
selectionArgs.add(it.toString())
96+
}
97+
98+
request.type?.let {
99+
selections.add("${CallLog.Calls.TYPE} = ?")
100+
selectionArgs.add(it.toString())
101+
}
102+
103+
val selection = if (selections.isNotEmpty()) selections.joinToString(" AND ") else null
104+
val selectionArgsArray = if (selectionArgs.isNotEmpty()) selectionArgs.toTypedArray() else null
105+
106+
val sortOrder = "${CallLog.Calls.DATE} DESC"
107+
108+
resolver.query(
109+
CallLog.Calls.CONTENT_URI,
110+
projection,
111+
selection,
112+
selectionArgsArray,
113+
sortOrder,
114+
).use { cursor ->
115+
if (cursor == null) return emptyList()
116+
117+
val numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER)
118+
val cachedNameIndex = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
119+
val dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE)
120+
val durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION)
121+
val typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE)
122+
123+
// Skip offset rows
124+
if (request.offset > 0 && cursor.moveToPosition(request.offset - 1)) {
125+
// Successfully moved to offset position
126+
}
127+
128+
val out = mutableListOf<CallLogRecord>()
129+
var count = 0
130+
while (cursor.moveToNext() && count < request.limit) {
131+
out += CallLogRecord(
132+
number = cursor.getString(numberIndex),
133+
cachedName = cursor.getString(cachedNameIndex),
134+
date = cursor.getLong(dateIndex),
135+
duration = cursor.getLong(durationIndex),
136+
type = cursor.getInt(typeIndex),
137+
)
138+
count++
139+
}
140+
return out
141+
}
142+
}
143+
}
144+
145+
class CallLogHandler private constructor(
146+
private val appContext: Context,
147+
private val dataSource: CallLogDataSource,
148+
) {
149+
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemCallLogDataSource)
150+
151+
fun handleCallLogSearch(paramsJson: String?): GatewaySession.InvokeResult {
152+
if (!dataSource.hasReadPermission(appContext)) {
153+
return GatewaySession.InvokeResult.error(
154+
code = "CALL_LOG_PERMISSION_REQUIRED",
155+
message = "CALL_LOG_PERMISSION_REQUIRED: grant Call Log permission",
156+
)
157+
}
158+
159+
val request = parseSearchRequest(paramsJson)
160+
?: return GatewaySession.InvokeResult.error(
161+
code = "INVALID_REQUEST",
162+
message = "INVALID_REQUEST: expected JSON object",
163+
)
164+
165+
return try {
166+
val callLogs = dataSource.search(appContext, request)
167+
GatewaySession.InvokeResult.ok(
168+
buildJsonObject {
169+
put(
170+
"callLogs",
171+
buildJsonArray {
172+
callLogs.forEach { add(callLogJson(it)) }
173+
},
174+
)
175+
}.toString(),
176+
)
177+
} catch (err: Throwable) {
178+
GatewaySession.InvokeResult.error(
179+
code = "CALL_LOG_UNAVAILABLE",
180+
message = "CALL_LOG_UNAVAILABLE: ${err.message ?: "call log query failed"}",
181+
)
182+
}
183+
}
184+
185+
private fun parseSearchRequest(paramsJson: String?): CallLogSearchRequest? {
186+
if (paramsJson.isNullOrBlank()) {
187+
return CallLogSearchRequest(
188+
limit = DEFAULT_CALL_LOG_LIMIT,
189+
offset = 0,
190+
cachedName = null,
191+
number = null,
192+
date = null,
193+
dateStart = null,
194+
dateEnd = null,
195+
duration = null,
196+
type = null,
197+
)
198+
}
199+
200+
val params = try {
201+
Json.parseToJsonElement(paramsJson).asObjectOrNull()
202+
} catch (_: Throwable) {
203+
null
204+
} ?: return null
205+
206+
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALL_LOG_LIMIT)
207+
.coerceIn(1, 200)
208+
val offset = ((params["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
209+
.coerceAtLeast(0)
210+
val cachedName = (params["cachedName"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
211+
val number = (params["number"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
212+
val date = (params["date"] as? JsonPrimitive)?.content?.toLongOrNull()
213+
val dateStart = (params["dateStart"] as? JsonPrimitive)?.content?.toLongOrNull()
214+
val dateEnd = (params["dateEnd"] as? JsonPrimitive)?.content?.toLongOrNull()
215+
val duration = (params["duration"] as? JsonPrimitive)?.content?.toLongOrNull()
216+
val type = (params["type"] as? JsonPrimitive)?.content?.toIntOrNull()
217+
218+
return CallLogSearchRequest(
219+
limit = limit,
220+
offset = offset,
221+
cachedName = cachedName,
222+
number = number,
223+
date = date,
224+
dateStart = dateStart,
225+
dateEnd = dateEnd,
226+
duration = duration,
227+
type = type,
228+
)
229+
}
230+
231+
private fun callLogJson(callLog: CallLogRecord): JsonObject {
232+
return buildJsonObject {
233+
put("number", JsonPrimitive(callLog.number))
234+
put("cachedName", JsonPrimitive(callLog.cachedName))
235+
put("date", JsonPrimitive(callLog.date))
236+
put("duration", JsonPrimitive(callLog.duration))
237+
put("type", JsonPrimitive(callLog.type))
238+
}
239+
}
240+
241+
companion object {
242+
internal fun forTesting(
243+
appContext: Context,
244+
dataSource: CallLogDataSource,
245+
): CallLogHandler = CallLogHandler(appContext = appContext, dataSource = dataSource)
246+
}
247+
}

apps/android/app/src/main/java/ai/openclaw/app/node/DeviceHandler.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ class DeviceHandler(
212212
promptableWhenDenied = true,
213213
),
214214
)
215+
put(
216+
"callLog",
217+
permissionStateJson(
218+
granted = hasPermission(Manifest.permission.READ_CALL_LOG),
219+
promptableWhenDenied = true,
220+
),
221+
)
215222
put(
216223
"motion",
217224
permissionStateJson(

apps/android/app/src/main/java/ai/openclaw/app/node/InvokeCommandRegistry.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
55
import ai.openclaw.app.protocol.OpenClawCanvasCommand
66
import ai.openclaw.app.protocol.OpenClawCameraCommand
77
import ai.openclaw.app.protocol.OpenClawCapability
8+
import ai.openclaw.app.protocol.OpenClawCallLogCommand
89
import ai.openclaw.app.protocol.OpenClawContactsCommand
910
import ai.openclaw.app.protocol.OpenClawDeviceCommand
1011
import ai.openclaw.app.protocol.OpenClawLocationCommand
@@ -84,6 +85,7 @@ object InvokeCommandRegistry {
8485
name = OpenClawCapability.Motion.rawValue,
8586
availability = NodeCapabilityAvailability.MotionAvailable,
8687
),
88+
NodeCapabilitySpec(name = OpenClawCapability.CallLog.rawValue),
8789
)
8890

8991
val all: List<InvokeCommandSpec> =
@@ -187,6 +189,9 @@ object InvokeCommandRegistry {
187189
name = OpenClawSmsCommand.Send.rawValue,
188190
availability = InvokeCommandAvailability.SmsAvailable,
189191
),
192+
InvokeCommandSpec(
193+
name = OpenClawCallLogCommand.Search.rawValue,
194+
),
190195
InvokeCommandSpec(
191196
name = "debug.logs",
192197
availability = InvokeCommandAvailability.DebugBuild,

0 commit comments

Comments
 (0)