beeos-claw 0.5.65-staging.3 → 0.5.65-staging.5

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": "beeos-claw",
3
- "version": "0.5.65-staging.3",
3
+ "version": "0.5.65-staging.5",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",
@@ -38,17 +38,10 @@ export async function withinDeadline(run, deadline) {
38
38
  * invoking start() again would create competing reconnect loops.
39
39
  */
40
40
  export async function startGatewayWithinDeadline(gateway, deadline) {
41
- try {
42
- await withinDeadline(() => gateway.start(), deadline);
43
- return;
44
- } catch (error) {
45
- if (!isRetryableGatewayHandshake(error)) throw error;
46
- }
47
-
48
- await withinDeadline((signal) => waitForGatewayConnection(gateway, signal), deadline);
41
+ return await withinDeadline((signal) => startAndObserveGateway(gateway, signal), deadline);
49
42
  }
50
43
 
51
- export async function closeRuntimeResources({ consumer, gateway }, cleanupTimeoutMs = 5_000) {
44
+ export async function closeRuntimeResources({ consumer, gateway, releaseGatewayTransportObserver }, cleanupTimeoutMs = 5_000) {
52
45
  let cleanupError;
53
46
  try {
54
47
  if (consumer) {
@@ -63,6 +56,11 @@ export async function closeRuntimeResources({ consumer, gateway }, cleanupTimeou
63
56
  }
64
57
  } catch (error) {
65
58
  cleanupError ??= new Error(WORKER_CLEANUP_MESSAGE, { cause: error });
59
+ } finally {
60
+ // The retained observer is only needed while GatewayClient can still
61
+ // emit a transport error. Release it after the bounded stop completes,
62
+ // including the consumer-stop failure path.
63
+ releaseGatewayTransportObserver?.();
66
64
  }
67
65
  }
68
66
  return cleanupError;
@@ -116,32 +114,41 @@ function isRetryableGatewayHandshake(error) {
116
114
  ((error.retryable === true) || error.code === "UNAVAILABLE");
117
115
  }
118
116
 
119
- function waitForGatewayConnection(gateway, signal) {
117
+ function startAndObserveGateway(gateway, signal) {
120
118
  return new Promise((resolve, reject) => {
121
119
  let settled = false;
122
- const cleanup = () => {
120
+ let observedTransportFailure = false;
121
+ const cleanup = (retainTransportObserver = false) => {
123
122
  gateway.off("connected", onConnected);
124
123
  gateway.off("auth-failed", onAuthFailed);
125
- gateway.off("error", onError);
124
+ if (!retainTransportObserver) gateway.off("error", onError);
125
+ gateway.off("disconnected", onDisconnected);
126
126
  signal.removeEventListener("abort", onAbort);
127
127
  };
128
- const settle = (callback, value) => {
128
+ const releaseTransportObserver = () => gateway.off("error", onError);
129
+ const settle = (callback, value, retainTransportObserver = false) => {
129
130
  if (settled) return;
130
131
  settled = true;
131
- cleanup();
132
+ cleanup(retainTransportObserver);
132
133
  callback(value);
133
134
  };
134
- const onConnected = () => settle(resolve);
135
+ // `error` has special EventEmitter semantics: after the initial handshake
136
+ // succeeds, a later WebSocket transport error must stay observed until the
137
+ // worker shuts the Gateway down. GatewayClient owns reconnects; this
138
+ // observer only prevents that owned transient from becoming process-fatal.
139
+ const onConnected = () => settle(resolve, releaseTransportObserver, true);
135
140
  const onAuthFailed = (failure) => {
136
141
  const reason = failure && typeof failure.reason === "string"
137
142
  ? failure.reason
138
143
  : "Gateway authentication failed";
139
144
  settle(reject, new Error(reason));
140
145
  };
141
- // GatewayClient emits `error` for each failed reconnect before scheduling
142
- // the next one. Keep an error listener while waiting so EventEmitter does
143
- // not crash the worker; auth-failed remains the immediate terminal signal.
144
- const onError = () => {};
146
+ // GatewayClient owns reconnect scheduling for a WebSocket-level startup
147
+ // failure. Install this observer before `start()` so its first error cannot
148
+ // become EventEmitter's fatal unhandled `error` event. The subsequent
149
+ // `start()` rejection is intentionally held open for that owned retry.
150
+ const onError = () => { observedTransportFailure = true; };
151
+ const onDisconnected = () => { observedTransportFailure = true; };
145
152
  const onAbort = () => settle(reject, signal.reason ?? new Error("Gateway connection aborted"));
146
153
 
147
154
  if (signal.aborted) {
@@ -151,7 +158,22 @@ function waitForGatewayConnection(gateway, signal) {
151
158
  gateway.on("connected", onConnected);
152
159
  gateway.on("auth-failed", onAuthFailed);
153
160
  gateway.on("error", onError);
161
+ gateway.on("disconnected", onDisconnected);
154
162
  signal.addEventListener("abort", onAbort, { once: true });
163
+
164
+ Promise.resolve()
165
+ .then(() => gateway.start())
166
+ .then(
167
+ () => settle(resolve, releaseTransportObserver, true),
168
+ (error) => {
169
+ // Typed handshake UNAVAILABLE and a pre-auth transport failure both
170
+ // leave GatewayClient's single bounded reconnect loop authoritative.
171
+ // A nonretryable handshake instead emits auth-failed and settles
172
+ // above without waiting for the worker deadline.
173
+ if (isRetryableGatewayHandshake(error) || observedTransportFailure) return;
174
+ settle(reject, error);
175
+ },
176
+ );
155
177
  });
156
178
  }
157
179
 
@@ -114,13 +114,14 @@ export async function main() {
114
114
  retry: { baseMs: 100, maxMs: 500, maxAttempts: 60 } });
115
115
  let consumer;
116
116
  let primaryError;
117
+ let releaseGatewayTransportObserver;
117
118
  const runtimeLease = () => ({ instanceId, leaseId: nonempty(authority.leaseId, "authority.leaseId"),
118
119
  handlerIdentity: nonempty(authority.handlerIdentity, "authority.handlerIdentity"),
119
120
  runtimeEpoch: decimal(authority.runtimeEpoch, "authority.runtimeEpoch"),
120
121
  runtimeLeaseCredential: nonempty(authority.runtimeLeaseCredential, "authority.runtimeLeaseCredential"),
121
122
  leaseExpiresAt: new Date(Number(leaseClaims.exp) * 1_000).toISOString() });
122
123
  try {
123
- await startGatewayWithinDeadline(gateway, deadline);
124
+ releaseGatewayTransportObserver = await startGatewayWithinDeadline(gateway, deadline);
124
125
  const identities = new FileRuntimeAgentIdentityRegistry(`${statePrefix}-identities.json`);
125
126
  const dispatcher = new OpenClawRuntimeCommandDispatcher({ getGateway: () => gateway,
126
127
  resources: rejectingResources(), identities,
@@ -150,7 +151,7 @@ export async function main() {
150
151
  primaryError = error;
151
152
  } finally {
152
153
  primaryError = selectRuntimeError(primaryError,
153
- await closeRuntimeResources({ consumer, gateway }));
154
+ await closeRuntimeResources({ consumer, gateway, releaseGatewayTransportObserver }));
154
155
  try { completionGate.assertNoFailure(); }
155
156
  catch (error) { primaryError ??= error; }
156
157
  }