@firstperson/firstperson 2026.1.35 → 2026.1.36

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.
@@ -10,7 +10,8 @@
10
10
  "Bash(npm install:*)",
11
11
  "Bash(npm run build:*)",
12
12
  "Bash(npx tsc:*)",
13
- "Bash(npm run:*)"
13
+ "Bash(npm run:*)",
14
+ "Bash(npm test:*)"
14
15
  ]
15
16
  }
16
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstperson/firstperson",
3
- "version": "2026.1.35",
3
+ "version": "2026.1.36",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for the First Person iOS app",
6
6
  "main": "index.ts",
package/src/channel.ts CHANGED
@@ -14,6 +14,30 @@ import type { FirstPersonConfig, ResolvedFirstPersonAccount, CoreConfig } from "
14
14
 
15
15
  const DEFAULT_RELAY_URL = "wss://chat.firstperson.ai";
16
16
 
17
+ // Dedupe cache to track recently processed messages (20-minute window matches OpenClaw)
18
+ const DEDUPE_WINDOW_MS = 20 * 60 * 1000; // 20 minutes
19
+ const processedMessages = new Map<string, number>(); // messageId -> timestamp
20
+
21
+ function isDuplicateMessage(messageId: string): boolean {
22
+ const now = Date.now();
23
+
24
+ // Clean up old entries
25
+ for (const [id, timestamp] of processedMessages) {
26
+ if (now - timestamp > DEDUPE_WINDOW_MS) {
27
+ processedMessages.delete(id);
28
+ }
29
+ }
30
+
31
+ // Check if this message was already processed
32
+ if (processedMessages.has(messageId)) {
33
+ return true;
34
+ }
35
+
36
+ // Mark as processed
37
+ processedMessages.set(messageId, now);
38
+ return false;
39
+ }
40
+
17
41
  const meta = {
18
42
  id: "firstperson",
19
43
  label: "First Person",
@@ -325,6 +349,20 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
325
349
  onMessage: async (message) => {
326
350
  log?.info(`[fp] 1. received: ${message.messageId} from ${message.deviceId}`);
327
351
 
352
+ // Check for duplicate message (dedupe within 20-minute window)
353
+ if (isDuplicateMessage(message.messageId)) {
354
+ log?.info(`[fp] skipping duplicate message: ${message.messageId}`);
355
+ // Send "skipped" status instead of processing
356
+ sendStatusUpdate({
357
+ to: message.deviceId,
358
+ messageId: message.messageId,
359
+ status: "completed", // Use completed but with skip indication in details
360
+ message: "Message already processed (duplicate)",
361
+ details: { skipped: true, reason: "duplicate" },
362
+ });
363
+ return;
364
+ }
365
+
328
366
  // Track timing for status updates
329
367
  const dispatchStartTime = Date.now();
330
368