@agentdeck/bridge 1.0.21 → 1.0.23

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 (102) hide show
  1. package/dist/apme/activity-history.d.ts +59 -0
  2. package/dist/apme/activity-history.d.ts.map +1 -0
  3. package/dist/apme/activity-history.js +265 -0
  4. package/dist/apme/activity-history.js.map +1 -0
  5. package/dist/apme/dashboard-html.d.ts.map +1 -1
  6. package/dist/apme/dashboard-html.js +40 -6
  7. package/dist/apme/dashboard-html.js.map +1 -1
  8. package/dist/apme/http.d.ts +1 -0
  9. package/dist/apme/http.d.ts.map +1 -1
  10. package/dist/apme/http.js +9 -0
  11. package/dist/apme/http.js.map +1 -1
  12. package/dist/apme/runner.d.ts +20 -0
  13. package/dist/apme/runner.d.ts.map +1 -1
  14. package/dist/apme/runner.js +57 -0
  15. package/dist/apme/runner.js.map +1 -1
  16. package/dist/apme/settings.d.ts +21 -4
  17. package/dist/apme/settings.d.ts.map +1 -1
  18. package/dist/apme/settings.js +29 -5
  19. package/dist/apme/settings.js.map +1 -1
  20. package/dist/apme/store.d.ts +7 -0
  21. package/dist/apme/store.d.ts.map +1 -1
  22. package/dist/apme/store.js +35 -0
  23. package/dist/apme/store.js.map +1 -1
  24. package/dist/ble-sync-spawn.d.ts +27 -0
  25. package/dist/ble-sync-spawn.d.ts.map +1 -1
  26. package/dist/ble-sync-spawn.js +32 -0
  27. package/dist/ble-sync-spawn.js.map +1 -1
  28. package/dist/bridge-core.d.ts +21 -4
  29. package/dist/bridge-core.d.ts.map +1 -1
  30. package/dist/bridge-core.js +52 -23
  31. package/dist/bridge-core.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +261 -13
  34. package/dist/cli.js.map +1 -1
  35. package/dist/codex-rate-limits-live.d.ts +58 -9
  36. package/dist/codex-rate-limits-live.d.ts.map +1 -1
  37. package/dist/codex-rate-limits-live.js +64 -15
  38. package/dist/codex-rate-limits-live.js.map +1 -1
  39. package/dist/codex-rate-limits.d.ts +2 -2
  40. package/dist/codex-rate-limits.d.ts.map +1 -1
  41. package/dist/codex-rate-limits.js +34 -16
  42. package/dist/codex-rate-limits.js.map +1 -1
  43. package/dist/daemon-server.d.ts +46 -2
  44. package/dist/daemon-server.d.ts.map +1 -1
  45. package/dist/daemon-server.js +165 -44
  46. package/dist/daemon-server.js.map +1 -1
  47. package/dist/daemon-takeover.d.ts +3 -0
  48. package/dist/daemon-takeover.d.ts.map +1 -1
  49. package/dist/daemon-takeover.js +5 -0
  50. package/dist/daemon-takeover.js.map +1 -1
  51. package/dist/daemon.js +1 -1
  52. package/dist/esp32-flash-lease.d.ts +47 -0
  53. package/dist/esp32-flash-lease.d.ts.map +1 -0
  54. package/dist/esp32-flash-lease.js +84 -0
  55. package/dist/esp32-flash-lease.js.map +1 -0
  56. package/dist/esp32-flash-transport.d.ts +90 -0
  57. package/dist/esp32-flash-transport.d.ts.map +1 -0
  58. package/dist/esp32-flash-transport.js +157 -0
  59. package/dist/esp32-flash-transport.js.map +1 -0
  60. package/dist/esp32-flash.d.ts +171 -0
  61. package/dist/esp32-flash.d.ts.map +1 -0
  62. package/dist/esp32-flash.js +523 -0
  63. package/dist/esp32-flash.js.map +1 -0
  64. package/dist/esp32-serial.d.ts +9 -1
  65. package/dist/esp32-serial.d.ts.map +1 -1
  66. package/dist/esp32-serial.js +74 -1
  67. package/dist/esp32-serial.js.map +1 -1
  68. package/dist/esptool-js-node.d.ts +17 -0
  69. package/dist/esptool-js-node.d.ts.map +1 -0
  70. package/dist/esptool-js-node.js +17 -0
  71. package/dist/esptool-js-node.js.map +1 -0
  72. package/dist/hook-server.d.ts.map +1 -1
  73. package/dist/hook-server.js +17 -0
  74. package/dist/hook-server.js.map +1 -1
  75. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  76. package/dist/idotmatrix/idotmatrix-daemon-sync.js +13 -1
  77. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +4 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/python-ble-runtime.d.ts +2 -0
  82. package/dist/python-ble-runtime.d.ts.map +1 -1
  83. package/dist/python-ble-runtime.js +101 -7
  84. package/dist/python-ble-runtime.js.map +1 -1
  85. package/dist/review-runner.d.ts.map +1 -1
  86. package/dist/review-runner.js +27 -9
  87. package/dist/review-runner.js.map +1 -1
  88. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  89. package/dist/timebox/timebox-daemon-sync.js +11 -1
  90. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  91. package/dist/timeline-summarizer.d.ts +7 -2
  92. package/dist/timeline-summarizer.d.ts.map +1 -1
  93. package/dist/timeline-summarizer.js +13 -69
  94. package/dist/timeline-summarizer.js.map +1 -1
  95. package/dist/usage-api.d.ts +28 -1
  96. package/dist/usage-api.d.ts.map +1 -1
  97. package/dist/usage-api.js +74 -33
  98. package/dist/usage-api.js.map +1 -1
  99. package/dist/usage-event.d.ts.map +1 -1
  100. package/dist/usage-event.js +2 -17
  101. package/dist/usage-event.js.map +1 -1
  102. package/package.json +6 -4
