@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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/bridge.js +68 -15
  3. 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.2",
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 and fetch fresh /v1/rooms/{id}/context before deciding.",
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
- const response = await this.fetch(hookURL, {
664
- method: "POST",
665
- headers: {
666
- Authorization: `Bearer ${hookToken}`,
667
- "Content-Type": "application/json"
668
- },
669
- body: JSON.stringify(payload)
670
- });
671
- const text = await response.text();
672
- if (!response.ok) {
673
- throw new HTTPError(response.status, parseJSONText(text), `OpenClaw wake failed ${response.status}`);
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.warn(`stream disconnected error=${err.message}`);
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;