@clawling/clawchat-plugin-openclaw 2026.5.13-dev.0 → 2026.5.13-dev.2
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.
- package/dist/src/api-client.js +146 -26
- package/dist/src/client.js +4 -1
- package/dist/src/inbound.js +21 -4
- package/dist/src/login.runtime.js +4 -0
- package/dist/src/outbound.js +43 -8
- package/dist/src/refresh-manager.js +278 -0
- package/dist/src/reply-dispatcher.js +5 -2
- package/dist/src/runtime.js +552 -28
- package/dist/src/storage.js +81 -5
- package/dist/src/ws-alignment.js +2 -1
- package/dist/src/ws-client.js +42 -4
- package/package.json +1 -1
- package/src/api-client.ts +174 -31
- package/src/client.ts +12 -1
- package/src/inbound.ts +24 -5
- package/src/login.runtime.ts +4 -0
- package/src/outbound.ts +47 -9
- package/src/protocol-types.ts +8 -2
- package/src/refresh-manager.ts +371 -0
- package/src/reply-dispatcher.ts +5 -2
- package/src/runtime.ts +632 -25
- package/src/storage.ts +124 -4
- package/src/ws-alignment.ts +2 -1
- package/src/ws-client.ts +40 -4
package/src/storage.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type ActivationInput = {
|
|
|
21
21
|
conversationId?: string | null;
|
|
22
22
|
activatedAt?: number;
|
|
23
23
|
loginMethod?: string | null;
|
|
24
|
+
/** §E — exact `X-Device-Id` used at connect time (OpenClaw: `CHANNEL_ID`). */
|
|
25
|
+
deviceId?: string | null;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export type ActivationCredentials = {
|
|
@@ -28,6 +30,25 @@ export type ActivationCredentials = {
|
|
|
28
30
|
ownerUserId: string;
|
|
29
31
|
accessToken: string;
|
|
30
32
|
refreshToken: string | null;
|
|
33
|
+
/** §A.0 — epoch ms the row was activated; expiry fallback (`activatedAt + 24h`). */
|
|
34
|
+
activatedAt: number | null;
|
|
35
|
+
/** §E — connect-time device id used as `X-Device-Id` on refresh. */
|
|
36
|
+
deviceId: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** §0/§A.3 — persist a rotated `{access,refresh}` pair BEFORE the in-memory swap. */
|
|
40
|
+
export type RotateActivationTokensInput = {
|
|
41
|
+
platform: string;
|
|
42
|
+
accountId: string;
|
|
43
|
+
accessToken: string;
|
|
44
|
+
refreshToken: string;
|
|
45
|
+
rotatedAt?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** §C.1 — blank the credential columns but KEEP identity for re-pair. */
|
|
49
|
+
export type ClearActivationCredentialsInput = {
|
|
50
|
+
platform: string;
|
|
51
|
+
accountId: string;
|
|
31
52
|
};
|
|
32
53
|
|
|
33
54
|
export type ActivationBootstrapInput = {
|
|
@@ -242,6 +263,13 @@ ALTER TABLE clawchat_messages ADD COLUMN send_status TEXT;
|
|
|
242
263
|
ALTER TABLE clawchat_messages ADD COLUMN protocol_message_id TEXT;
|
|
243
264
|
ALTER TABLE clawchat_messages ADD COLUMN acked_at INTEGER;
|
|
244
265
|
ALTER TABLE clawchat_messages ADD COLUMN send_error TEXT;
|
|
266
|
+
`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
version: 7,
|
|
270
|
+
name: "activation_device_id",
|
|
271
|
+
sql: `
|
|
272
|
+
ALTER TABLE activations ADD COLUMN device_id TEXT;
|
|
245
273
|
`,
|
|
246
274
|
},
|
|
247
275
|
];
|
|
@@ -320,13 +348,14 @@ export class ClawChatStore {
|
|
|
320
348
|
const ownerUserId = input.ownerUserId?.trim() || null;
|
|
321
349
|
const accessToken = input.accessToken?.trim() || null;
|
|
322
350
|
const refreshToken = input.refreshToken?.trim() || null;
|
|
351
|
+
const deviceId = input.deviceId?.trim() || null;
|
|
323
352
|
this.requireDb()
|
|
324
353
|
.prepare(
|
|
325
354
|
`INSERT INTO activations(
|
|
326
355
|
platform, account_id, user_id, owner_user_id, access_token, refresh_token,
|
|
327
356
|
activated_at, login_method, conversation_id, bootstrap_sent,
|
|
328
|
-
bootstrap_claimed_at, updated_at
|
|
329
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
357
|
+
bootstrap_claimed_at, device_id, updated_at
|
|
358
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
330
359
|
ON CONFLICT(platform, account_id) DO UPDATE SET
|
|
331
360
|
user_id = excluded.user_id,
|
|
332
361
|
owner_user_id = excluded.owner_user_id,
|
|
@@ -337,6 +366,7 @@ export class ClawChatStore {
|
|
|
337
366
|
conversation_id = excluded.conversation_id,
|
|
338
367
|
bootstrap_sent = excluded.bootstrap_sent,
|
|
339
368
|
bootstrap_claimed_at = NULL,
|
|
369
|
+
device_id = excluded.device_id,
|
|
340
370
|
updated_at = excluded.updated_at`,
|
|
341
371
|
)
|
|
342
372
|
.run(
|
|
@@ -351,17 +381,68 @@ export class ClawChatStore {
|
|
|
351
381
|
conversationId,
|
|
352
382
|
conversationId ? 0 : 1,
|
|
353
383
|
null,
|
|
384
|
+
deviceId,
|
|
354
385
|
now,
|
|
355
386
|
);
|
|
356
387
|
void conversationId;
|
|
357
388
|
});
|
|
358
389
|
}
|
|
359
390
|
|
|
391
|
+
/**
|
|
392
|
+
* §0/§A.3 — persist a rotated access+refresh pair durably BEFORE the caller
|
|
393
|
+
* swaps the in-memory token. Identity columns are untouched. Returns whether
|
|
394
|
+
* a row was updated (false ⇒ no activation row exists yet).
|
|
395
|
+
*/
|
|
396
|
+
rotateActivationTokens(input: RotateActivationTokensInput): boolean | null {
|
|
397
|
+
return this.write(() => {
|
|
398
|
+
const now = input.rotatedAt ?? Date.now();
|
|
399
|
+
const result = this.requireDb()
|
|
400
|
+
.prepare(
|
|
401
|
+
`UPDATE activations SET
|
|
402
|
+
access_token = ?,
|
|
403
|
+
refresh_token = ?,
|
|
404
|
+
activated_at = ?,
|
|
405
|
+
updated_at = ?
|
|
406
|
+
WHERE platform = ? AND account_id = ?`,
|
|
407
|
+
)
|
|
408
|
+
.run(
|
|
409
|
+
input.accessToken,
|
|
410
|
+
input.refreshToken,
|
|
411
|
+
now,
|
|
412
|
+
now,
|
|
413
|
+
input.platform,
|
|
414
|
+
input.accountId,
|
|
415
|
+
);
|
|
416
|
+
return result.changes > 0;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* §C.1 — auto-logout: blank the `access_token` / `refresh_token` columns so
|
|
422
|
+
* the agent can no longer mint tokens, but KEEP `user_id` / `owner_user_id` /
|
|
423
|
+
* `device_id` so a `/clawchat-activate <code>` re-pair reuses the identity.
|
|
424
|
+
*/
|
|
425
|
+
clearActivationCredentials(input: ClearActivationCredentialsInput): boolean | null {
|
|
426
|
+
return this.write(() => {
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const result = this.requireDb()
|
|
429
|
+
.prepare(
|
|
430
|
+
`UPDATE activations SET
|
|
431
|
+
access_token = NULL,
|
|
432
|
+
refresh_token = NULL,
|
|
433
|
+
updated_at = ?
|
|
434
|
+
WHERE platform = ? AND account_id = ?`,
|
|
435
|
+
)
|
|
436
|
+
.run(now, input.platform, input.accountId);
|
|
437
|
+
return result.changes > 0;
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
360
441
|
getActivationCredentials(input: ConversationAccountInput): ActivationCredentials | null {
|
|
361
442
|
return this.read(() => {
|
|
362
443
|
const row = this.requireDb()
|
|
363
444
|
.prepare(
|
|
364
|
-
`SELECT user_id, owner_user_id, access_token, refresh_token
|
|
445
|
+
`SELECT user_id, owner_user_id, access_token, refresh_token, activated_at, device_id
|
|
365
446
|
FROM activations
|
|
366
447
|
WHERE platform = ?
|
|
367
448
|
AND account_id = ?`,
|
|
@@ -372,6 +453,8 @@ export class ClawChatStore {
|
|
|
372
453
|
owner_user_id?: unknown;
|
|
373
454
|
access_token?: unknown;
|
|
374
455
|
refresh_token?: unknown;
|
|
456
|
+
activated_at?: unknown;
|
|
457
|
+
device_id?: unknown;
|
|
375
458
|
}
|
|
376
459
|
| undefined;
|
|
377
460
|
const userId = typeof row?.user_id === "string" ? row.user_id.trim() : "";
|
|
@@ -383,8 +466,16 @@ export class ClawChatStore {
|
|
|
383
466
|
typeof row?.refresh_token === "string" && row.refresh_token.trim()
|
|
384
467
|
? row.refresh_token.trim()
|
|
385
468
|
: null;
|
|
469
|
+
const activatedAt =
|
|
470
|
+
typeof row?.activated_at === "number" && Number.isFinite(row.activated_at)
|
|
471
|
+
? row.activated_at
|
|
472
|
+
: null;
|
|
473
|
+
const deviceId =
|
|
474
|
+
typeof row?.device_id === "string" && row.device_id.trim()
|
|
475
|
+
? row.device_id.trim()
|
|
476
|
+
: null;
|
|
386
477
|
if (!userId || !ownerUserId || !accessToken) return null;
|
|
387
|
-
return { userId, ownerUserId, accessToken, refreshToken };
|
|
478
|
+
return { userId, ownerUserId, accessToken, refreshToken, activatedAt, deviceId };
|
|
388
479
|
}) ?? null;
|
|
389
480
|
}
|
|
390
481
|
|
|
@@ -670,6 +761,35 @@ export class ClawChatStore {
|
|
|
670
761
|
});
|
|
671
762
|
}
|
|
672
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Return the most recent server-resolved `device_id` recorded from a
|
|
766
|
+
* `hello-ok` for this account, if any. Reused on reconnect so a pod restart
|
|
767
|
+
* (which mints a fresh hostname and therefore a fresh derived device id)
|
|
768
|
+
* does not present a brand-new device to the server — that would trigger a
|
|
769
|
+
* full inbox replay and orphan the previous device's cursor.
|
|
770
|
+
*/
|
|
771
|
+
getLastResolvedDeviceId(input: ConversationAccountInput): string | null {
|
|
772
|
+
return this.read(() => {
|
|
773
|
+
const row = this.requireDb()
|
|
774
|
+
.prepare(
|
|
775
|
+
`SELECT resolved_device_id
|
|
776
|
+
FROM connections
|
|
777
|
+
WHERE platform = ?
|
|
778
|
+
AND account_id = ?
|
|
779
|
+
AND resolved_device_id IS NOT NULL
|
|
780
|
+
AND resolved_device_id <> ''
|
|
781
|
+
ORDER BY id DESC
|
|
782
|
+
LIMIT 1`,
|
|
783
|
+
)
|
|
784
|
+
.get(input.platform, input.accountId) as
|
|
785
|
+
| { resolved_device_id?: unknown }
|
|
786
|
+
| undefined;
|
|
787
|
+
return typeof row?.resolved_device_id === "string" && row.resolved_device_id.trim()
|
|
788
|
+
? row.resolved_device_id.trim()
|
|
789
|
+
: null;
|
|
790
|
+
}) ?? null;
|
|
791
|
+
}
|
|
792
|
+
|
|
673
793
|
recordToolCall(input: ToolCallInput): void {
|
|
674
794
|
this.write(() => {
|
|
675
795
|
const startedAt = input.startedAt ?? Date.now();
|
package/src/ws-alignment.ts
CHANGED
|
@@ -254,7 +254,8 @@ export function createProtocolControlHandler(options: CreateProtocolControlHandl
|
|
|
254
254
|
version: "2",
|
|
255
255
|
event: "pong",
|
|
256
256
|
trace_id: env.trace_id ?? "-",
|
|
257
|
-
|
|
257
|
+
// §12: echo the sender's emitted_at verbatim (do not restamp).
|
|
258
|
+
emitted_at: env.emitted_at ?? Date.now(),
|
|
258
259
|
payload: {},
|
|
259
260
|
}),
|
|
260
261
|
);
|
package/src/ws-client.ts
CHANGED
|
@@ -394,6 +394,10 @@ export class ClawChatClient extends EventEmitter {
|
|
|
394
394
|
// Agent runtime is single-device: multi_device stays off so the server
|
|
395
395
|
// never self-fans-out this connection's own messages. notify_signals is
|
|
396
396
|
// advertised because we now handle the notify.signal frame (§9.4).
|
|
397
|
+
// history_sync (§11.4) is intentionally omitted: it is only required to
|
|
398
|
+
// *send* history.transit, which a single-device agent never does, and we
|
|
399
|
+
// do not handle inbound history.transit — advertising it would invite
|
|
400
|
+
// frames we cannot process. This is a deliberate, spec-legal omission.
|
|
397
401
|
capabilities: {
|
|
398
402
|
multi_device: false,
|
|
399
403
|
device_replay: true,
|
|
@@ -451,7 +455,32 @@ export class ClawChatClient extends EventEmitter {
|
|
|
451
455
|
this.failHandshake(new ProtocolError("invalid hello-fail payload", env), 4002, "protocol error");
|
|
452
456
|
return;
|
|
453
457
|
}
|
|
454
|
-
|
|
458
|
+
// §14.1: distinguish upstream auth-service unavailability (5xx) from token
|
|
459
|
+
// rejection (4xx). On a 5xx the token may still be valid and the auth backend
|
|
460
|
+
// (member-backend) is down — backoff-reconnect with the SAME token and do
|
|
461
|
+
// NOT refresh (a 5xx storm must not become a mass token-refresh storm). Until
|
|
462
|
+
// the server emits the distinct 5xx reason, every other hello-fail is treated
|
|
463
|
+
// as a terminal token rejection (the caller acquires a fresh token first).
|
|
464
|
+
if (/auth service unavailable/i.test(reason)) {
|
|
465
|
+
const err = new TransportError(reason);
|
|
466
|
+
this.expectedConnectTraceId = undefined;
|
|
467
|
+
this.clearTimers();
|
|
468
|
+
this.rejectPending(err);
|
|
469
|
+
this.connectReject?.(err);
|
|
470
|
+
this.connectResolve = undefined;
|
|
471
|
+
this.connectReject = undefined;
|
|
472
|
+
this.emitError(err);
|
|
473
|
+
if (this.opts.transport.state !== "closed") {
|
|
474
|
+
// Close WITHOUT marking closing/authFailed so handleClose backoff-reconnects.
|
|
475
|
+
this.opts.transport.close(4001, "auth service unavailable");
|
|
476
|
+
} else if (!this.closing && this.opts.reconnect.enabled) {
|
|
477
|
+
this.scheduleReconnect(reason);
|
|
478
|
+
} else {
|
|
479
|
+
this.transition("disconnected");
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const err = new AuthError(reason);
|
|
455
484
|
this.authFailed = true;
|
|
456
485
|
this.expectedConnectTraceId = undefined;
|
|
457
486
|
this.sendQueue.length = 0;
|
|
@@ -499,10 +528,17 @@ export class ClawChatClient extends EventEmitter {
|
|
|
499
528
|
}
|
|
500
529
|
clearTimeout(entry.timer);
|
|
501
530
|
this.pending.delete(env.trace_id);
|
|
502
|
-
const payload = env.payload && typeof env.payload === "object"
|
|
531
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
532
|
+
? env.payload as { code?: unknown; reason?: unknown; message?: unknown }
|
|
533
|
+
: undefined;
|
|
503
534
|
const code = typeof payload?.code === "string" && payload.code ? payload.code : "unknown";
|
|
504
|
-
|
|
505
|
-
|
|
535
|
+
// §14.3: the human-readable hint is `reason` (fall back to legacy `message`).
|
|
536
|
+
const hint = typeof payload?.reason === "string" && payload.reason
|
|
537
|
+
? payload.reason
|
|
538
|
+
: typeof payload?.message === "string" && payload.message
|
|
539
|
+
? payload.message
|
|
540
|
+
: "message send failed";
|
|
541
|
+
entry.reject(new MessageSendError(env.trace_id, code, hint, env.chat_id));
|
|
506
542
|
}
|
|
507
543
|
|
|
508
544
|
private startHeartbeat(): void {
|