@agentdeck/bridge 1.0.21 → 1.0.22

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 (48) hide show
  1. package/dist/ble-sync-spawn.d.ts +27 -0
  2. package/dist/ble-sync-spawn.d.ts.map +1 -1
  3. package/dist/ble-sync-spawn.js +32 -0
  4. package/dist/ble-sync-spawn.js.map +1 -1
  5. package/dist/bridge-core.d.ts +21 -4
  6. package/dist/bridge-core.d.ts.map +1 -1
  7. package/dist/bridge-core.js +52 -23
  8. package/dist/bridge-core.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +13 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/codex-rate-limits-live.d.ts +58 -9
  13. package/dist/codex-rate-limits-live.d.ts.map +1 -1
  14. package/dist/codex-rate-limits-live.js +64 -15
  15. package/dist/codex-rate-limits-live.js.map +1 -1
  16. package/dist/codex-rate-limits.d.ts +2 -2
  17. package/dist/codex-rate-limits.d.ts.map +1 -1
  18. package/dist/codex-rate-limits.js +34 -16
  19. package/dist/codex-rate-limits.js.map +1 -1
  20. package/dist/daemon-server.d.ts +46 -2
  21. package/dist/daemon-server.d.ts.map +1 -1
  22. package/dist/daemon-server.js +65 -40
  23. package/dist/daemon-server.js.map +1 -1
  24. package/dist/daemon.js +1 -1
  25. package/dist/hook-server.d.ts.map +1 -1
  26. package/dist/hook-server.js +17 -0
  27. package/dist/hook-server.js.map +1 -1
  28. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  29. package/dist/idotmatrix/idotmatrix-daemon-sync.js +13 -1
  30. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/python-ble-runtime.d.ts +2 -0
  35. package/dist/python-ble-runtime.d.ts.map +1 -1
  36. package/dist/python-ble-runtime.js +101 -7
  37. package/dist/python-ble-runtime.js.map +1 -1
  38. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  39. package/dist/timebox/timebox-daemon-sync.js +11 -1
  40. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  41. package/dist/usage-api.d.ts +28 -1
  42. package/dist/usage-api.d.ts.map +1 -1
  43. package/dist/usage-api.js +74 -33
  44. package/dist/usage-api.js.map +1 -1
  45. package/dist/usage-event.d.ts.map +1 -1
  46. package/dist/usage-event.js +2 -17
  47. package/dist/usage-event.js.map +1 -1
  48. package/package.json +3 -3
@@ -76,7 +76,7 @@ import { listActive as listActiveSessions, findAvailablePort, findExistingDaemon
76
76
  import { isForeignDaemon } from './daemon-takeover.js';
77
77
  import { loadDaemonSettings } from './daemon-settings.js';
78
78
  import { resolveDaemonPort, describeDaemonPortSource, PREFERRED_PORT_RECLAIM_MS, } from './daemon-port.js';
79
- import { fetchUsageFromApi, hasOAuthToken, resetConsecutiveFailures } from './usage-api.js';
79
+ import { fetchUsageFromApi, resetConsecutiveFailures } from './usage-api.js';
80
80
  import { getOrCreateToken, isLocalConnection, validateToken } from './auth.js';
81
81
  import { buildPublicHealth, gateHttpRequest, isAuthorizedHttpRequest } from './http-auth-gate.js';
82
82
  import { closePairingWindow, getPairingWindowStatus, openPairingWindow, pairingWindowOpen, redeemPairingCode, } from './pairing-window.js';
@@ -201,16 +201,50 @@ function saveStagedFw() {
201
201
  log(`[agentdeck] staged-fw persist failed: ${err instanceof Error ? err.message : String(err)}`);
202
202
  }
203
203
  }
