@clawling/clawchat-plugin-openclaw 2026.10.25-1 → 2026.10.27-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4630,9 +4630,8 @@ async function runOpenclawClawlingGateway(params, lifecycle) {
4630
4630
  // Synchronous and fire-and-forget on purpose: there is no ack to send (the
4631
4631
  // frame is BestEffort and non-ackable).
4632
4632
  //
4633
- // docs/client-integration.md §9.8 describes the permanent tombstone and
4634
- // actual synchronous write boundary; it does not establish a shared SQL
4635
- // transaction. See that section before changing recall/claim ordering.
4633
+ // docs/client-integration.md §9.8 describes the permanent tombstone and the
4634
+ // BEGIN IMMEDIATE transactions that serialize recall and claim.
4636
4635
  client.on("message:recall", (env) => {
4637
4636
  const target = recallTargetMessageId(env.payload);
4638
4637
  if (!target) {
@@ -65,7 +65,7 @@ export const OFFICIAL_SKILLS_BASE = "https://raw.githubusercontent.com/clawling/
65
65
  * in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
66
66
  * imports the same ref, so the `livewares` tree at that tag is pinned too.
67
67
  */
68
- export const DEFAULT_SKILLS_REF = "skills-v1.18.1";
68
+ export const DEFAULT_SKILLS_REF = "skills-v1.19.0";
69
69
  /** Refuse to treat an absurdly large response as a skill file (defence in depth). */
70
70
  export const MAX_SKILL_BYTES = 256 * 1024;
71
71
  /** This adapter's host target inside `skills/manifest.json`. */
@@ -1030,18 +1030,28 @@ export class ClawChatStore {
1030
1030
  }
1031
1031
  claimMessageOnce(input) {
1032
1032
  return this.write(() => {
1033
- // docs/client-integration.md §9.8: consult the recall tombstone on ingest.
1034
- // That section describes the synchronous write() boundary and the lack
1035
- // of a shared SQL transaction for the tombstone check and claim insert.
1036
- if (input.messageId && this.isMessageRecalled(input))
1037
- return false;
1038
- const result = this.requireDb()
1039
- .prepare(`INSERT OR IGNORE INTO clawchat_messages(
1040
- platform, account_id, kind, direction, event_type, trace_id, chat_id,
1041
- message_id, text, raw_json, created_at, send_status
1042
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1043
- .run(input.platform, input.accountId, input.kind, input.direction, input.eventType, input.traceId ?? null, input.chatId ?? null, input.messageId ?? null, input.text ?? null, toJson(input.raw), input.createdAt ?? Date.now(), input.direction === "outbound" ? "pending" : null);
1044
- return result.changes > 0;
1033
+ // docs/client-integration.md §9.8: the tombstone check and the claim
1034
+ // insert run in one BEGIN IMMEDIATE transaction.
1035
+ const db = this.requireDb();
1036
+ db.exec("BEGIN IMMEDIATE");
1037
+ try {
1038
+ if (input.messageId && this.isMessageRecalled(input)) {
1039
+ db.exec("COMMIT");
1040
+ return false;
1041
+ }
1042
+ const result = db
1043
+ .prepare(`INSERT OR IGNORE INTO clawchat_messages(
1044
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
1045
+ message_id, text, raw_json, created_at, send_status
1046
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1047
+ .run(input.platform, input.accountId, input.kind, input.direction, input.eventType, input.traceId ?? null, input.chatId ?? null, input.messageId ?? null, input.text ?? null, toJson(input.raw), input.createdAt ?? Date.now(), input.direction === "outbound" ? "pending" : null);
1048
+ db.exec("COMMIT");
1049
+ return result.changes > 0;
1050
+ }
1051
+ catch (error) {
1052
+ db.exec("ROLLBACK");
1053
+ throw error;
1054
+ }
1045
1055
  });
1046
1056
  }
1047
1057
  markMessageFailed(input) {
@@ -1144,8 +1154,8 @@ export class ClawChatStore {
1144
1154
  * account was paired, or one dropped by the inbound filter). `null` means the
1145
1155
  * store is unavailable.
1146
1156
  *
1147
- * Records the permanent recall tombstone and purges any row already stored;
1148
- * see docs/client-integration.md §9.8 for the actual write() boundary.
1157
+ * Records the permanent recall tombstone and deletes ledger rows in one
1158
+ * BEGIN IMMEDIATE transaction; see docs/client-integration.md §9.8.
1149
1159
  * Returns the number of ledger rows deleted (0 when the recall
1150
1160
  * arrived before the message did — the tombstone still stands and the later
1151
1161
  * ingest is refused by {@link claimMessageOnce}).
@@ -1157,13 +1167,21 @@ export class ClawChatStore {
1157
1167
  return 0;
1158
1168
  return this.write(() => {
1159
1169
  const db = this.requireDb();
1160
- db.prepare(`INSERT OR IGNORE INTO recalled_messages(platform, account_id, message_id, recalled_at)
1161
- VALUES (?, ?, ?, ?)`).run(input.platform, input.accountId, input.messageId, input.recalledAt ?? Date.now());
1162
- const result = db
1163
- .prepare(`DELETE FROM clawchat_messages
1164
- WHERE platform = ? AND account_id = ? AND message_id = ?`)
1165
- .run(input.platform, input.accountId, input.messageId);
1166
- return Number(result.changes);
1170
+ db.exec("BEGIN IMMEDIATE");
1171
+ try {
1172
+ db.prepare(`INSERT OR IGNORE INTO recalled_messages(platform, account_id, message_id, recalled_at)
1173
+ VALUES (?, ?, ?, ?)`).run(input.platform, input.accountId, input.messageId, input.recalledAt ?? Date.now());
1174
+ const result = db
1175
+ .prepare(`DELETE FROM clawchat_messages
1176
+ WHERE platform = ? AND account_id = ? AND message_id = ?`)
1177
+ .run(input.platform, input.accountId, input.messageId);
1178
+ db.exec("COMMIT");
1179
+ return Number(result.changes);
1180
+ }
1181
+ catch (error) {
1182
+ db.exec("ROLLBACK");
1183
+ throw error;
1184
+ }
1167
1185
  });
1168
1186
  }
1169
1187
  /**
@@ -1,30 +1,87 @@
1
1
  export const WIDGET_V1_GUIDE = `## Home-screen widgets (widget/v1)
2
2
 
3
- A widget is a small card on your partner's phone home screen. You write it with \`clawchat_widget_send\`; their taps come back to you as ordinary messages.
3
+ A widget is a small card on your partner's phone home screen. You write it with \`clawchat_widget_send\`; their taps come back to you as ordinary messages. It only ever goes to your private chat with them.
4
+
5
+ ### When to send
4
6
 
5
7
  - Send one only after your partner asked for a widget — a recipe card, or their own words.
6
- - Every message is the full snapshot, never a diff. The latest one per \`id\` wins.
7
- - At most one per \`id\` a minute; more calls in that minute collapse into the last one. Do not refresh on a timer: data that changes by itself belongs in \`source\`.
8
- - It only ever goes to your private chat with your partner.
8
+ - If they keep asking you for the same thing (a price, a list, a count), you may offer a widget once. Make it only after they say yes. Never offer the same one twice.
9
+
10
+ ### When to update
11
+
12
+ Send a new snapshot in the same turn whenever what it shows has changed:
13
+
14
+ - they tapped or typed on it (a \`[widget <id>] …\` message);
15
+ - they changed the same data in chat: "remind me to buy milk tomorrow" also goes onto the to-do widget;
16
+ - a new day began: "today", "tomorrow" or "N days left" in your last snapshot are now wrong. Send it again in your first turn of the day. If it counts days, schedule yourself a turn early every morning for it.
17
+
18
+ Nothing changed: send nothing. Every message is the full snapshot, never a diff; the latest one per \`id\` wins. At most one per \`id\` a minute. Data that changes by itself (prices, weather) belongs in \`source\`, not in repeated snapshots.
19
+
20
+ ### Design
21
+
22
+ - One widget answers one question. \`title\`: a few words. Never repeat it, the \`badge\` or the big number elsewhere; no "tap + to add" hints.
23
+ - Write \`small\`, \`medium\` and \`large\`, each from one layout below, with the same main figure. A missing size is stretched from another and looks broken.
24
+ - Name things in every size: a figure alone says nothing. Every text is one line. Per line: small about 7 Chinese characters (12 Latin), medium and large about 17 (30). Write it short; never rely on "…".
25
+ - Height is tight. \`small\` and \`medium\` fit exactly one of: \`[caption; number title; caption]\`, 4 lines of text, or a \`list\` of 3. A \`button\` counts as 2 lines; with a \`source\`, one line less (its footer). Anything taller is not drawn.
26
+ - \`large\` is twice as tall as \`medium\`: fill it with what medium had no room for (more items, a breakdown, the next ones). Never the medium tree alone, never a figure twice.
27
+ - One big \`number\` (\`style: title\`) per size; in \`small\` at most 4 characters ("81737" is 5: \`style: body\` there). \`unit\` is a unit only ("km", "kg", "%"); labels go in a \`caption\`.
28
+ - Supporting lines: \`caption\` with \`tone: secondary\`. Columns side by side get the same lines, so their tops line up. \`tone: accent\` in one place at most, only for what is urgent. An \`icon\` only when it tells items apart.
29
+ - A \`list\` holds things of one kind: write all of them, we show what fits and "N more" — only when the \`list\` is the whole size, not inside a \`column\`. Buttons and sentences never go in a \`list\`; only its first row may have a big number.
30
+ - \`action: capture\` only when they add items by typing (to-dos, expenses); then no \`button\` doing the same. A \`button\` only for one tap that changes something (check in, add a cup, draw again).
31
+ - \`badge\`: only the number of open items, and then no big number saying the same.
32
+ - Nothing that goes stale: dates ("10/24") and clock times ("last 8:20"), never "today", "tomorrow" or "50 min ago" (not in the \`title\` either), even if they ask "how long since": a widget has no running clock. A count for one day carries its date.
33
+ - Nothing left: an empty \`list\`; we draw the empty state.
34
+ - Every text in your partner's language, their items in their own words.
35
+
36
+ ### Layouts
37
+
38
+ Pick by what it shows. \`[a; b]\` is a \`column\`, \`(a | b)\` a \`row\`.
39
+
40
+ 1. One figure (spent this month, cups of water, km this week):
41
+ small \`[caption label; number title + unit; caption context]\` (with a \`source\`: drop the last caption), or with a tap \`[number title + unit; button]\`;
42
+ medium \`([caption label; number title + unit; caption context] | X)\`, X is one thing: a \`ring\` for a goal, a \`button\`, or \`[caption; number body]\`. Never two things stacked beside it;
43
+ large \`[the medium tree; list of up to 5 (text grow | number body)]\`: a breakdown or the last entries.
44
+ 2. Several things, each with a figure or a time (prices, rates, books, a schedule, countdowns): every size is a \`list\`, one row per thing, the nearest or most important first.
45
+ first row \`(text name grow | number title + unit)\`, the rest \`(text name grow | number body + unit)\` or \`(text name grow | caption time)\`;
46
+ small: the same rows with short names. A \`list\` grows with taller widgets and counts "N more"; a \`column\` does not.
47
+ 3. Things to tick (to-dos, shopping, habits): see the document below.
48
+ 4. Words (a fortune, a word of the day): one \`column\`, one text per line, never side by side in a \`row\`.
49
+ small \`[text title; caption; caption]\`; medium \`[text title; text body; caption; caption]\`, or \`[(text title grow | button); text body; caption]\` with a tap; large: up to 4 more \`caption\` lines. Split long sentences.
50
+
51
+ ### Removing one
52
+
53
+ Asked to remove a widget: send \`{"v": 1, "id": "todo", "removed": true}\` with its \`id\`. It leaves their widget list; a copy on the home screen shows "deleted" until they long-press it away. \`[widget todo] removed\` means they deleted it themselves. Either way, stop sending it. A later snapshot with that \`id\` brings it back, so send one only if they ask again.
9
54
 
10
55
  ### Document
11
56
 
12
57
  \`\`\`json
13
- {"v": 1, "id": "todo", "title": "To-do", "badge": 3,
58
+ {"v": 1, "id": "todo", "title": "To-do", "badge": 2,
14
59
  "action": {"do": "capture"},
60
+ "small": {"type": "list", "items": [
61
+ {"type": "row", "children": [
62
+ {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
63
+ {"type": "text", "text": "Pay rent", "grow": true}]},
64
+ {"type": "row", "children": [
65
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
66
+ {"type": "text", "text": "Call mom", "grow": true}]}]},
15
67
  "medium": {"type": "list", "items": [
16
68
  {"type": "row", "children": [
17
69
  {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
18
70
  {"type": "text", "text": "Pay rent", "grow": true},
19
- {"type": "text", "text": "today", "style": "caption", "tone": "accent"}]}]}}
71
+ {"type": "text", "text": "10/24", "style": "caption", "tone": "accent"}]},
72
+ {"type": "row", "children": [
73
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
74
+ {"type": "text", "text": "Call mom", "grow": true}]}]}}
20
75
  \`\`\`
21
76
 
77
+ \`large\`: the same \`list\` as \`medium\`. Always list every item; we show what fits and "N more".
78
+
22
79
  - \`v\`: 1. \`id\`: \`^[a-z0-9][a-z0-9_-]{0,31}$\`, the same across updates. \`title\`: up to 40 characters.
23
80
  - \`badge\` (optional): an integer 0–9999, or up to 12 characters.
24
81
  - \`action\` (optional, top right): \`capture\`, \`chat\` or \`liveware\`.
25
82
  - \`small\` / \`medium\` / \`large\`: one block tree per size, at least one. A missing size borrows \`medium\`, then \`large\`, then \`small\`. A medium list shows about 3 rows.
26
83
  - At most 16 KiB of UTF-8. Unknown fields and block types are skipped.
27
- - Write the text in your partner's language. Colours, fonts and spacing are ours; you choose the structure.
84
+ - Colours, fonts and spacing are ours; you choose the structure.
28
85
 
29
86
  ### Blocks
30
87
 
@@ -59,7 +116,7 @@ A widget is a small card on your partner's phone home screen. You write it with
59
116
  "source": {"url": "https://…", "every": "30m", "fields": {"price": "data.price"}}
60
117
  \`\`\`
61
118
 
62
- The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. Use the field as \`{price}\` in \`text\` or \`number\`. Your partner sees the domain.
119
+ The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. A field is a dot path into that JSON, array items by number (\`data.items.0.price\`); each step is letters, digits or \`_\` only, so a key like \`xnet-1\` cannot be read. Field names: lowercase letters, digits and \`_\`; every \`{name}\` you write must be one of them. Write \`{price}\` in \`text\` or \`number\` where the value goes: a value you type in yourself goes stale. Your partner sees the domain.
63
120
 
64
121
  ### What comes back
65
122
 
@@ -73,6 +130,7 @@ Each tap or entry arrives as an ordinary message with one line for you:
73
130
  [widget todo] pressed "Refresh" (b1)
74
131
  [widget todo] capture: buy milk
75
132
  [widget todo] resend
133
+ [widget todo] removed
76
134
  \`\`\`
77
135
 
78
136
  They can arrive late and several at once: apply each in order, they are not duplicates. Then send one new full snapshot. \`resend\` means a device lost the widget: send the latest snapshot again.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.10.25-1",
3
+ "version": "2026.10.27-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clawchat-core
3
- version: 1.12.1
3
+ version: 1.13.0
4
4
  description: Use when a request involves ClawChat profile, friends, user search, moments/dynamics, comments, reactions, avatar, media, memory, output visibility, read-only conversation lookup, sending an image, video, file, or voice/audio clip into a conversation, managing the owner's other agents or groups, updating the owner's home-screen widgets, or plugin install/update/activation.
5
5
  ---
6
6
 
@@ -196,31 +196,88 @@ Do not invent invite codes, tokens, moment ids, comment ids, user ids, emoji rea
196
196
 
197
197
  ## Home-screen widgets (widget/v1)
198
198
 
199
- A widget is a small card on your partner's phone home screen. You write it with `clawchat_widget_send`; their taps come back to you as ordinary messages.
199
+ A widget is a small card on your partner's phone home screen. You write it with `clawchat_widget_send`; their taps come back to you as ordinary messages. It only ever goes to your private chat with them.
200
+
201
+ ### When to send
200
202
 
201
203
  - Send one only after your partner asked for a widget — a recipe card, or their own words.
202
- - Every message is the full snapshot, never a diff. The latest one per `id` wins.
203
- - At most one per `id` a minute; more calls in that minute collapse into the last one. Do not refresh on a timer: data that changes by itself belongs in `source`.
204
- - It only ever goes to your private chat with your partner.
204
+ - If they keep asking you for the same thing (a price, a list, a count), you may offer a widget once. Make it only after they say yes. Never offer the same one twice.
205
+
206
+ ### When to update
207
+
208
+ Send a new snapshot in the same turn whenever what it shows has changed:
209
+
210
+ - they tapped or typed on it (a `[widget <id>] …` message);
211
+ - they changed the same data in chat: "remind me to buy milk tomorrow" also goes onto the to-do widget;
212
+ - a new day began: "today", "tomorrow" or "N days left" in your last snapshot are now wrong. Send it again in your first turn of the day. If it counts days, schedule yourself a turn early every morning for it.
213
+
214
+ Nothing changed: send nothing. Every message is the full snapshot, never a diff; the latest one per `id` wins. At most one per `id` a minute. Data that changes by itself (prices, weather) belongs in `source`, not in repeated snapshots.
215
+
216
+ ### Design
217
+
218
+ - One widget answers one question. `title`: a few words. Never repeat it, the `badge` or the big number elsewhere; no "tap + to add" hints.
219
+ - Write `small`, `medium` and `large`, each from one layout below, with the same main figure. A missing size is stretched from another and looks broken.
220
+ - Name things in every size: a figure alone says nothing. Every text is one line. Per line: small about 7 Chinese characters (12 Latin), medium and large about 17 (30). Write it short; never rely on "…".
221
+ - Height is tight. `small` and `medium` fit exactly one of: `[caption; number title; caption]`, 4 lines of text, or a `list` of 3. A `button` counts as 2 lines; with a `source`, one line less (its footer). Anything taller is not drawn.
222
+ - `large` is twice as tall as `medium`: fill it with what medium had no room for (more items, a breakdown, the next ones). Never the medium tree alone, never a figure twice.
223
+ - One big `number` (`style: title`) per size; in `small` at most 4 characters ("81737" is 5: `style: body` there). `unit` is a unit only ("km", "kg", "%"); labels go in a `caption`.
224
+ - Supporting lines: `caption` with `tone: secondary`. Columns side by side get the same lines, so their tops line up. `tone: accent` in one place at most, only for what is urgent. An `icon` only when it tells items apart.
225
+ - A `list` holds things of one kind: write all of them, we show what fits and "N more" — only when the `list` is the whole size, not inside a `column`. Buttons and sentences never go in a `list`; only its first row may have a big number.
226
+ - `action: capture` only when they add items by typing (to-dos, expenses); then no `button` doing the same. A `button` only for one tap that changes something (check in, add a cup, draw again).
227
+ - `badge`: only the number of open items, and then no big number saying the same.
228
+ - Nothing that goes stale: dates ("10/24") and clock times ("last 8:20"), never "today", "tomorrow" or "50 min ago" (not in the `title` either), even if they ask "how long since": a widget has no running clock. A count for one day carries its date.
229
+ - Nothing left: an empty `list`; we draw the empty state.
230
+ - Every text in your partner's language, their items in their own words.
231
+
232
+ ### Layouts
233
+
234
+ Pick by what it shows. `[a; b]` is a `column`, `(a | b)` a `row`.
235
+
236
+ 1. One figure (spent this month, cups of water, km this week):
237
+ small `[caption label; number title + unit; caption context]` (with a `source`: drop the last caption), or with a tap `[number title + unit; button]`;
238
+ medium `([caption label; number title + unit; caption context] | X)`, X is one thing: a `ring` for a goal, a `button`, or `[caption; number body]`. Never two things stacked beside it;
239
+ large `[the medium tree; list of up to 5 (text grow | number body)]`: a breakdown or the last entries.
240
+ 2. Several things, each with a figure or a time (prices, rates, books, a schedule, countdowns): every size is a `list`, one row per thing, the nearest or most important first.
241
+ first row `(text name grow | number title + unit)`, the rest `(text name grow | number body + unit)` or `(text name grow | caption time)`;
242
+ small: the same rows with short names. A `list` grows with taller widgets and counts "N more"; a `column` does not.
243
+ 3. Things to tick (to-dos, shopping, habits): see the document below.
244
+ 4. Words (a fortune, a word of the day): one `column`, one text per line, never side by side in a `row`.
245
+ small `[text title; caption; caption]`; medium `[text title; text body; caption; caption]`, or `[(text title grow | button); text body; caption]` with a tap; large: up to 4 more `caption` lines. Split long sentences.
246
+
247
+ ### Removing one
248
+
249
+ Asked to remove a widget: send `{"v": 1, "id": "todo", "removed": true}` with its `id`. It leaves their widget list; a copy on the home screen shows "deleted" until they long-press it away. `[widget todo] removed` means they deleted it themselves. Either way, stop sending it. A later snapshot with that `id` brings it back, so send one only if they ask again.
205
250
 
206
251
  ### Document
207
252
 
208
253
  ```json
209
- {"v": 1, "id": "todo", "title": "To-do", "badge": 3,
254
+ {"v": 1, "id": "todo", "title": "To-do", "badge": 2,
210
255
  "action": {"do": "capture"},
256
+ "small": {"type": "list", "items": [
257
+ {"type": "row", "children": [
258
+ {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
259
+ {"type": "text", "text": "Pay rent", "grow": true}]},
260
+ {"type": "row", "children": [
261
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
262
+ {"type": "text", "text": "Call mom", "grow": true}]}]},
211
263
  "medium": {"type": "list", "items": [
212
264
  {"type": "row", "children": [
213
265
  {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
214
266
  {"type": "text", "text": "Pay rent", "grow": true},
215
- {"type": "text", "text": "today", "style": "caption", "tone": "accent"}]}]}}
267
+ {"type": "text", "text": "10/24", "style": "caption", "tone": "accent"}]},
268
+ {"type": "row", "children": [
269
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
270
+ {"type": "text", "text": "Call mom", "grow": true}]}]}}
216
271
  ```
217
272
 
273
+ `large`: the same `list` as `medium`. Always list every item; we show what fits and "N more".
274
+
218
275
  - `v`: 1. `id`: `^[a-z0-9][a-z0-9_-]{0,31}$`, the same across updates. `title`: up to 40 characters.
219
276
  - `badge` (optional): an integer 0–9999, or up to 12 characters.
220
277
  - `action` (optional, top right): `capture`, `chat` or `liveware`.
221
278
  - `small` / `medium` / `large`: one block tree per size, at least one. A missing size borrows `medium`, then `large`, then `small`. A medium list shows about 3 rows.
222
279
  - At most 16 KiB of UTF-8. Unknown fields and block types are skipped.
223
- - Write the text in your partner's language. Colours, fonts and spacing are ours; you choose the structure.
280
+ - Colours, fonts and spacing are ours; you choose the structure.
224
281
 
225
282
  ### Blocks
226
283
 
@@ -255,7 +312,7 @@ A widget is a small card on your partner's phone home screen. You write it with
255
312
  "source": {"url": "https://…", "every": "30m", "fields": {"price": "data.price"}}
256
313
  ```
257
314
 
258
- The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. Use the field as `{price}` in `text` or `number`. Your partner sees the domain.
315
+ The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. A field is a dot path into that JSON, array items by number (`data.items.0.price`); each step is letters, digits or `_` only, so a key like `xnet-1` cannot be read. Field names: lowercase letters, digits and `_`; every `{name}` you write must be one of them. Write `{price}` in `text` or `number` where the value goes: a value you type in yourself goes stale. Your partner sees the domain.
259
316
 
260
317
  ### What comes back
261
318
 
@@ -269,6 +326,7 @@ Each tap or entry arrives as an ordinary message with one line for you:
269
326
  [widget todo] pressed "Refresh" (b1)
270
327
  [widget todo] capture: buy milk
271
328
  [widget todo] resend
329
+ [widget todo] removed
272
330
  ```
273
331
 
274
332
  They can arrive late and several at once: apply each in order, they are not duplicates. Then send one new full snapshot. `resend` means a device lost the widget: send the latest snapshot again.
@@ -3,10 +3,10 @@
3
3
  "skills": {
4
4
  "openclaw": {
5
5
  "clawchat-core": {
6
- "version": "1.12.1",
6
+ "version": "1.13.0",
7
7
  "path": "openclaw/clawchat-core/SKILL.md",
8
- "sha256": "6d158352113150af99692901c729b6857ca62687144755ed03139dac5d3f7ead",
9
- "bytes": 21797
8
+ "sha256": "534096815b7216c180c573cd6917e1b77fb244affaf283fc750a7760d04946d2",
9
+ "bytes": 27385
10
10
  },
11
11
  "clawchat-liveware": {
12
12
  "version": "1.2.4",
@@ -41,10 +41,10 @@
41
41
  },
42
42
  "hermes": {
43
43
  "clawchat-core": {
44
- "version": "1.18.1",
44
+ "version": "1.19.0",
45
45
  "path": "hermes/clawchat-core/SKILL.md",
46
- "sha256": "ddb28ca5857dd9d023d7aa986da2440c87f1c4f496fad16c88e8e9552e33c519",
47
- "bytes": 32550
46
+ "sha256": "6da45c16ed831512b11b5ee478961d7deb0a9c23fd8c244cfd181409ba2ac253",
47
+ "bytes": 38138
48
48
  },
49
49
  "clawchat-liveware": {
50
50
  "version": "1.3.1",
package/src/runtime.ts CHANGED
@@ -5132,9 +5132,8 @@ async function runOpenclawClawlingGateway(
5132
5132
  // Synchronous and fire-and-forget on purpose: there is no ack to send (the
5133
5133
  // frame is BestEffort and non-ackable).
5134
5134
  //
5135
- // docs/client-integration.md §9.8 describes the permanent tombstone and
5136
- // actual synchronous write boundary; it does not establish a shared SQL
5137
- // transaction. See that section before changing recall/claim ordering.
5135
+ // docs/client-integration.md §9.8 describes the permanent tombstone and the
5136
+ // BEGIN IMMEDIATE transactions that serialize recall and claim.
5138
5137
  client.on("message:recall", (env: Envelope) => {
5139
5138
  const target = recallTargetMessageId(env.payload);
5140
5139
  if (!target) {
@@ -70,7 +70,7 @@ export const OFFICIAL_SKILLS_BASE =
70
70
  * in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
71
71
  * imports the same ref, so the `livewares` tree at that tag is pinned too.
72
72
  */
73
- export const DEFAULT_SKILLS_REF = "skills-v1.18.1";
73
+ export const DEFAULT_SKILLS_REF = "skills-v1.19.0";
74
74
 
75
75
  /** Refuse to treat an absurdly large response as a skill file (defence in depth). */
76
76
  export const MAX_SKILL_BYTES = 256 * 1024;
package/src/storage.ts CHANGED
@@ -1324,32 +1324,42 @@ export class ClawChatStore {
1324
1324
 
1325
1325
  claimMessageOnce(input: MessageInput): true | false | null {
1326
1326
  return this.write(() => {
1327
- // docs/client-integration.md §9.8: consult the recall tombstone on ingest.
1328
- // That section describes the synchronous write() boundary and the lack
1329
- // of a shared SQL transaction for the tombstone check and claim insert.
1330
- if (input.messageId && this.isMessageRecalled(input)) return false;
1331
- const result = this.requireDb()
1332
- .prepare(
1333
- `INSERT OR IGNORE INTO clawchat_messages(
1334
- platform, account_id, kind, direction, event_type, trace_id, chat_id,
1335
- message_id, text, raw_json, created_at, send_status
1336
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1337
- )
1338
- .run(
1339
- input.platform,
1340
- input.accountId,
1341
- input.kind,
1342
- input.direction,
1343
- input.eventType,
1344
- input.traceId ?? null,
1345
- input.chatId ?? null,
1346
- input.messageId ?? null,
1347
- input.text ?? null,
1348
- toJson(input.raw),
1349
- input.createdAt ?? Date.now(),
1350
- input.direction === "outbound" ? "pending" : null,
1351
- );
1352
- return result.changes > 0;
1327
+ // docs/client-integration.md §9.8: the tombstone check and the claim
1328
+ // insert run in one BEGIN IMMEDIATE transaction.
1329
+ const db = this.requireDb();
1330
+ db.exec("BEGIN IMMEDIATE");
1331
+ try {
1332
+ if (input.messageId && this.isMessageRecalled(input)) {
1333
+ db.exec("COMMIT");
1334
+ return false;
1335
+ }
1336
+ const result = db
1337
+ .prepare(
1338
+ `INSERT OR IGNORE INTO clawchat_messages(
1339
+ platform, account_id, kind, direction, event_type, trace_id, chat_id,
1340
+ message_id, text, raw_json, created_at, send_status
1341
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1342
+ )
1343
+ .run(
1344
+ input.platform,
1345
+ input.accountId,
1346
+ input.kind,
1347
+ input.direction,
1348
+ input.eventType,
1349
+ input.traceId ?? null,
1350
+ input.chatId ?? null,
1351
+ input.messageId ?? null,
1352
+ input.text ?? null,
1353
+ toJson(input.raw),
1354
+ input.createdAt ?? Date.now(),
1355
+ input.direction === "outbound" ? "pending" : null,
1356
+ );
1357
+ db.exec("COMMIT");
1358
+ return result.changes > 0;
1359
+ } catch (error) {
1360
+ db.exec("ROLLBACK");
1361
+ throw error;
1362
+ }
1353
1363
  });
1354
1364
  }
1355
1365
 
@@ -1506,8 +1516,8 @@ export class ClawChatStore {
1506
1516
  * account was paired, or one dropped by the inbound filter). `null` means the
1507
1517
  * store is unavailable.
1508
1518
  *
1509
- * Records the permanent recall tombstone and purges any row already stored;
1510
- * see docs/client-integration.md §9.8 for the actual write() boundary.
1519
+ * Records the permanent recall tombstone and deletes ledger rows in one
1520
+ * BEGIN IMMEDIATE transaction; see docs/client-integration.md §9.8.
1511
1521
  * Returns the number of ledger rows deleted (0 when the recall
1512
1522
  * arrived before the message did — the tombstone still stands and the later
1513
1523
  * ingest is refused by {@link claimMessageOnce}).
@@ -1523,17 +1533,24 @@ export class ClawChatStore {
1523
1533
  if (!input.messageId) return 0;
1524
1534
  return this.write(() => {
1525
1535
  const db = this.requireDb();
1526
- db.prepare(
1527
- `INSERT OR IGNORE INTO recalled_messages(platform, account_id, message_id, recalled_at)
1528
- VALUES (?, ?, ?, ?)`,
1529
- ).run(input.platform, input.accountId, input.messageId, input.recalledAt ?? Date.now());
1530
- const result = db
1531
- .prepare(
1532
- `DELETE FROM clawchat_messages
1533
- WHERE platform = ? AND account_id = ? AND message_id = ?`,
1534
- )
1535
- .run(input.platform, input.accountId, input.messageId);
1536
- return Number(result.changes);
1536
+ db.exec("BEGIN IMMEDIATE");
1537
+ try {
1538
+ db.prepare(
1539
+ `INSERT OR IGNORE INTO recalled_messages(platform, account_id, message_id, recalled_at)
1540
+ VALUES (?, ?, ?, ?)`,
1541
+ ).run(input.platform, input.accountId, input.messageId, input.recalledAt ?? Date.now());
1542
+ const result = db
1543
+ .prepare(
1544
+ `DELETE FROM clawchat_messages
1545
+ WHERE platform = ? AND account_id = ? AND message_id = ?`,
1546
+ )
1547
+ .run(input.platform, input.accountId, input.messageId);
1548
+ db.exec("COMMIT");
1549
+ return Number(result.changes);
1550
+ } catch (error) {
1551
+ db.exec("ROLLBACK");
1552
+ throw error;
1553
+ }
1537
1554
  });
1538
1555
  }
1539
1556
 
@@ -1,30 +1,87 @@
1
1
  export const WIDGET_V1_GUIDE = `## Home-screen widgets (widget/v1)
2
2
 
3
- A widget is a small card on your partner's phone home screen. You write it with \`clawchat_widget_send\`; their taps come back to you as ordinary messages.
3
+ A widget is a small card on your partner's phone home screen. You write it with \`clawchat_widget_send\`; their taps come back to you as ordinary messages. It only ever goes to your private chat with them.
4
+
5
+ ### When to send
4
6
 
5
7
  - Send one only after your partner asked for a widget — a recipe card, or their own words.
6
- - Every message is the full snapshot, never a diff. The latest one per \`id\` wins.
7
- - At most one per \`id\` a minute; more calls in that minute collapse into the last one. Do not refresh on a timer: data that changes by itself belongs in \`source\`.
8
- - It only ever goes to your private chat with your partner.
8
+ - If they keep asking you for the same thing (a price, a list, a count), you may offer a widget once. Make it only after they say yes. Never offer the same one twice.
9
+
10
+ ### When to update
11
+
12
+ Send a new snapshot in the same turn whenever what it shows has changed:
13
+
14
+ - they tapped or typed on it (a \`[widget <id>] …\` message);
15
+ - they changed the same data in chat: "remind me to buy milk tomorrow" also goes onto the to-do widget;
16
+ - a new day began: "today", "tomorrow" or "N days left" in your last snapshot are now wrong. Send it again in your first turn of the day. If it counts days, schedule yourself a turn early every morning for it.
17
+
18
+ Nothing changed: send nothing. Every message is the full snapshot, never a diff; the latest one per \`id\` wins. At most one per \`id\` a minute. Data that changes by itself (prices, weather) belongs in \`source\`, not in repeated snapshots.
19
+
20
+ ### Design
21
+
22
+ - One widget answers one question. \`title\`: a few words. Never repeat it, the \`badge\` or the big number elsewhere; no "tap + to add" hints.
23
+ - Write \`small\`, \`medium\` and \`large\`, each from one layout below, with the same main figure. A missing size is stretched from another and looks broken.
24
+ - Name things in every size: a figure alone says nothing. Every text is one line. Per line: small about 7 Chinese characters (12 Latin), medium and large about 17 (30). Write it short; never rely on "…".
25
+ - Height is tight. \`small\` and \`medium\` fit exactly one of: \`[caption; number title; caption]\`, 4 lines of text, or a \`list\` of 3. A \`button\` counts as 2 lines; with a \`source\`, one line less (its footer). Anything taller is not drawn.
26
+ - \`large\` is twice as tall as \`medium\`: fill it with what medium had no room for (more items, a breakdown, the next ones). Never the medium tree alone, never a figure twice.
27
+ - One big \`number\` (\`style: title\`) per size; in \`small\` at most 4 characters ("81737" is 5: \`style: body\` there). \`unit\` is a unit only ("km", "kg", "%"); labels go in a \`caption\`.
28
+ - Supporting lines: \`caption\` with \`tone: secondary\`. Columns side by side get the same lines, so their tops line up. \`tone: accent\` in one place at most, only for what is urgent. An \`icon\` only when it tells items apart.
29
+ - A \`list\` holds things of one kind: write all of them, we show what fits and "N more" — only when the \`list\` is the whole size, not inside a \`column\`. Buttons and sentences never go in a \`list\`; only its first row may have a big number.
30
+ - \`action: capture\` only when they add items by typing (to-dos, expenses); then no \`button\` doing the same. A \`button\` only for one tap that changes something (check in, add a cup, draw again).
31
+ - \`badge\`: only the number of open items, and then no big number saying the same.
32
+ - Nothing that goes stale: dates ("10/24") and clock times ("last 8:20"), never "today", "tomorrow" or "50 min ago" (not in the \`title\` either), even if they ask "how long since": a widget has no running clock. A count for one day carries its date.
33
+ - Nothing left: an empty \`list\`; we draw the empty state.
34
+ - Every text in your partner's language, their items in their own words.
35
+
36
+ ### Layouts
37
+
38
+ Pick by what it shows. \`[a; b]\` is a \`column\`, \`(a | b)\` a \`row\`.
39
+
40
+ 1. One figure (spent this month, cups of water, km this week):
41
+ small \`[caption label; number title + unit; caption context]\` (with a \`source\`: drop the last caption), or with a tap \`[number title + unit; button]\`;
42
+ medium \`([caption label; number title + unit; caption context] | X)\`, X is one thing: a \`ring\` for a goal, a \`button\`, or \`[caption; number body]\`. Never two things stacked beside it;
43
+ large \`[the medium tree; list of up to 5 (text grow | number body)]\`: a breakdown or the last entries.
44
+ 2. Several things, each with a figure or a time (prices, rates, books, a schedule, countdowns): every size is a \`list\`, one row per thing, the nearest or most important first.
45
+ first row \`(text name grow | number title + unit)\`, the rest \`(text name grow | number body + unit)\` or \`(text name grow | caption time)\`;
46
+ small: the same rows with short names. A \`list\` grows with taller widgets and counts "N more"; a \`column\` does not.
47
+ 3. Things to tick (to-dos, shopping, habits): see the document below.
48
+ 4. Words (a fortune, a word of the day): one \`column\`, one text per line, never side by side in a \`row\`.
49
+ small \`[text title; caption; caption]\`; medium \`[text title; text body; caption; caption]\`, or \`[(text title grow | button); text body; caption]\` with a tap; large: up to 4 more \`caption\` lines. Split long sentences.
50
+
51
+ ### Removing one
52
+
53
+ Asked to remove a widget: send \`{"v": 1, "id": "todo", "removed": true}\` with its \`id\`. It leaves their widget list; a copy on the home screen shows "deleted" until they long-press it away. \`[widget todo] removed\` means they deleted it themselves. Either way, stop sending it. A later snapshot with that \`id\` brings it back, so send one only if they ask again.
9
54
 
10
55
  ### Document
11
56
 
12
57
  \`\`\`json
13
- {"v": 1, "id": "todo", "title": "To-do", "badge": 3,
58
+ {"v": 1, "id": "todo", "title": "To-do", "badge": 2,
14
59
  "action": {"do": "capture"},
60
+ "small": {"type": "list", "items": [
61
+ {"type": "row", "children": [
62
+ {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
63
+ {"type": "text", "text": "Pay rent", "grow": true}]},
64
+ {"type": "row", "children": [
65
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
66
+ {"type": "text", "text": "Call mom", "grow": true}]}]},
15
67
  "medium": {"type": "list", "items": [
16
68
  {"type": "row", "children": [
17
69
  {"type": "check", "id": "t1", "label": "Pay rent", "checked": false},
18
70
  {"type": "text", "text": "Pay rent", "grow": true},
19
- {"type": "text", "text": "today", "style": "caption", "tone": "accent"}]}]}}
71
+ {"type": "text", "text": "10/24", "style": "caption", "tone": "accent"}]},
72
+ {"type": "row", "children": [
73
+ {"type": "check", "id": "t2", "label": "Call mom", "checked": false},
74
+ {"type": "text", "text": "Call mom", "grow": true}]}]}}
20
75
  \`\`\`
21
76
 
77
+ \`large\`: the same \`list\` as \`medium\`. Always list every item; we show what fits and "N more".
78
+
22
79
  - \`v\`: 1. \`id\`: \`^[a-z0-9][a-z0-9_-]{0,31}$\`, the same across updates. \`title\`: up to 40 characters.
23
80
  - \`badge\` (optional): an integer 0–9999, or up to 12 characters.
24
81
  - \`action\` (optional, top right): \`capture\`, \`chat\` or \`liveware\`.
25
82
  - \`small\` / \`medium\` / \`large\`: one block tree per size, at least one. A missing size borrows \`medium\`, then \`large\`, then \`small\`. A medium list shows about 3 rows.
26
83
  - At most 16 KiB of UTF-8. Unknown fields and block types are skipped.
27
- - Write the text in your partner's language. Colours, fonts and spacing are ours; you choose the structure.
84
+ - Colours, fonts and spacing are ours; you choose the structure.
28
85
 
29
86
  ### Blocks
30
87
 
@@ -59,7 +116,7 @@ A widget is a small card on your partner's phone home screen. You write it with
59
116
  "source": {"url": "https://…", "every": "30m", "fields": {"price": "data.price"}}
60
117
  \`\`\`
61
118
 
62
- The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. Use the field as \`{price}\` in \`text\` or \`number\`. Your partner sees the domain.
119
+ The phone fetches it itself: HTTPS GET, no credentials, JSON up to 64 KiB, every 15m to 24h. A field is a dot path into that JSON, array items by number (\`data.items.0.price\`); each step is letters, digits or \`_\` only, so a key like \`xnet-1\` cannot be read. Field names: lowercase letters, digits and \`_\`; every \`{name}\` you write must be one of them. Write \`{price}\` in \`text\` or \`number\` where the value goes: a value you type in yourself goes stale. Your partner sees the domain.
63
120
 
64
121
  ### What comes back
65
122
 
@@ -73,6 +130,7 @@ Each tap or entry arrives as an ordinary message with one line for you:
73
130
  [widget todo] pressed "Refresh" (b1)
74
131
  [widget todo] capture: buy milk
75
132
  [widget todo] resend
133
+ [widget todo] removed
76
134
  \`\`\`
77
135
 
78
136
  They can arrive late and several at once: apply each in order, they are not duplicates. Then send one new full snapshot. \`resend\` means a device lost the widget: send the latest snapshot again.