@@ -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';
@@ -102,7 +102,8 @@ import { codexTurnOutcomeFromRollout, lastAgentMessageFromCodexRollout } from '.
102
102
  import { callFoundationModelsHelper } from './foundation-models-helper.js';
103
103
  import { initModules, stopModules, createDefaultModules, } from './modules/index.js';
104
104
  import { bindHostFor, daemonModuleConfigs, describeDaemonPosture, resolveDaemonPosture, } from './network-posture.js';
105
- import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendAuthProvisionToAll, sendWifiProvision, sendWifiProvisionToAll, handleESP32Wake, getESP32Ports, getSerialConnectionStatus, getSerialLastError, getSerialReachableBoards } from './esp32-serial.js';
105
+ import { esp32ConnectionCount, getESP32DeviceInfo, onESP32Message, sendAuthProvisionToAll, sendWifiProvision, sendWifiProvisionToAll, handleESP32Wake, getESP32Ports, getSerialConnectionStatus, getSerialLastError, getSerialReachableBoards, releaseESP32SerialPorts } from './esp32-serial.js';
106
+ import { clampLeaseSeconds, clearLease, readLease, writeLease } from './esp32-flash-lease.js';
106
107
  import { loadWifiConfig } from './wifi-config.js';
107
108
  import { getConnectedAdbDevices, hasAdb, getAdbDeviceCount } from './adb-reverse.js';
108
109
  import { getPixooDeviceDetails, pixooDeviceCount } from './pixoo/pixoo-bridge.js';
@@ -201,16 +202,50 @@ function saveStagedFw() {
201
202
  log(`[agentdeck] staged-fw persist failed: ${err instanceof Error ? err.message : String(err)}`);
202
203
  }
203
204
  }
