@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 +4 -2
- package/src/channel.ts +126 -9
- package/src/relay-client.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstperson/firstperson",
|
|
3
|
-
"version": "2026.1.
|
|
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": [
|
|
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
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
});
|
package/src/relay-client.ts
CHANGED
|
@@ -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;
|