@firstperson/firstperson 2026.1.34 → 2026.1.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstperson/firstperson",
3
- "version": "2026.1.34",
3
+ "version": "2026.1.35",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for the First Person iOS app",
6
6
  "main": "index.ts",
@@ -19,7 +19,9 @@
19
19
  "ai-assistant"
20
20
  ],
21
21
  "openclaw": {
22
- "extensions": ["./index.ts"],
22
+ "extensions": [
23
+ "./index.ts"
24
+ ],
23
25
  "channel": {
24
26
  "id": "firstperson",
25
27
  "label": "First Person",
package/src/channel.ts CHANGED
@@ -294,7 +294,7 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
294
294
  lastError: null,
295
295
  });
296
296
 
297
- const { startRelayConnection } = await import("./relay-client.js");
297
+ const { startRelayConnection, sendStatusUpdate } = await import("./relay-client.js");
298
298
 
299
299
  return startRelayConnection({
300
300
  relayUrl: account.relayUrl,
@@ -325,6 +325,9 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
325
325
  onMessage: async (message) => {
326
326
  log?.info(`[fp] 1. received: ${message.messageId} from ${message.deviceId}`);
327
327
 
328
+ // Track timing for status updates
329
+ const dispatchStartTime = Date.now();
330
+
328
331
  try {
329
332
  const globalRuntime = getFirstPersonRuntime();
330
333
  const channelApi = (globalRuntime as any).channel;
@@ -417,6 +420,13 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
417
420
 
418
421
  if (!route) {
419
422
  log?.warn(`[fp] no route - check bindings for ${message.deviceId}`);
423
+ // Send error status to client
424
+ sendStatusUpdate({
425
+ to: message.deviceId,
426
+ messageId: message.messageId,
427
+ status: "failed",
428
+ message: "No agent route configured for this device",
429
+ });
420
430
  return;
421
431
  }
422
432
 
@@ -477,6 +487,17 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
477
487
  });
478
488
  log?.info(`[fp] 8. session recorded, starting dispatch...`);
479
489
 
490
+ // ========== SEND "PROCESSING" STATUS ==========
491
+ sendStatusUpdate({
492
+ to: message.deviceId,
493
+ messageId: message.messageId,
494
+ status: "processing",
495
+ message: "Your message is being processed",
496
+ details: {
497
+ startedAt: Date.now(),
498
+ },
499
+ });
500
+
480
501
  // Create dispatcher for replies
481
502
  const { sendTextMessage } = await import("./relay-client.js");
482
503
  const { dispatcher, replyOptions } = channelApi.reply.createReplyDispatcherWithTyping({
@@ -493,23 +514,83 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
493
514
  },
494
515
  onError: (err: Error) => {
495
516
  log?.error(`[fp] reply delivery failed: ${err.message}`);
517
+ // Send error status to client
518
+ sendStatusUpdate({
519
+ to: message.deviceId,
520
+ messageId: message.messageId,
521
+ status: "failed",
522
+ message: `Failed to deliver reply: ${err.message}`,
523
+ });
496
524
  },
497
525
  });
498
526
 
499
- // Dispatch to agent
500
- await channelApi.reply.dispatchReplyFromConfig({
501
- ctx: ctxPayload,
502
- cfg,
503
- dispatcher,
504
- replyOptions,
505
- });
506
- log?.info(`[fp] 11. dispatch complete`);
527
+ // ========== DISPATCH WITH ENHANCED REPLY OPTIONS ==========
528
+ // Add callbacks to track agent progress
529
+ const enhancedReplyOptions = {
530
+ ...replyOptions,
531
+ onAgentRunStart: (runId: string) => {
532
+ log?.info(`[fp] agent run started: ${runId}`);
533
+ },
534
+ onToolResult: (payload: { name?: string }) => {
535
+ log?.info(`[fp] tool executed: ${payload.name}`);
536
+ },
537
+ };
538
+
539
+ try {
540
+ // Dispatch to agent
541
+ await channelApi.reply.dispatchReplyFromConfig({
542
+ ctx: ctxPayload,
543
+ cfg,
544
+ dispatcher,
545
+ replyOptions: enhancedReplyOptions,
546
+ });
547
+
548
+ const dispatchDuration = Date.now() - dispatchStartTime;
549
+ log?.info(`[fp] 11. dispatch complete (took ${dispatchDuration}ms)`);
550
+
551
+ // Send completion status
552
+ sendStatusUpdate({
553
+ to: message.deviceId,
554
+ messageId: message.messageId,
555
+ status: "completed",
556
+ message: "Response delivered",
557
+ details: {
558
+ durationMs: dispatchDuration,
559
+ },
560
+ });
561
+ } catch (dispatchError) {
562
+ const err = dispatchError as Error;
563
+ log?.error(`[fp] dispatch failed: ${err.message}`);
564
+
565
+ // Send error status to client
566
+ sendStatusUpdate({
567
+ to: message.deviceId,
568
+ messageId: message.messageId,
569
+ status: "failed",
570
+ message: err.message.includes("timed out")
571
+ ? "Request timed out. The AI is taking longer than expected."
572
+ : `Error: ${err.message}`,
573
+ details: {
574
+ errorType: err.message.includes("timed out") ? "timeout" : "error",
575
+ durationMs: Date.now() - dispatchStartTime,
576
+ },
577
+ });
578
+ }
507
579
  return;
508
580
  }