205
+ /** Resolve a stage target (canonical board, registry key, or IP) to the board
206
+ * string the advert is keyed on.
207
+ *
208
+ * This map is read back by `stagedFwAdvert(device_info.board)`, so anything
209
+ * else stored as the key is an advert that can never fire. Staging with an IP
210
+ * is not a misuse to reject — it is the documented way to name one unit when
211
+ * two share a `board` string (see esp32-ota's ambiguity error) — so normalize
212
+ * it here instead. Consequence worth stating: the feed pull carries only a
213
+ * board, so a stage aimed at one unit adverts to every unit of that board.
214
+ * They run the same image, so that is correct rather than merely tolerable.
215
+ *
216
+ * Unlike `findWifiOtaTarget` this must NOT require an open socket: a
217
+ * wake-sync-sleep board holds none by design, which is the entire reason
218
+ * staging exists. An unrecognized target is kept verbatim — staging for a
219
+ * board this daemon has not met yet is legitimate. */
220
+ function resolveStagedFwBoard(target) {
221
+ for (const [key, device] of wifiEsp32Devices) {
222
+ if (key === target || device.board === target || device.ip === target)
223
+ return device.board;
224
+ }
225
+ // A pull-sync client ages out of the WS roster between wakes; the feed
226
+ // tracker keeps its IP→board memory precisely for that gap.
227
+ const puller = feedPulls.clients().find((c) => c.client === target || c.board === target);
228
+ return puller?.board ?? target;
229
+ }
204
230
  /** Stage a build for a board. Reads the file once to fingerprint it; the file
205
231
  * 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) {
232
+ * re-fingerprinted by re-staging).
233
+ *
234
+ * `pullSeen` reports whether this daemon has actually observed a feed pull
235
+ * from the board, because "staged" proves neither transfer nor install and a
236
+ * bare success reads as if it did. Only pull-sync firmware fetches a staged
237
+ * image; a board that holds a live WS never asks, so its stage sits forever
238
+ * while the CLI says it is handled. It is daemon-lifetime state, so `false`
239
+ * means "not seen since this daemon started" — not "cannot pull". */
240
+ function stageEsp32Fw(target, firmwarePath) {
241
+ const board = resolveStagedFwBoard(target);
208
242
  const firmware = readFileSync(firmwarePath);
209
243
  const md5 = createHash('md5').update(firmware).digest('hex');
210
244
  stagedFwByBoard.set(board, { firmwarePath, md5, size: firmware.length, stagedAt: Date.now() });
211
245
  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 };
246
+ const pullSeen = feedPulls.clients().some((c) => c.board === board);
247
+ 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'}`);
248
+ return { board, bytes: firmware.length, md5, pullSeen };
214
249
  }
215
250
  /** The staged advert for a feed response, re-validated against the file on
216
251
  * disk so a rebuilt-in-place binary never adverts a stale md5. */
@@ -490,6 +525,11 @@ export const __wifiOtaTestApi = {
490
525
  unregisterWifiEsp32Socket,
491
526
  handleEsp32OtaReply,
492
527
  performWifiEsp32Ota,
528
+ resolveStagedFwBoard,
529
+ stageEsp32Fw,
530
+ stagedFwAdvert,
531
+ feedPulls,
532
+ clearStagedFwForTest: () => { stagedFwByBoard.clear(); },
493
533
  };