204
+ /** Resolve a stage target (canonical board, registry key, or IP) to the board
205
+ * string the advert is keyed on.
206
+ *
207
+ * This map is read back by `stagedFwAdvert(device_info.board)`, so anything
208
+ * else stored as the key is an advert that can never fire. Staging with an IP
209
+ * is not a misuse to reject — it is the documented way to name one unit when
210
+ * two share a `board` string (see esp32-ota's ambiguity error) — so normalize
211
+ * it here instead. Consequence worth stating: the feed pull carries only a
212
+ * board, so a stage aimed at one unit adverts to every unit of that board.
213
+ * They run the same image, so that is correct rather than merely tolerable.
214
+ *
215
+ * Unlike `findWifiOtaTarget` this must NOT require an open socket: a
216
+ * wake-sync-sleep board holds none by design, which is the entire reason
217
+ * staging exists. An unrecognized target is kept verbatim — staging for a
218
+ * board this daemon has not met yet is legitimate. */
219
+ function resolveStagedFwBoard(target) {
220
+ for (const [key, device] of wifiEsp32Devices) {
221
+ if (key === target || device.board === target || device.ip === target)
222
+ return device.board;
223
+ }
224
+ // A pull-sync client ages out of the WS roster between wakes; the feed
225
+ // tracker keeps its IP→board memory precisely for that gap.
226
+ const puller = feedPulls.clients().find((c) => c.client === target || c.board === target);
227
+ return puller?.board ?? target;
228
+ }
204
229
  /** Stage a build for a board. Reads the file once to fingerprint it; the file
205
230
  * itself is re-read at device download time (a rebuild at the same path is
206
- * re-fingerprinted by re-staging). */
207
- function stageEsp32Fw(board, firmwarePath) {
231
+ * re-fingerprinted by re-staging).
232
+ *
233
+ * `pullSeen` reports whether this daemon has actually observed a feed pull
234
+ * from the board, because "staged" proves neither transfer nor install and a
235
+ * bare success reads as if it did. Only pull-sync firmware fetches a staged
236
+ * image; a board that holds a live WS never asks, so its stage sits forever
237
+ * while the CLI says it is handled. It is daemon-lifetime state, so `false`
238
+ * means "not seen since this daemon started" — not "cannot pull". */
239
+ function stageEsp32Fw(target, firmwarePath) {
240
+ const board = resolveStagedFwBoard(target);
208
241
  const firmware = readFileSync(firmwarePath);
209
242
  const md5 = createHash('md5').update(firmware).digest('hex');
210
243
  stagedFwByBoard.set(board, { firmwarePath, md5, size: firmware.length, stagedAt: Date.now() });
211
244
  saveStagedFw();
212
- log(`[agentdeck] staged firmware for ${board}: ${firmware.length} bytes md5=${md5} — installs on its next feed pull`);
213
- return { board, bytes: firmware.length, md5 };
245
+ const pullSeen = feedPulls.clients().some((c) => c.board === board);
246
+ log(`[agentdeck] staged firmware for ${board}: ${firmware.length} bytes md5=${md5} — ${pullSeen ? 'installs on its next feed pull' : 'NO feed pull seen from this board since daemon start'}`);
247
+ return { board, bytes: firmware.length, md5, pullSeen };
214
248
  }
215
249
  /** The staged advert for a feed response, re-validated against the file on
216
250
  * disk so a rebuilt-in-place binary never adverts a stale md5. */
@@ -490,6 +524,11 @@ export const __wifiOtaTestApi = {
490
524
  unregisterWifiEsp32Socket,
491
525
  handleEsp32OtaReply,
492
526
  performWifiEsp32Ota,
527
+ resolveStagedFwBoard,
528
+ stageEsp32Fw,
529
+ stagedFwAdvert,
530
+ feedPulls,
531
+ clearStagedFwForTest: () => { stagedFwByBoard.clear(); },
493
532
  };
