@febro28/aya-bridge 0.1.2 → 0.1.3
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 +2 -2
- package/src/bridge.js +68 -15
- package/test/run.js +694 -544
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@febro28/aya-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenClaw sidecar that consumes areyouai agent stream deliveries and wakes local OpenClaw hooks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aya": "bin/aya.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node ./test/run.js parse && node ./test/run.js turn && node ./test/run.js retry && node ./test/run.js refresh && node ./test/run.js dedupe && node ./test/run.js recovery && node ./test/run.js terminal"
|
|
9
|
+
"test": "node ./test/run.js parse && node ./test/run.js turn && node ./test/run.js retry && node ./test/run.js refresh && node ./test/run.js disconnect && node ./test/run.js typing && node ./test/run.js dedupe && node ./test/run.js recovery && node ./test/run.js recovery-failure && node ./test/run.js terminal"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=20"
|
package/src/bridge.js
CHANGED
|
@@ -7,6 +7,7 @@ const os = require("node:os");
|
|
|
7
7
|
const { setTimeout: sleep } = require("node:timers/promises");
|
|
8
8
|
|
|
9
9
|
const DEFAULT_BASE_DIR = path.join(os.homedir(), ".areyouai");
|
|
10
|
+
const DEFAULT_TYPING_TTL_MS = 30_000;
|
|
10
11
|
|
|
11
12
|
class HTTPError extends Error {
|
|
12
13
|
constructor(status, body, message) {
|
|
@@ -196,6 +197,20 @@ function computeBackoffMs(reconnect, attempt) {
|
|
|
196
197
|
return growth + jitterAdd;
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
function isExpectedStreamDisconnectError(err) {
|
|
201
|
+
if (!err) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (err.name === "AbortError") {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const message = String(err.message || "").trim().toLowerCase();
|
|
208
|
+
if (!message) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return ["terminated", "eof", "end of file", "premature close", "socket closed", "connection closed"].some((needle) => message.includes(needle));
|
|
212
|
+
}
|
|
213
|
+
|
|
199
214
|
async function* parseSSE(body) {
|
|
200
215
|
const decoder = new TextDecoder();
|
|
201
216
|
let buffer = "";
|
|
@@ -480,6 +495,29 @@ class BridgeDaemon {
|
|
|
480
495
|
return payload;
|
|
481
496
|
}
|
|
482
497
|
|
|
498
|
+
async sendRoomTypingSignal(roomId, token, state, ttlMs) {
|
|
499
|
+
const cleanRoomId = String(roomId || "").trim();
|
|
500
|
+
const cleanToken = String(token || "").trim();
|
|
501
|
+
const cleanState = String(state || "").trim();
|
|
502
|
+
if (!cleanRoomId || !cleanToken || (cleanState !== "start" && cleanState !== "stop")) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
await this.requestJSON(`/v1/rooms/${encodeURIComponent(cleanRoomId)}/typing`, {
|
|
507
|
+
method: "POST",
|
|
508
|
+
auth: "none",
|
|
509
|
+
headers: {
|
|
510
|
+
Authorization: `Bearer ${cleanToken}`
|
|
511
|
+
},
|
|
512
|
+
body: cleanState === "start" ? { state: cleanState, ttl_ms: ttlMs } : { state: cleanState }
|
|
513
|
+
});
|
|
514
|
+
return true;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
this.logger.warn(`typing signal failed room_id=${cleanRoomId} state=${cleanState} error=${err.message}`);
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
483
521
|
async readRoomToken(roomId) {
|
|
484
522
|
return readJSON(path.join(this.paths.tokenDir, `${roomId}.json`), null);
|
|
485
523
|
}
|
|
@@ -628,6 +666,11 @@ class BridgeDaemon {
|
|
|
628
666
|
const tokenState = await this.ensureFreshRoomTokenForJob(job);
|
|
629
667
|
const tokenExpiresAt = tokenState?.expires_at || job.token_expires_at || null;
|
|
630
668
|
const tokenPath = path.join(this.paths.tokenDir, `${roomId}.json`);
|
|
669
|
+
const typingToken = String(tokenState?.token || job.room_token || "").trim();
|
|
670
|
+
let typingStarted = false;
|
|
671
|
+
if (typingToken) {
|
|
672
|
+
typingStarted = await this.sendRoomTypingSignal(roomId, typingToken, "start", DEFAULT_TYPING_TTL_MS);
|
|
673
|
+
}
|
|
631
674
|
const contract = {
|
|
632
675
|
contract: "aya.wake.v1",
|
|
633
676
|
delivery_id: deliveryId,
|
|
@@ -642,7 +685,7 @@ class BridgeDaemon {
|
|
|
642
685
|
token_path: tokenPath,
|
|
643
686
|
token_expires_at: tokenExpiresAt,
|
|
644
687
|
instructions: [
|
|
645
|
-
"Read token_path
|
|
688
|
+
"Read token_path, fetch fresh /v1/rooms/{id}/context, then POST /v1/rooms/{id}/context/ack with the returned turn_index after the response parses successfully.",
|
|
646
689
|
"Reply exactly once only if next_actor_id in fresh context equals your agent_id.",
|
|
647
690
|
"If token missing/expired or API returns 401, refresh with POST /v1/rooms/{id}/access-token and retry once.",
|
|
648
691
|
"If API returns 409 turn_mismatch or stale_bundle_hash, stop and wait for next wake."
|
|
@@ -660,19 +703,25 @@ class BridgeDaemon {
|
|
|
660
703
|
deliver: false,
|
|
661
704
|
timeoutSeconds: 120
|
|
662
705
|
};
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
706
|
+
try {
|
|
707
|
+
const response = await this.fetch(hookURL, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
Authorization: `Bearer ${hookToken}`,
|
|
711
|
+
"Content-Type": "application/json"
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify(payload)
|
|
714
|
+
});
|
|
715
|
+
const text = await response.text();
|
|
716
|
+
if (!response.ok) {
|
|
717
|
+
throw new HTTPError(response.status, parseJSONText(text), `OpenClaw wake failed ${response.status}`);
|
|
718
|
+
}
|
|
719
|
+
return parseJSONText(text);
|
|
720
|
+
} finally {
|
|
721
|
+
if (typingStarted) {
|
|
722
|
+
await this.sendRoomTypingSignal(roomId, typingToken, "stop");
|
|
723
|
+
}
|
|
674
724
|
}
|
|
675
|
-
return parseJSONText(text);
|
|
676
725
|
}
|
|
677
726
|
|
|
678
727
|
async drainWakeQueue() {
|
|
@@ -709,7 +758,6 @@ class BridgeDaemon {
|
|
|
709
758
|
}
|
|
710
759
|
|
|
711
760
|
async processRecovery() {
|
|
712
|
-
this.state.last_acknowledged_delivery_id = "";
|
|
713
761
|
this.state.last_stream_status = "recovery";
|
|
714
762
|
await this.saveState();
|
|
715
763
|
const payload = await this.requestJSON("/v1/agent/actionable-rooms");
|
|
@@ -750,6 +798,10 @@ class BridgeDaemon {
|
|
|
750
798
|
});
|
|
751
799
|
}
|
|
752
800
|
await this.drainWakeQueue();
|
|
801
|
+
if (String(this.state.last_acknowledged_delivery_id || "").trim()) {
|
|
802
|
+
this.state.last_acknowledged_delivery_id = "";
|
|
803
|
+
await this.saveState();
|
|
804
|
+
}
|
|
753
805
|
}
|
|
754
806
|
|
|
755
807
|
async handleTurnReady(payload) {
|
|
@@ -907,7 +959,8 @@ class BridgeDaemon {
|
|
|
907
959
|
}
|
|
908
960
|
this.state.last_stream_status = "disconnected";
|
|
909
961
|
await this.saveState();
|
|
910
|
-
this.logger.
|
|
962
|
+
const log = isExpectedStreamDisconnectError(err) ? this.logger.info : this.logger.warn;
|
|
963
|
+
log(`stream disconnected error=${err.message}`);
|
|
911
964
|
}
|
|
912
965
|
if (signal?.aborted) {
|
|
913
966
|
break;
|