509
581
 
510
582
  // Fallback: agent.enqueueInbound
511
583
  if ((globalRuntime as any).agent?.enqueueInbound) {
512
584
  log?.info(`[fp] fallback: using agent.enqueueInbound`);
585
+
586
+ // Send processing status
587
+ sendStatusUpdate({
588
+ to: message.deviceId,
589
+ messageId: message.messageId,
590
+ status: "processing",
591
+ message: "Your message is being processed",
592
+ });
593
+
513
594
  const { sendTextMessage } = await import("./relay-client.js");
514
595
  await (globalRuntime as any).agent.enqueueInbound({
515
596
  channel: "firstperson",
@@ -532,6 +613,13 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
532
613
  text,
533
614
  replyTo: message.messageId,
534
615
  });
616
+ // Send completion status
617
+ sendStatusUpdate({
618
+ to: message.deviceId,
619
+ messageId: message.messageId,
620
+ status: "completed",
621
+ message: "Response delivered",
622
+ });
535
623
  },
536
624
  });
537
625
  return;
@@ -540,6 +628,15 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
540
628
  // Fallback: gateway.handleInbound
541
629
  if ((globalRuntime as any).gateway?.handleInbound) {
542
630
  log?.info(`[fp] fallback: using gateway.handleInbound`);
631
+
632
+ // Send processing status
633
+ sendStatusUpdate({
634
+ to: message.deviceId,
635
+ messageId: message.messageId,
636
+ status: "processing",
637
+ message: "Your message is being processed",
638
+ });
639
+
543
640
  const { sendTextMessage } = await import("./relay-client.js");
544
641
  await (globalRuntime as any).gateway.handleInbound({
545
642
  channel: "firstperson",
@@ -557,15 +654,35 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
557
654
  text,
558
655
  replyTo: message.messageId,
559
656
  });
657
+ // Send completion status
658
+ sendStatusUpdate({
659
+ to: message.deviceId,
660
+ messageId: message.messageId,
661
+ status: "completed",
662
+ message: "Response delivered",
663
+ });
560
664
  },
561
665
  });
562
666
  return;
563
667
  }
564
668
 
565
669
  log?.error(`[fp] ERROR: no dispatch method available on runtime`);
670
+ sendStatusUpdate({
671
+ to: message.deviceId,
672
+ messageId: message.messageId,
673
+ status: "failed",
674
+ message: "No dispatch method available",
675
+ });
566
676
 
567
677
  } catch (err) {
568
678
  log?.error(`[fp] ERROR processing message: ${err}`);
679
+ // Send error status to client
680
+ sendStatusUpdate({
681
+ to: message.deviceId,
682
+ messageId: message.messageId,
683
+ status: "failed",
684
+ message: `Error processing message: ${(err as Error).message}`,
685
+ });
569
686
  }
570
687
  },
571
688
  });
@@ -29,6 +29,30 @@ export interface SendMessageResult {
29
29
  chatId: string;
30
30
  }
31
31
 
32
+ /**
33
+ * Message status types matching the relay protocol
34
+ */
35
+ export type MessageStatus =
36
+ | "received"
37
+ | "forwarded"
38
+ | "processing"
39
+ | "completed"
40
+ | "failed"
41
+ | "gateway_offline";
42
+
43
+ /**
44
+ * Status message sent to relay for forwarding to iOS client
45
+ */
46
+ export interface StatusUpdate {
47
+ type: "message_status";
48
+ messageId: string;
49
+ to: string;
50
+ status: MessageStatus;
51
+ message?: string;
52
+ timestamp: number;
53
+ details?: Record<string, unknown>;
54
+ }
55
+
32
56
  // Store active connection and pending reply callbacks
33
57
  let activeWs: WebSocket | null = null;
34
58
  let activeLogger: Logger | null = null;
@@ -43,6 +67,45 @@ function log(level: "info" | "warn" | "error", msg: string): void {
43
67
  }
44
68
  }
45
69
 
70
+ /**
71
+ * Send a status update for a message to the relay.
72
+ * The relay will forward this to the iOS client.
73
+ */
74
+ export function sendStatusUpdate(params: {
75
+ to: string;
76
+ messageId: string;
77
+ status: MessageStatus;
78
+ message?: string;
79
+ details?: Record<string, unknown>;
80
+ }): void {
81
+ const { to, messageId, status, message, details } = params;
82
+
83
+ if (!activeWs || activeWs.readyState !== WebSocket.OPEN) {
84
+ log("warn", `[relay-client] Cannot send status update - no active connection`);
85
+ return;
86
+ }
87
+
88
+ const statusMsg: StatusUpdate = {
89
+ type: "message_status",
90
+ messageId,
91
+ to,
92
+ status,
93
+ timestamp: Date.now(),
94
+ ...(message && { message }),
95
+ ...(details && { details }),
96
+ };
97
+
98
+ activeWs.send(JSON.stringify(statusMsg));
99
+ log("info", `[relay-client] Sent status update: ${status} for message ${messageId} to ${to}`);
100
+ }
101
+
102
+ /**
103
+ * Check if the relay connection is active
104
+ */
105
+ export function isRelayConnected(): boolean {
106
+ return activeWs !== null && activeWs.readyState === WebSocket.OPEN;
107
+ }
108
+
46
109
  export async function sendTextMessage(params: {
47
110
  relayUrl: string;
48
111
  token: string;