494
533
  async function performWifiEsp32Ota(core, target, firmwarePath) {
495
534
  const { key, device } = findWifiOtaTarget(target);
@@ -679,13 +718,15 @@ async function fetchUsageViaWs(siblings) {
679
718
  export async function fetchUsageRelayed(selfPort) {
680
719
  const sessions = listActiveSessions();
681
720
  const siblings = sessions.filter(s => s.port !== selfPort && s.agentType !== 'daemon');
721
+ // A sibling's reading is a real successful one, not a failure fallback, so it
722
+ // is `fresh` — its age contract is the 5-minute gate inside fetchUsageViaHttp.
682
723
  if (siblings.length > 0) {
683
724
  const httpResult = await fetchUsageViaHttp(siblings);
684
725
  if (httpResult)
685
- return httpResult.usage;
726
+ return { data: httpResult.usage, fresh: true };
686
727
  const wsResult = await fetchUsageViaWs(siblings);
687
728
  if (wsResult)
688
- return wsResult;
729
+ return { data: wsResult, fresh: true };
689
730
  // Relay is only an optimization to avoid multiple API pollers. A PTY session
690
731
  // bridge only refreshes usage when it has *direct* inbound WS clients, but
691
732
  // dashboards/Stream Deck connect to the daemon (sole entry point) — so a
@@ -3074,15 +3115,7 @@ export async function startDaemon(opts) {
3074
3115
  // Reset backoff from pre-sleep failures and fetch fresh usage after network stabilizes
3075
3116
  resetConsecutiveFailures();
3076
3117
  setTimeout(() => {
3077
- fetchUsageRelayed(port).then((usage) => {
3078
- if (usage)
3079
- core.updateApiUsage(usage);
3080
- else {
3081
- core.oauthConnected = hasOAuthToken();
3082
- if (core.cachedApiUsage)
3083
- core.apiUsageStale = true;
3084
- }
3085
- });
3118
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
3086
3119
  }, 4000);
3087
3120
  });
3088
3121
  // Subscribe to sibling session bridges' timelines + modelCatalog relay
@@ -3177,6 +3210,19 @@ export async function startDaemon(opts) {
3177
3210
  // So split the event by half instead of by event: the ACCOUNT half (quota,
3178
3211
  // scoped caps, extra usage, subscriptions, Codex/Ollama, usageStale) comes
3179
3212
  // from the daemon's authoritative aggregate, the SESSION half from the relay.
3213
+ //
3214
+ // That split applies on the `else` branch ONLY. When the relay carries
3215
+ // Claude percentages its event goes out verbatim, so the account half —
3216
+ // `codexRateLimits` included — is the BRIDGE's. Usually identical: both
3217
+ // sides run the same plan-aware rollout read. They can differ only when no
3218
+ // plan-matching rollout exists at all, since the daemon additionally holds
3219
+ // a `codex app-server` reading that a session bridge structurally cannot
3220
+ // (no host processes there) — then successive events alternate between a
3221
+ // real Codex window and a voided one. Left as is deliberately: the only
3222
+ // targeted fix is to overwrite the block from `core.buildUsage()` here, and
3223
+ // that is the call which arms the throttled spawn, on the one path whose
3224
+ // whole history is about not clobbering the dashboard. Tracked as issue
3225
+ // #253, not just as this comment.
3180
3226
  if (hasClaudeData) {
3181
3227
  core.wsServer.broadcast(evt);
3182
3228
  }
@@ -4458,12 +4504,7 @@ export async function startDaemon(opts) {
4458
4504
  return;
4459
4505
  }
4460
4506
  if (cmd.type === 'query_usage') {
4461
- fetchUsageRelayed(port).then((usage) => {
4462
- if (usage)
4463
- core.updateApiUsage(usage);
4464
- else if (core.cachedApiUsage)
4465
- core.apiUsageStale = true;
4466
- });
4507
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
4467
4508
  }
4468
4509
  };
4469
4510
  // ── Device-sourced voice ──────────────────────────────────────────────
@@ -5286,15 +5327,7 @@ export async function startDaemon(opts) {
5286
5327
  // Fetch usage on connect if stale
5287
5328
  const cacheAge = Date.now() - core.lastApiFetchTime;
5288
5329
  if (!core.cachedApiUsage || (core.lastApiFetchTime > 0 && cacheAge > 5 * 60 * 1000)) {
5289
- fetchUsageRelayed(port).then((usage) => {
5290
- if (usage)
5291
- core.updateApiUsage(usage);
5292
- else {
5293
- core.oauthConnected = hasOAuthToken();
5294
- if (core.cachedApiUsage)
5295
- core.apiUsageStale = true;
5296
- }
5297
- });
5330
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
5298
5331
  }
5299
5332
  });
5300
5333
  core.wsServer.onClientDisconnect((ws) => {
@@ -5511,15 +5544,7 @@ export async function startDaemon(opts) {
5511
5544
  }
5512
5545
  // Initial usage fetch (delayed 10s)
5513
5546
  core.addTimeout(setTimeout(() => {
5514
- fetchUsageRelayed(port).then((usage) => {
5515
- if (usage)
5516
- core.updateApiUsage(usage);
5517
- else {
5518
- core.oauthConnected = hasOAuthToken();
5519
- if (core.cachedApiUsage)
5520
- core.apiUsageStale = true;
5521
- }
5522
- });
5547
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
5523
5548
  }, 10_000));
5524
5549
  // Backstop sweep for held PreToolUse approval responses whose per-entry timer
5525
5550
  // somehow didn't fire — resolves anything older than 60s to "ask" so a held