@homespunapps/cli 1.6.53 → 1.6.54

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.
@@ -80,7 +80,7 @@ export async function runWork(args) {
80
80
  process.on("SIGTERM", stop);
81
81
  const socket = opts.once
82
82
  ? null
83
- : openWakeSocket(opts, cfg.apiKey, base, () => wake?.());
83
+ : await openWakeSocket(opts, cfg.apiKey, base, () => wake?.());
84
84
  try {
85
85
  for (;;) {
86
86
  const claimed = await claim(base, cfg.apiKey, opts);
@@ -238,22 +238,66 @@ async function report(base, apiKey, taskId, verb, text) {
238
238
  * worker that killed itself over a lost optimisation would be worse than one that
239
239
  * kept polling.
240
240
  */
241
- function openWakeSocket(opts, apiKey, base, onWake) {
241
+ /**
242
+ * Exported for testing. This function is the one part of `work` that had NO test and
243
+ * shipped broken twice over (a URL built from the app id rather than its real
244
+ * location, and a swallowed connect error), so it is worth being able to point a test
245
+ * straight at it rather than only at the command that calls it.
246
+ */
247
+ export async function openWakeSocket(opts, apiKey, base, onWake) {
242
248
  if (opts.appIds.length !== 1)
243
249
  return null;
244
250
  const appId = opts.appIds[0];
251
+ // ASK THE RELAY where the app lives. This was reconstructed as `${base}/a/${appId}/`,
252
+ // which is wrong in two ways at once: an app is served under its SLUG, not its id,
253
+ // and on a usercontent deployment it is a different origin entirely
254
+ // (`<slug>.homespunapps.com`) rather than a path on the API host. The socket
255
+ // therefore never connected, and the reconnect logic below reported it as
256
+ // "wake socket lost", which reads like a transient blip rather than a URL that
257
+ // could never work. Found by running a worker against a live relay; no test
258
+ // covered this function at all.
259
+ //
260
+ // A failure here is NOT fatal: polling still drains the queue, so a worker that
261
+ // cannot resolve its app keeps working, just without the wake hint.
262
+ let appUrl;
263
+ try {
264
+ const res = await fetch(`${base}/v1/apps/${appId}`, {
265
+ headers: { authorization: `Bearer ${apiKey}` },
266
+ });
267
+ if (!res.ok) {
268
+ warn(`cannot resolve app ${appId} for the wake socket (${res.status}); polling only`);
269
+ return null;
270
+ }
271
+ appUrl = (await res.json()).url ?? "";
272
+ if (!appUrl) {
273
+ warn(`app ${appId} reports no url; polling only`);
274
+ return null;
275
+ }
276
+ }
277
+ catch (err) {
278
+ warn(`cannot resolve app ${appId} for the wake socket (${err instanceof Error ? err.message : String(err)}); polling only`);
279
+ return null;
280
+ }
245
281
  let closed = false;
282
+ let connectedOnce = false;
246
283
  let delay = RECONNECT_MIN_MS;
247
284
  let handle = null;
248
285
  let announcedOutage = false;
249
286
  const connect = () => {
250
287
  if (closed)
251
288
  return;
252
- const wsUrl = appWsUrlFromAppUrl(`${base}/a/${appId}/`);
253
- handle = openAppStream({ wsUrl, apiKey, since: Number.MAX_SAFE_INTEGER }, {
289
+ const wsUrl = appWsUrlFromAppUrl(appUrl);
290
+ handle = openAppStream(
291
+ // `since: 0`, not a huge sentinel. The intent was "do not replay history", and
292
+ // MAX_SAFE_INTEGER expressed that by sending a cursor the relay will not accept,
293
+ // so the socket was refused and the wake frame never arrived. The worker sets no
294
+ // `onEntry` handler at all, so a replayed batch is discarded as it is parsed:
295
+ // there is nothing to avoid.
296
+ { wsUrl, apiKey, since: 0 }, {
254
297
  onHello: () => {
255
298
  // A successful connect resets the backoff, so a flapping link does not
256
299
  // inherit the previous outage's delay.
300
+ connectedOnce = true;
257
301
  delay = RECONNECT_MIN_MS;
258
302
  if (announcedOutage) {
259
303
  warn("wake socket reconnected");
@@ -261,15 +305,21 @@ function openWakeSocket(opts, apiKey, base, onWake) {
261
305
  }
262
306
  },
263
307
  onAgentTaskAvailable: onWake,
264
- onClose: () => scheduleReconnect(),
265
- onError: () => scheduleReconnect(),
308
+ onClose: ({ code, reason }) => scheduleReconnect(`closed ${code}${reason ? ": " + reason : ""}`),
309
+ // The error is REPORTED, not swallowed. Discarding it is what made the
310
+ // original URL bug take three guesses to find: every failure looked
311
+ // identical from the outside.
312
+ onError: (err) => scheduleReconnect(err instanceof Error ? err.message : String(err)),
266
313
  });
267
314
  };
268
- const scheduleReconnect = () => {
315
+ const scheduleReconnect = (why) => {
269
316
  if (closed)
270
317
  return;
271
318
  if (!announcedOutage) {
272
- warn("wake socket lost; polling continues while it reconnects");
319
+ warn((connectedOnce
320
+ ? "wake socket lost; polling continues while it reconnects"
321
+ : "wake socket could not connect; polling only until it does") +
322
+ ` (${why})`);
273
323
  announcedOutage = true;
274
324
  }
275
325
  const wait = delay;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homespunapps/cli",
3
- "version": "1.6.53",
3
+ "version": "1.6.54",
4
4
  "description": "Command-line client for the Homespun relay: deploy a real multi-user web app from your agent, then keep reading and writing its data.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "test:unit": "vitest run"
37
37
  },
38
38
  "dependencies": {
39
- "@homespunapps/core": "^1.6.53",
39
+ "@homespunapps/core": "^1.6.54",
40
40
  "qrcode-terminal": "^0.12.0"
41
41
  },
42
42
  "devDependencies": {