494
534
  async function performWifiEsp32Ota(core, target, firmwarePath) {
495
535
  const { key, device } = findWifiOtaTarget(target);
@@ -679,13 +719,15 @@ async function fetchUsageViaWs(siblings) {
679
719
  export async function fetchUsageRelayed(selfPort) {
680
720
  const sessions = listActiveSessions();
681
721
  const siblings = sessions.filter(s => s.port !== selfPort && s.agentType !== 'daemon');
722
+ // A sibling's reading is a real successful one, not a failure fallback, so it
723
+ // is `fresh` — its age contract is the 5-minute gate inside fetchUsageViaHttp.
682
724
  if (siblings.length > 0) {
683
725
  const httpResult = await fetchUsageViaHttp(siblings);
684
726
  if (httpResult)
685
- return httpResult.usage;
727
+ return { data: httpResult.usage, fresh: true };
686
728
  const wsResult = await fetchUsageViaWs(siblings);
687
729
  if (wsResult)
688
- return wsResult;
730
+ return { data: wsResult, fresh: true };
689
731
  // Relay is only an optimization to avoid multiple API pollers. A PTY session
690
732
  // bridge only refreshes usage when it has *direct* inbound WS clients, but
691
733
  // dashboards/Stream Deck connect to the daemon (sole entry point) — so a
@@ -1093,6 +1135,10 @@ export async function startDaemon(opts) {
1093
1135
  /** Produces live timeline rows for observed Kiro sessions — see the class
1094
1136
  * doc for why Kiro needs a producer when hook agents do not. */
1095
1137
  const kiroTimelineFeed = new KiroTimelineFeed();
1138
+ /** Kiro has no CLI hooks, so transcript rows also feed APME. These maps are
1139
+ * bounded by the observer's current live set and discarded on disappearance. */
1140
+ const kiroApmeSessions = new Set();
1141
+ const kiroApmeResponseBySession = new Map();
1096
1142
  /** Observed Claude sessions whose AskUserQuestion PreToolUse is held open for
1097
1143
  * a device answer, keyed by the bare Claude session UUID. Its presence is
1098
1144
  * what makes the session's options pressable (`liveAnswerable`) and what
@@ -1606,6 +1652,62 @@ export async function startDaemon(opts) {
1606
1652
  }
1607
1653
  return;
1608
1654
  }
1655
+ // ===== Serial suspend / resume (USB flashing) =====
1656
+ // Both sit BEHIND the normal auth gate — `isAuthorizedHttpRequest()` treats
1657
+ // same-machine as authenticated, which is exactly the audience here, so
1658
+ // this adds no standing unauthenticated route. The one always-on pre-auth
1659
+ // route is still `GET /health`.
1660
+ //
1661
+ // `agentdeck esp32 flash` calls suspend, flashes, and calls resume in a
1662
+ // `finally`. Neither call is load-bearing on its own: expiry is enforced
1663
+ // when the lease is READ, so a CLI that is killed mid-flash recovers with
1664
+ // nothing running.
1665
+ if (req.method === 'POST' && pathname === '/esp32/serial/suspend') {
1666
+ (async () => {
1667
+ const body = await readJsonBody(req);
1668
+ const seconds = clampLeaseSeconds(body.seconds ?? 120);
1669
+ const lease = writeLease({
1670
+ until: Date.now() + seconds * 1000,
1671
+ reason: typeof body.reason === 'string' ? body.reason : 'usb flash',
1672
+ pid: typeof body.pid === 'number' ? body.pid : undefined,
1673
+ board: typeof body.board === 'string' ? body.board : undefined,
1674
+ });
1675
+ // Refusing to OPEN is only half of it — a port this daemon already
1676
+ // holds still blocks the flasher, and two readers on one TTY steal
1677
+ // each other's bytes instead of failing cleanly.
1678
+ const released = releaseESP32SerialPorts(`flash lease (${lease.board ?? 'usb flash'})`);
1679
+ return { ok: true, until: lease.until, seconds, released };
1680
+ })().then((result) => {
1681
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1682
+ res.end(JSON.stringify(result));
1683
+ }).catch((err) => {
1684
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1685
+ res.end(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }));
1686
+ });
1687
+ return;
1688
+ }
1689
+ if (req.method === 'POST' && pathname === '/esp32/serial/resume') {
1690
+ // Idempotent by contract: resuming when nothing is suspended is a
1691
+ // success. The CLI calls this from a `finally`, where it may well run
1692
+ // after the lease has already expired on its own.
1693
+ const was = readLease();
1694
+ clearLease();
1695
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1696
+ res.end(JSON.stringify({ ok: true, wasSuspended: was !== null }));
1697
+ return;
1698
+ }
1699
+ if (req.method === 'GET' && pathname === '/esp32/serial/status') {
1700
+ const lease = readLease();
1701
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1702
+ res.end(JSON.stringify({
1703
+ ok: true,
1704
+ suspended: lease !== null,
1705
+ until: lease?.until,
1706
+ board: lease?.board,
1707
+ ports: getESP32Ports(),
1708
+ }));
1709
+ return;
1710
+ }
1609
1711
  if (req.method === 'POST' && pathname === '/esp32/ota') {
1610
1712
  (async () => {
1611
1713
  const body = await readJsonBody(req);
@@ -3074,15 +3176,7 @@ export async function startDaemon(opts) {
3074
3176
  // Reset backoff from pre-sleep failures and fetch fresh usage after network stabilizes
3075
3177
  resetConsecutiveFailures();
3076
3178
  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
- });
3179
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
3086
3180
  }, 4000);
3087
3181
  });
3088
3182
  // Subscribe to sibling session bridges' timelines + modelCatalog relay
@@ -3177,6 +3271,19 @@ export async function startDaemon(opts) {
3177
3271
  // So split the event by half instead of by event: the ACCOUNT half (quota,
3178
3272
  // scoped caps, extra usage, subscriptions, Codex/Ollama, usageStale) comes
3179
3273
  // from the daemon's authoritative aggregate, the SESSION half from the relay.
3274
+ //
3275
+ // That split applies on the `else` branch ONLY. When the relay carries
3276
+ // Claude percentages its event goes out verbatim, so the account half —
3277
+ // `codexRateLimits` included — is the BRIDGE's. Usually identical: both
3278
+ // sides run the same plan-aware rollout read. They can differ only when no
3279
+ // plan-matching rollout exists at all, since the daemon additionally holds
3280
+ // a `codex app-server` reading that a session bridge structurally cannot
3281
+ // (no host processes there) — then successive events alternate between a
3282
+ // real Codex window and a voided one. Left as is deliberately: the only
3283
+ // targeted fix is to overwrite the block from `core.buildUsage()` here, and
3284
+ // that is the call which arms the throttled spawn, on the one path whose
3285
+ // whole history is about not clobbering the dashboard. Tracked as issue
3286
+ // #253, not just as this comment.
3180
3287
  if (hasClaudeData) {
3181
3288
  core.wsServer.broadcast(evt);
3182
3289
  }
@@ -3466,11 +3573,46 @@ export async function startDaemon(opts) {
3466
3573
  // beside it stays empty. #218 taught the per-session QUERY to read Kiro's
3467
3574
  // own transcript; the main timeline is a STREAM, and this is what feeds it.
3468
3575
  try {
3469
- const kiroIds = passiveSessionObserver.collect([])
3470
- .filter((s) => typeof s.agentType === 'string' && s.agentType.startsWith('kiro'))
3471
- .map((s) => s.id);
3576
+ const observedKiro = passiveSessionObserver.collect([])
3577
+ .filter((s) => typeof s.agentType === 'string' && s.agentType.startsWith('kiro'));
3578
+ const kiroIds = observedKiro.map((s) => s.id);
3579
+ const projectBySession = new Map(observedKiro.map((s) => [rawSessionId(s.id), s.projectName]));
3580
+ const currentSessions = new Set(kiroIds.map(rawSessionId));
3581
+ for (const sessionId of kiroApmeSessions) {
3582
+ if (currentSessions.has(sessionId))
3583
+ continue;
3584
+ apme?.collector.closeRun(sessionId);
3585
+ kiroApmeSessions.delete(sessionId);
3586
+ kiroApmeResponseBySession.delete(sessionId);
3587
+ }
3472
3588
  for (const entry of kiroTimelineFeed.pump(kiroIds)) {
3473
3589
  core.bridgeTimeline.addEntry(entry);
3590
+ if (!apme || !entry.sessionId)
3591
+ continue;
3592
+ const sessionId = rawSessionId(entry.sessionId);
3593
+ if (entry.type === 'chat_start') {
3594
+ if (!apme.collector.getRunId(sessionId)) {
3595
+ apme.collector.openRun({
3596
+ sessionId,
3597
+ agentType: 'kiro-cli',
3598
+ projectName: projectBySession.get(sessionId) ?? undefined,
3599
+ });
3600
+ }
3601
+ kiroApmeSessions.add(sessionId);
3602
+ kiroApmeResponseBySession.delete(sessionId);
3603
+ apme.collector.ingestHook(sessionId, 'user_prompt_submit', {
3604
+ prompt: entry.detail || entry.raw,
3605
+ agent_type: 'kiro-cli',
3606
+ project_name: projectBySession.get(sessionId),
3607
+ });
3608
+ }
3609
+ else if (entry.type === 'chat_response' && apme.collector.getRunId(sessionId)) {
3610
+ const piece = entry.detail || entry.raw;
3611
+ const response = [kiroApmeResponseBySession.get(sessionId), piece].filter(Boolean).join('\n');
3612
+ kiroApmeResponseBySession.set(sessionId, response);
3613
+ apme.collector.setTurnResponse(sessionId, response);
3614
+ apme.collector.noteTurnStop(sessionId);
3615
+ }
3474
3616
  }
3475
3617
  }
3476
3618
  catch (err) {
@@ -4458,12 +4600,7 @@ export async function startDaemon(opts) {
4458
4600
  return;
4459
4601
  }
4460
4602
  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
- });
4603
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
4467
4604
  }
4468
4605
  };
4469
4606
  // ── Device-sourced voice ──────────────────────────────────────────────
@@ -5286,15 +5423,7 @@ export async function startDaemon(opts) {
5286
5423
  // Fetch usage on connect if stale
5287
5424
  const cacheAge = Date.now() - core.lastApiFetchTime;
5288
5425
  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
- });
5426
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
5298
5427
  }
5299
5428
  });
5300
5429
  core.wsServer.onClientDisconnect((ws) => {
@@ -5511,15 +5640,7 @@ export async function startDaemon(opts) {
5511
5640
  }
5512
5641
  // Initial usage fetch (delayed 10s)
5513
5642
  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
- });
5643
+ fetchUsageRelayed(port).then((result) => core.applyUsageResult(result));
5523
5644
  }, 10_000));
5524
5645
  // Backstop sweep for held PreToolUse approval responses whose per-entry timer
5525
5646
  // somehow didn't fire — resolves anything older than 60s to "ask" so a held