@humwick/runner 0.0.0-stage → 0.130.1

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 (293) hide show
  1. package/LICENSE +167 -0
  2. package/agent-bin/agent-relay-browser +10 -0
  3. package/agent-bin/humwick +10 -0
  4. package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
  5. package/hooks/fff-search-redirect/hooks.json +13 -0
  6. package/hooks/rg-r-guard/hooks.json +18 -0
  7. package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
  8. package/hooks/web-tools-redirect/hooks.json +13 -0
  9. package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
  10. package/node_modules/@humwick/offload/LICENSE +167 -0
  11. package/node_modules/@humwick/offload/README.md +57 -0
  12. package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
  13. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
  14. package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
  15. package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
  16. package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
  17. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
  18. package/node_modules/@humwick/offload/dist/envelope.js +80 -0
  19. package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
  20. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
  21. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
  22. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
  23. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
  24. package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
  25. package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
  26. package/node_modules/@humwick/offload/dist/index.js +5 -0
  27. package/node_modules/@humwick/offload/dist/index.js.map +1 -0
  28. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
  29. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
  30. package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
  31. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
  32. package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
  33. package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
  34. package/node_modules/@humwick/offload/dist/types.js +176 -0
  35. package/node_modules/@humwick/offload/dist/types.js.map +1 -0
  36. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
  37. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
  38. package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
  39. package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
  40. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
  41. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
  42. package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
  43. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
  44. package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
  45. package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
  46. package/node_modules/@humwick/offload/dist/worker.js +1 -0
  47. package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
  48. package/node_modules/@humwick/offload/package.json +42 -0
  49. package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
  50. package/node_modules/@humwick/offload/src/envelope.ts +92 -0
  51. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
  52. package/node_modules/@humwick/offload/src/index.ts +4 -0
  53. package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
  54. package/node_modules/@humwick/offload/src/types.ts +235 -0
  55. package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
  56. package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
  57. package/node_modules/@humwick/offload/src/worker.ts +5 -0
  58. package/package.json +47 -4
  59. package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
  60. package/plugins/claude/.claude-plugin/plugin.json +8 -0
  61. package/plugins/claude/claude-statusline-probe.cjs +144 -0
  62. package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
  63. package/plugins/claude/hooks/gate-run-guard.sh +9 -0
  64. package/plugins/claude/hooks/gate-run-guard.ts +237 -0
  65. package/plugins/claude/hooks/hooks.json +158 -0
  66. package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
  67. package/plugins/claude/hooks/kill-guard.sh +341 -0
  68. package/plugins/claude/hooks/kill-guard.ts +337 -0
  69. package/plugins/claude/hooks/permission-request.sh +22 -0
  70. package/plugins/claude/hooks/post-compact.sh +10 -0
  71. package/plugins/claude/hooks/post-tool-use.sh +8 -0
  72. package/plugins/claude/hooks/pre-compact.sh +12 -0
  73. package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
  74. package/plugins/claude/hooks/relay-status.sh +373 -0
  75. package/plugins/claude/hooks/session-end.sh +26 -0
  76. package/plugins/claude/hooks/session-start.sh +31 -0
  77. package/plugins/claude/hooks/shell-trust.ts +28 -0
  78. package/plugins/claude/hooks/stop-failure.sh +47 -0
  79. package/plugins/claude/hooks/stop.sh +48 -0
  80. package/plugins/claude/hooks/subagent-guard.cjs +26 -0
  81. package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
  82. package/plugins/claude/hooks/subagent-start.sh +13 -0
  83. package/plugins/claude/hooks/subagent-stop.sh +19 -0
  84. package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
  85. package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
  86. package/plugins/claude/monitors/monitors.json +7 -0
  87. package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
  88. package/plugins/claude/monitors/relay-monitor.ts +58 -0
  89. package/plugins/claude/skills/disconnect/SKILL.md +16 -0
  90. package/plugins/claude/skills/label/SKILL.md +23 -0
  91. package/plugins/claude/skills/message/SKILL.md +40 -0
  92. package/plugins/claude/skills/pair/SKILL.md +30 -0
  93. package/plugins/claude/skills/react/SKILL.md +18 -0
  94. package/plugins/claude/skills/read-message/SKILL.md +24 -0
  95. package/plugins/claude/skills/reply/SKILL.md +44 -0
  96. package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
  97. package/plugins/claude/skills/slice-first/SKILL.md +32 -0
  98. package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
  99. package/plugins/claude/skills/status/SKILL.md +16 -0
  100. package/plugins/claude/skills/tags/SKILL.md +25 -0
  101. package/plugins/claude/skills/task/SKILL.md +28 -0
  102. package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
  103. package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
  104. package/plugins/codex/skills/disconnect/SKILL.md +15 -0
  105. package/plugins/codex/skills/guide/SKILL.md +15 -0
  106. package/plugins/codex/skills/label/SKILL.md +22 -0
  107. package/plugins/codex/skills/message/SKILL.md +40 -0
  108. package/plugins/codex/skills/pair/SKILL.md +29 -0
  109. package/plugins/codex/skills/react/SKILL.md +17 -0
  110. package/plugins/codex/skills/read-message/SKILL.md +23 -0
  111. package/plugins/codex/skills/reply/SKILL.md +44 -0
  112. package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
  113. package/plugins/codex/skills/slice-first/SKILL.md +32 -0
  114. package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
  115. package/plugins/codex/skills/status/SKILL.md +15 -0
  116. package/plugins/codex/skills/tags/SKILL.md +24 -0
  117. package/plugins/codex/skills/task/SKILL.md +28 -0
  118. package/src/adapter.ts +1061 -0
  119. package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
  120. package/src/adapters/claude-api.ts +243 -0
  121. package/src/adapters/claude-context-command.ts +16 -0
  122. package/src/adapters/claude-credentials.ts +22 -0
  123. package/src/adapters/claude-delivery-lanes.ts +28 -0
  124. package/src/adapters/claude-delivery.ts +166 -0
  125. package/src/adapters/claude-effort.ts +94 -0
  126. package/src/adapters/claude-exit.ts +290 -0
  127. package/src/adapters/claude-headless-mirror.ts +88 -0
  128. package/src/adapters/claude-headless-refusal.ts +51 -0
  129. package/src/adapters/claude-headless.ts +617 -0
  130. package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
  131. package/src/adapters/claude-permission.ts +243 -0
  132. package/src/adapters/claude-prompt-gates.ts +269 -0
  133. package/src/adapters/claude-prompt-hook.ts +145 -0
  134. package/src/adapters/claude-session-capture.ts +293 -0
  135. package/src/adapters/claude-session-probe.ts +414 -0
  136. package/src/adapters/claude-shutdown.ts +190 -0
  137. package/src/adapters/claude-status-detectors.ts +426 -0
  138. package/src/adapters/claude-subagents.ts +93 -0
  139. package/src/adapters/claude-submit-confirmation.ts +287 -0
  140. package/src/adapters/claude-tmux.ts +548 -0
  141. package/src/adapters/claude-token-usage-capture.ts +253 -0
  142. package/src/adapters/claude-tool-policy.ts +165 -0
  143. package/src/adapters/claude-transcript-tail.ts +193 -0
  144. package/src/adapters/claude-transcript.ts +812 -0
  145. package/src/adapters/claude.ts +1097 -0
  146. package/src/adapters/codex-agent-message-capture.ts +189 -0
  147. package/src/adapters/codex-app-server.ts +70 -0
  148. package/src/adapters/codex-client.ts +362 -0
  149. package/src/adapters/codex-effective-config.ts +87 -0
  150. package/src/adapters/codex-launch-config.ts +153 -0
  151. package/src/adapters/codex-rollout-context.ts +272 -0
  152. package/src/adapters/codex-skill-discovery.ts +18 -0
  153. package/src/adapters/codex-token-usage-capture.ts +163 -0
  154. package/src/adapters/codex-tool-output.ts +53 -0
  155. package/src/adapters/codex-turn-outcome.ts +27 -0
  156. package/src/adapters/codex-version.ts +64 -0
  157. package/src/adapters/codex.ts +1442 -0
  158. package/src/adapters/incremental-file.ts +110 -0
  159. package/src/adapters/opencode-api.ts +71 -0
  160. package/src/adapters/opencode-context.ts +277 -0
  161. package/src/adapters/opencode-model-registry.ts +126 -0
  162. package/src/adapters/opencode-provider.ts +169 -0
  163. package/src/adapters/opencode-server-retry.ts +50 -0
  164. package/src/adapters/opencode-soft-steer.ts +302 -0
  165. package/src/adapters/opencode-spawn.ts +145 -0
  166. package/src/adapters/opencode-subscribe.ts +62 -0
  167. package/src/adapters/opencode-token-usage-capture.ts +187 -0
  168. package/src/adapters/opencode-tool-events.ts +302 -0
  169. package/src/adapters/opencode-tools-config.ts +17 -0
  170. package/src/adapters/opencode.ts +2264 -0
  171. package/src/adapters/project-shell.ts +80 -0
  172. package/src/adapters/provider-launch-config.ts +21 -0
  173. package/src/adapters/provider-quota-harvest.ts +127 -0
  174. package/src/adapters/service-access.ts +153 -0
  175. package/src/adapters/tmux-offload.ts +207 -0
  176. package/src/adapters/tool-summary.ts +214 -0
  177. package/src/agent-redact.ts +75 -0
  178. package/src/agent-runner/commands/dispatch.ts +185 -0
  179. package/src/agent-runner/commands/lifecycle.ts +84 -0
  180. package/src/agent-runner/commands/prompt.ts +284 -0
  181. package/src/agent-runner/credentials.ts +208 -0
  182. package/src/agent-runner/http-liveness.ts +85 -0
  183. package/src/agent-runner/lifecycle-milestones.ts +207 -0
  184. package/src/agent-runner/lifecycle-phase.ts +80 -0
  185. package/src/agent-runner/registration.ts +163 -0
  186. package/src/agent-runner/runner-context.ts +158 -0
  187. package/src/agent-runner/session-mirror.ts +278 -0
  188. package/src/agent-runner/task-claims.ts +116 -0
  189. package/src/agent-runner/token-usage-coverage.ts +54 -0
  190. package/src/agent-runner/turn-report.ts +269 -0
  191. package/src/api-budget-accounting.ts +91 -0
  192. package/src/area-memory.ts +255 -0
  193. package/src/artifact-path-materialize.ts +52 -0
  194. package/src/async-guard.ts +35 -0
  195. package/src/attachment-cache-sweep.ts +31 -0
  196. package/src/attachment-cache.ts +195 -0
  197. package/src/background-script-reconcile.ts +106 -0
  198. package/src/bounded-log.ts +59 -0
  199. package/src/bounded-spawn-sync.ts +18 -0
  200. package/src/busy-reconciler.ts +282 -0
  201. package/src/claim-tracker.ts +140 -0
  202. package/src/claude-settings.ts +167 -0
  203. package/src/claude-workspace-trust.ts +289 -0
  204. package/src/config.ts +547 -0
  205. package/src/context-boundary.ts +101 -0
  206. package/src/context-command.ts +151 -0
  207. package/src/context-probe-state.ts +6 -0
  208. package/src/continuation-archive.ts +179 -0
  209. package/src/control-server.ts +730 -0
  210. package/src/delivery-policy.ts +9 -0
  211. package/src/delivery-result.ts +24 -0
  212. package/src/effort-command.ts +125 -0
  213. package/src/empty-turn.ts +816 -0
  214. package/src/free-port.ts +25 -0
  215. package/src/fs-offload.ts +26 -0
  216. package/src/gate-run-config.ts +35 -0
  217. package/src/gate-run-lease-probe.ts +38 -0
  218. package/src/host-asset-plan.ts +450 -0
  219. package/src/host-claude-config-worker.ts +34 -0
  220. package/src/host-sources.ts +40 -0
  221. package/src/inbox-carrier.ts +7 -0
  222. package/src/index.ts +386 -0
  223. package/src/launch-assembly.ts +955 -0
  224. package/src/liveness.ts +105 -0
  225. package/src/logger.ts +108 -0
  226. package/src/mcp-outbox.ts +204 -0
  227. package/src/mcp-preflight.ts +86 -0
  228. package/src/mcp-source-selection.ts +54 -0
  229. package/src/mcp-tool-deferral.ts +35 -0
  230. package/src/mcp-vault-env.ts +25 -0
  231. package/src/message-body-config/config.ts +16 -0
  232. package/src/message-body-config.ts +1 -0
  233. package/src/mid-turn-delivery.ts +43 -0
  234. package/src/native-memory-lever.ts +329 -0
  235. package/src/native-self-resume.ts +244 -0
  236. package/src/outbox.ts +453 -0
  237. package/src/park-post.ts +17 -0
  238. package/src/pending-permission-store.ts +148 -0
  239. package/src/process-meta.ts +9 -0
  240. package/src/profile-home.ts +419 -0
  241. package/src/profile-projection.ts +411 -0
  242. package/src/project-hooks.ts +65 -0
  243. package/src/project-instructions.ts +119 -0
  244. package/src/provider-double.ts +59 -0
  245. package/src/provider-hold.ts +228 -0
  246. package/src/provider-home-marker.ts +3 -0
  247. package/src/provider-home-tombstone.ts +4 -0
  248. package/src/provider-input-queue.ts +134 -0
  249. package/src/provider-integration.ts +93 -0
  250. package/src/provider-processes.ts +17 -0
  251. package/src/provider-submit-failure.ts +15 -0
  252. package/src/providers.ts +35 -0
  253. package/src/provisioning.ts +703 -0
  254. package/src/question-decision.ts +18 -0
  255. package/src/quota.ts +22 -0
  256. package/src/rate-limit.ts +351 -0
  257. package/src/refused-prompt-store.ts +36 -0
  258. package/src/relay-injection-events.ts +161 -0
  259. package/src/relay-instructions.ts +107 -0
  260. package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
  261. package/src/relay-mcp-proxy.ts +704 -0
  262. package/src/relay-mcp.ts +583 -0
  263. package/src/reply-obligation-cache.ts +211 -0
  264. package/src/response-capture-report.ts +111 -0
  265. package/src/response-routing.ts +6 -0
  266. package/src/runner-core.ts +2955 -0
  267. package/src/runner-exit.ts +12 -0
  268. package/src/runner-helpers.ts +582 -0
  269. package/src/runner-insights.ts +105 -0
  270. package/src/runner-mcp-proxy.ts +46 -0
  271. package/src/runner-spawn-config.ts +68 -0
  272. package/src/runner-stale-scratch-sweep.ts +21 -0
  273. package/src/runner.ts +9 -0
  274. package/src/runtime-token-file.ts +45 -0
  275. package/src/runtime-token-remint.ts +56 -0
  276. package/src/service-access.ts +88 -0
  277. package/src/session-destroy.ts +27 -0
  278. package/src/session-insights.ts +118 -0
  279. package/src/session-scratch.ts +296 -0
  280. package/src/spawn-input.ts +147 -0
  281. package/src/startup-config-fetch.ts +48 -0
  282. package/src/subreaper.ts +113 -0
  283. package/src/template-resolver.ts +29 -0
  284. package/src/test-run-dedup.ts +435 -0
  285. package/src/transcript-context.ts +99 -0
  286. package/src/transport-errors.ts +48 -0
  287. package/src/turn-opener.ts +55 -0
  288. package/src/turn-reconcile-event.ts +88 -0
  289. package/src/vault-proxy-hop.ts +99 -0
  290. package/src/version.ts +44 -0
  291. package/src/web-tool-call-proxy.ts +128 -0
  292. package/src/web-tools.ts +59 -0
  293. package/README.md +0 -3
@@ -0,0 +1,189 @@
1
+ type EmitAgentMessage = (body: string) => void;
2
+ type ResponseEntry = { itemId?: string; body: string };
3
+ type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
4
+
5
+ const MAX_CAPTURED_RESPONSE_CHARS = 16_000;
6
+ // `\s` is defined as exactly the character set String#trim strips, so this tests
7
+ // `text.trim() !== ""` without allocating the trimmed copy. It is NOT free — matching forces a
8
+ // flatten of the accumulated buffer's rope — so it must stay off the per-delta hot path and is
9
+ // only used where the pre-#2504 whitespace guard actually changed the outcome.
10
+ const NON_WHITESPACE = /\S/;
11
+ const TRUNCATION_MARKER_PREFIX = "[truncated:";
12
+
13
+ export class CodexAgentMessageCapture {
14
+ private segments: ResponseEntry[][] = [];
15
+ private currentSegment: ResponseEntry[] = [];
16
+ private readonly buffers = new Map<string, string>();
17
+ private readonly flushedLengths = new Map<string, number>();
18
+ private readonly responseRecordedLengths = new Map<string, number>();
19
+
20
+ reset(): void {
21
+ this.segments = [];
22
+ this.currentSegment = [];
23
+ this.buffers.clear();
24
+ this.flushedLengths.clear();
25
+ this.responseRecordedLengths.clear();
26
+ }
27
+
28
+ appendDelta(itemId: string | undefined, delta: string): void {
29
+ if (!itemId || !delta) return;
30
+ this.buffers.set(itemId, `${this.buffers.get(itemId) ?? ""}${delta}`);
31
+ }
32
+
33
+ completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage, options: CompleteItemOptions = {}): void {
34
+ const fullText = text ?? (itemId ? this.buffers.get(itemId) : undefined);
35
+ this.flushText(itemId, fullText, emit);
36
+ if (options.includeInResponse === false || options.phase === "commentary") {
37
+ this.forgetResponseItem(itemId);
38
+ } else {
39
+ this.recordResponseText(itemId, fullText);
40
+ }
41
+ if (!itemId) return;
42
+ this.buffers.delete(itemId);
43
+ this.flushedLengths.delete(itemId);
44
+ }
45
+
46
+ closeSegment(emit: EmitAgentMessage): void {
47
+ this.flushPending(emit);
48
+ if (!this.currentSegment.length) return;
49
+ this.segments.push(this.currentSegment);
50
+ this.currentSegment = [];
51
+ }
52
+
53
+ // Deterministic turn-final response: prefer the trailing structured report span, not a generic
54
+ // post-handoff coda. The original #1079/#1086 trailing-run rule correctly kept early narration
55
+ // out, but it over-trimmed Codex turns shaped "summary -> workspace ready -> Landed..." and made
56
+ // report-up deliver only the coda (#1136). We still drop leading narration; we just let structured
57
+ // report markers beat a terse final status line.
58
+ finish(emit: EmitAgentMessage): string | undefined {
59
+ this.closeSegment(emit);
60
+ const trailingSegment = selectFinalResponseSegments(this.segments);
61
+ this.segments = [];
62
+ const text = trailingSegment ? trailingSegment.map(segmentBody).join("\n\n").trim() : undefined;
63
+ return truncateCapturedResponse(text);
64
+ }
65
+
66
+ private flushPending(emit: EmitAgentMessage): void {
67
+ // #2504 hot path: EVERY reasoning/plan delta calls closeSegment -> flushPending, so this runs
68
+ // thousands of times per turn. Bail before the defensive entry-array copy when there is no
69
+ // open agentMessage buffer (the common case) — that snapshot exists only so `emit` re-entering
70
+ // and mutating `buffers` cannot disturb the iteration, and there is nothing to iterate here.
71
+ if (!this.buffers.size) return;
72
+ for (const [itemId, text] of [...this.buffers.entries()]) {
73
+ this.flushText(itemId, text, emit);
74
+ this.recordResponseText(itemId, text);
75
+ }
76
+ }
77
+
78
+ private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
79
+ if (!text) return;
80
+ const offset = itemId ? (this.flushedLengths.get(itemId) ?? 0) : 0;
81
+ // #2504: `text` is the WHOLE accumulated item buffer and this is re-entered on every
82
+ // reasoning/plan delta. `.length` is O(1) on a rope, `.slice()`/`.trim()` are not — they
83
+ // flatten and copy the entire buffer. Skip both when nothing new has arrived, which is the
84
+ // overwhelmingly common case. Equivalent to the old code: slicing at/past the end yields ""
85
+ // and an empty body returned without emitting; the only other effect was rewinding the offset
86
+ // on a shorter (non-extension) text, preserved below.
87
+ if (itemId && text.length <= offset) {
88
+ if (text.length < offset) this.flushedLengths.set(itemId, text.length);
89
+ return;
90
+ }
91
+ const chunk = text.slice(offset);
92
+ if (itemId) this.flushedLengths.set(itemId, text.length);
93
+ const body = chunk.trim();
94
+ if (!body) return;
95
+ emit(body);
96
+ }
97
+
98
+ // #2504 root cause. This used to open with `const body = text.trim()` — a full flatten+copy of
99
+ // the entire accumulated agentMessage buffer — before branching, and on the itemId path `body`
100
+ // was then DEAD: that branch computes its own incremental `chunk` and pushes that. Because every
101
+ // reasoning/plan delta re-enters here via closeSegment -> flushPending with the same growing
102
+ // buffer, the copy ran per delta over the whole buffer: O(N^2) transient-string churn that
103
+ // fragmented the runner's allocator into multi-GB RSS bun never returned. Work here is now
104
+ // bounded by the number of NEW characters, and is O(1) when nothing new arrived.
105
+ private recordResponseText(itemId: string | undefined, text: string | undefined): void {
106
+ if (!text) return;
107
+ if (itemId) {
108
+ const offset = this.responseRecordedLengths.get(itemId) ?? 0;
109
+ // THE hot path — every reasoning/plan delta lands here with an unchanged buffer. `.length`
110
+ // is O(1); the pre-fix full-buffer trim was not, and its outcome was always "record
111
+ // nothing" anyway (slicing at the end yields an empty chunk, and rewriting the offset to
112
+ // the value it already holds is a no-op).
113
+ if (text.length === offset) return;
114
+ if (text.length < offset) {
115
+ // The item's text got SHORTER than what we recorded — a buffer re-opened by fresh deltas
116
+ // after completeItem, which clears `buffers` but deliberately keeps this offset. Nothing
117
+ // can be recorded from it; the pre-fix code only rewound the offset, and only for
118
+ // non-blank text.
119
+ if (NON_WHITESPACE.test(text)) this.responseRecordedLengths.set(itemId, text.length);
120
+ return;
121
+ }
122
+ // Only the NEW tail is examined. `slice` on the accumulated buffer is rope-aware in JSC and
123
+ // stays proportional to the tail; a regex probe over `text` would NOT be — it forces a full
124
+ // flatten of the rope on every delta, which is the very cost this fix exists to remove.
125
+ const chunk = text.slice(offset).trim();
126
+ // The pre-fix guard was `if (!text.trim()) return` — skip the offset advance while the item
127
+ // is still ENTIRELY whitespace. A non-blank chunk already proves the text is non-blank (the
128
+ // chunk is a substring of it), so the whole-text probe below is only reached for an all-blank
129
+ // tail, and then it stops at the buffer's first real character.
130
+ if (!chunk && !NON_WHITESPACE.test(text)) return;
131
+ this.responseRecordedLengths.set(itemId, text.length);
132
+ if (!chunk) return;
133
+ this.currentSegment.push({ itemId, body: chunk });
134
+ return;
135
+ }
136
+ const body = text.trim();
137
+ if (!body) return;
138
+ this.currentSegment.push({ body });
139
+ }
140
+
141
+ private forgetResponseItem(itemId: string | undefined): void {
142
+ if (!itemId) return;
143
+ this.responseRecordedLengths.delete(itemId);
144
+ this.currentSegment = this.currentSegment.filter((entry) => entry.itemId !== itemId);
145
+ this.segments = this.segments
146
+ .map((segment) => segment.filter((entry) => entry.itemId !== itemId))
147
+ .filter((segment) => segment.length > 0);
148
+ }
149
+ }
150
+
151
+ function segmentBody(segment: ResponseEntry[]): string {
152
+ return segment.map((entry) => entry.body).join("\n\n").trim();
153
+ }
154
+
155
+ function selectFinalResponseSegments(segments: ResponseEntry[][]): ResponseEntry[][] | undefined {
156
+ const bodies = segments.map(segmentBody).filter(Boolean);
157
+ if (!bodies.length) return undefined;
158
+ let end = bodies.length - 1;
159
+ while (end > 0 && isGenericFinalCoda(bodies[end]!) && bodies.slice(0, end).some(isSubstantiveReportSegment)) end--;
160
+ const firstReport = bodies.slice(0, end + 1).findIndex(isReportBoundarySegment);
161
+ if (firstReport >= 0) return segments.slice(firstReport, end + 1);
162
+ for (let i = end; i >= 0; i--) {
163
+ if (isSubstantiveReportSegment(bodies[i]!)) return [segments[i]!];
164
+ }
165
+ return [segments[end]!];
166
+ }
167
+
168
+ const REPORT_BOUNDARY = /(^|\n)(#{1,6}\s|\*\*(summary|what changed|rca|fix|tests?|gates?|verification|risk)\b|(?:summary|what changed|rca|fix|tests?|gates?|verification|risk):)/i;
169
+ const REPORT_STRUCTURE = /(^|\n)\s*[-*]\s+\S|`[^`]+`|\b(typecheck|bun test|tests? pass(?:ed)?|gates? green|rca|verified|implemented)\b/i;
170
+ const GENERIC_FINAL_CODA = /^(?:done|landed(?: on main)?|committed|merged|ready|no further action|nothing further|all set)\b|(?:\bno further action needed\b|\bnothing further needed\b)$/i;
171
+
172
+ function isReportBoundarySegment(body: string): boolean {
173
+ return REPORT_BOUNDARY.test(body.trim());
174
+ }
175
+
176
+ function isSubstantiveReportSegment(body: string): boolean {
177
+ const trimmed = body.trim();
178
+ return !isGenericFinalCoda(trimmed) && (trimmed.length >= 120 || isReportBoundarySegment(trimmed) || REPORT_STRUCTURE.test(trimmed));
179
+ }
180
+
181
+ function isGenericFinalCoda(body: string): boolean {
182
+ const trimmed = body.trim();
183
+ return trimmed.length <= 180 && GENERIC_FINAL_CODA.test(trimmed) && !isReportBoundarySegment(trimmed);
184
+ }
185
+
186
+ function truncateCapturedResponse(text: string | undefined): string | undefined {
187
+ if (!text || text.length <= MAX_CAPTURED_RESPONSE_CHARS) return text;
188
+ return `${text.slice(0, MAX_CAPTURED_RESPONSE_CHARS).trimEnd()}\n\n${TRUNCATION_MARKER_PREFIX} showing first ${MAX_CAPTURED_RESPONSE_CHARS} of ${text.length} chars]`;
189
+ }
@@ -0,0 +1,70 @@
1
+ import { errMessage } from "@humwick/sdk";
2
+ import type { SpawnArgs } from "../adapter";
3
+ import { CodexAppClient } from "./codex-client";
4
+
5
+ const STARTUP_TIMEOUT_MS = 60_000;
6
+ const STDERR_TAIL_CHARS = 8_192;
7
+
8
+ /** Connect only after our child announces its bound socket, not after a guessed delay (#3225). */
9
+ export async function spawnCodexAppServer(args: SpawnArgs, signal?: AbortSignal) {
10
+ signal?.throwIfAborted();
11
+ const appServerUrl = args.env.CODEX_APP_SERVER_URL!;
12
+ const appServer = Bun.spawn([args.command, ...args.args], {
13
+ cwd: args.cwd, env: args.env, stdin: "ignore", stdout: "inherit", stderr: "pipe",
14
+ });
15
+ const client = new CodexAppClient(appServerUrl);
16
+ const listening = Promise.withResolvers<void>();
17
+ const stderrReader = appServer.stderr.getReader();
18
+ let stderrTail = "";
19
+ let announced = false;
20
+ const stderrDone = (async () => {
21
+ const decoder = new TextDecoder();
22
+ try {
23
+ while (true) {
24
+ const { done, value: chunk } = await stderrReader.read();
25
+ if (done) return;
26
+ process.stderr.write(chunk);
27
+ stderrTail = (stderrTail + decoder.decode(chunk, { stream: true })).slice(-STDERR_TAIL_CHARS);
28
+ if (announced) continue;
29
+ const match = /^\s*listening on:\s*(ws:\/\/\S+)[\r\n]/m.exec(stderrTail);
30
+ if (!match) continue;
31
+ announced = true;
32
+ if (new URL(match[1]!).href !== new URL(appServerUrl).href) {
33
+ listening.reject(new Error(`Codex app-server listened on ${match[1]} instead of ${appServerUrl}`));
34
+ } else {
35
+ listening.resolve();
36
+ }
37
+ }
38
+ } finally {
39
+ stderrReader.releaseLock();
40
+ }
41
+ })();
42
+ // Keep draining after startup so the child cannot block on a full stderr pipe.
43
+ void stderrDone.catch((error) => listening.reject(error));
44
+ const exited = appServer.exited.then(async (code) => {
45
+ await stderrDone;
46
+ throw new Error(`Codex app-server exited during startup (code ${code}, signal ${appServer.signalCode ?? "none"})`);
47
+ });
48
+ const deadline = new AbortController();
49
+ const timer = setTimeout(() => deadline.abort(new Error(`Codex app-server startup timed out after ${STARTUP_TIMEOUT_MS}ms`)), STARTUP_TIMEOUT_MS);
50
+ const startupSignal = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
51
+ const aborted = Promise.withResolvers<never>();
52
+ const onAbort = () => aborted.reject(startupSignal.reason);
53
+ startupSignal.addEventListener("abort", onAbort, { once: true });
54
+ if (startupSignal.aborted) onAbort();
55
+ try {
56
+ await Promise.race([listening.promise, exited, aborted.promise]);
57
+ await Promise.race([client.connect(), exited, aborted.promise]);
58
+ return { appServer, appServerUrl, client };
59
+ } catch (error) {
60
+ client.close();
61
+ appServer.kill("SIGKILL");
62
+ await appServer.exited;
63
+ await stderrReader.cancel().catch(() => {});
64
+ await stderrDone.catch(() => {});
65
+ throw new Error(`${errMessage(error)}; endpoint ${appServerUrl}; app-server stderr: ${stderrTail.trim() || "(empty)"}`);
66
+ } finally {
67
+ clearTimeout(timer);
68
+ startupSignal.removeEventListener("abort", onAbort);
69
+ }
70
+ }
@@ -0,0 +1,362 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+
3
+ type JsonRpcId = number | string;
4
+
5
+ type JsonRpcRequest = {
6
+ id: JsonRpcId;
7
+ method: string;
8
+ params?: unknown;
9
+ };
10
+
11
+ type JsonRpcResponse = {
12
+ id: JsonRpcId;
13
+ result?: unknown;
14
+ error?: { code: number; message: string; data?: unknown };
15
+ };
16
+
17
+ type JsonRpcNotification = {
18
+ method: string;
19
+ params?: Record<string, unknown>;
20
+ };
21
+
22
+ type ThreadStatus =
23
+ | { type: "notLoaded" }
24
+ | { type: "idle" }
25
+ | { type: "systemError" }
26
+ | { type: "active"; activeFlags: string[] };
27
+
28
+ type TurnStatus = "completed" | "interrupted" | "failed" | "inProgress";
29
+
30
+ interface Turn {
31
+ id: string;
32
+ status: TurnStatus;
33
+ startedAt: number | null;
34
+ completedAt: number | null;
35
+ }
36
+
37
+ export interface Thread {
38
+ id: string;
39
+ cwd: string;
40
+ status: ThreadStatus;
41
+ updatedAt: number;
42
+ preview: string;
43
+ turns?: Turn[];
44
+ }
45
+
46
+ export type ClientEvent =
47
+ | { type: "notification"; message: JsonRpcNotification }
48
+ | { type: "server-request"; message: JsonRpcRequest }
49
+ | { type: "response"; message: JsonRpcResponse };
50
+
51
+ interface TurnStartResponse {
52
+ turn: Turn;
53
+ }
54
+
55
+ interface ThreadStartResponse {
56
+ thread: Thread;
57
+ }
58
+
59
+ interface ThreadResumeResponse {
60
+ thread: Thread;
61
+ }
62
+
63
+ interface ThreadReadResponse {
64
+ thread: Thread;
65
+ }
66
+
67
+ interface ThreadListResponse {
68
+ data: Thread[];
69
+ nextCursor: string | null;
70
+ }
71
+
72
+ interface ThreadLoadedListResponse {
73
+ data: string[];
74
+ nextCursor: string | null;
75
+ }
76
+
77
+ interface AccountRateLimitsResponse {
78
+ rateLimits?: unknown;
79
+ rate_limits?: unknown;
80
+ }
81
+
82
+ export const CODEX_APP_CLIENT_EVENT_CAP = 5_000;
83
+
84
+ // #2504: the frame ring is diagnostic-only (getEvents()), but a COUNT cap alone bounds nothing —
85
+ // a single app-server frame can carry a whole file diff or a large tool payload, so 5,000 frames
86
+ // is an unbounded number of bytes. Cap the retained frame text too and evict oldest-first. The
87
+ // budget is charged in raw frame characters; the parsed objects retained alongside them cost a
88
+ // multiple of that, so keep the ceiling modest. The newest frame is always retained even if it
89
+ // alone exceeds the budget, so an oversized frame can never empty the ring or spin the evictor.
90
+ export const CODEX_APP_CLIENT_EVENT_BYTE_CAP = 4_000_000;
91
+
92
+ /**
93
+ * #2539 Phase A — a JSON-RPC error response from the app-server, kept STRUCTURED. The client
94
+ * used to flatten every error into `"<message> (<code>)"`, which forced callers to classify
95
+ * rejections by message substring and made the turn/steer fallbacks (not-steerable vs
96
+ * stale-turn vs transport) indistinguishable from real faults. The message keeps the old
97
+ * `"<message> (<code>)"` shape so existing diagnostics and message-matching keep working.
98
+ */
99
+ export class CodexRpcError extends Error {
100
+ readonly code: number;
101
+ readonly data?: unknown;
102
+ constructor(code: number, message: string, data?: unknown) {
103
+ super(`${message} (${code})`);
104
+ this.name = "CodexRpcError";
105
+ this.code = code;
106
+ this.data = data;
107
+ }
108
+ }
109
+
110
+ // #1339: every JSON-RPC method the app-server exposes acknowledges promptly — turn/start
111
+ // returns the created turn immediately (turn progress/completion arrive later as separate
112
+ // notifications), compact/start acks and reports completion via thread/compacted, etc. So a
113
+ // request whose response never arrives (a half-open socket, a lost frame, an app-server that
114
+ // wedged mid-handling) is always a fault, never normal latency. Without a ceiling, request()
115
+ // would hang forever; because deliver() awaits turnStart under the runner's single-flight
116
+ // `delivering` latch, one hung request permanently wedges the whole input pump — the agent
117
+ // looks "unresponsive": input is accepted but no turn ever starts and nothing new drains.
118
+ // Reject after this ceiling so the hang surfaces (logged delivery failure) and the pump
119
+ // recovers by re-queuing. Generous vs. the sub-second normal ack, so no false timeouts.
120
+ export const CODEX_APP_CLIENT_REQUEST_TIMEOUT_MS = 30_000;
121
+
122
+ export class CodexAppClient {
123
+ private ws!: WebSocket;
124
+ private nextId = 1;
125
+ private pending = new Map<JsonRpcId, { resolve: (value: any) => void; reject: (err: unknown) => void }>();
126
+ // Each retained frame carries the raw character count it was charged, so eviction never has to
127
+ // re-measure — re-serializing frames inside record() would be its own per-frame leak.
128
+ private events: { event: ClientEvent; size: number }[] = [];
129
+ private eventBytes = 0;
130
+ private listeners = new Set<(event: ClientEvent) => void>();
131
+ private connected = false;
132
+ private connectionListeners = new Set<(connected: boolean) => void>();
133
+
134
+ constructor(
135
+ private readonly url: string,
136
+ private readonly log: (msg: string) => void = () => {},
137
+ private readonly requestTimeoutMs: number = CODEX_APP_CLIENT_REQUEST_TIMEOUT_MS,
138
+ ) {}
139
+
140
+ async connect(): Promise<void> {
141
+ if (this.connected) return;
142
+ await new Promise<void>((resolve, reject) => {
143
+ const ws = new WebSocket(this.url);
144
+ this.ws = ws;
145
+
146
+ ws.onopen = () => {
147
+ this.connected = true;
148
+ this.emitConnection(true);
149
+ resolve();
150
+ };
151
+ ws.onerror = (event) => reject(new Error(`websocket error: ${String((event as ErrorEvent).message || "unknown")}`));
152
+ ws.onclose = (event) => {
153
+ this.connected = false;
154
+ this.emitConnection(false);
155
+ const err = new Error(`websocket closed code=${event.code} reason=${event.reason || "(none)"}`);
156
+ for (const pending of this.pending.values()) pending.reject(err);
157
+ this.pending.clear();
158
+ };
159
+ ws.onmessage = (event) => this.handleMessage(String(event.data));
160
+ });
161
+ }
162
+
163
+ close(): void {
164
+ if (!this.ws) return;
165
+ this.ws.close();
166
+ }
167
+
168
+ isConnected(): boolean {
169
+ return this.connected;
170
+ }
171
+
172
+ async initialize(): Promise<unknown> {
173
+ return this.request("initialize", {
174
+ clientInfo: {
175
+ name: "agent-relay-runner-codex",
176
+ title: "Agent Relay Codex Runner",
177
+ version: "0.1.0",
178
+ },
179
+ capabilities: {
180
+ experimentalApi: true,
181
+ },
182
+ });
183
+ }
184
+
185
+ onEvent(listener: (event: ClientEvent) => void): () => void {
186
+ this.listeners.add(listener);
187
+ return () => this.listeners.delete(listener);
188
+ }
189
+
190
+ onConnectionChange(listener: (connected: boolean) => void): () => void {
191
+ this.connectionListeners.add(listener);
192
+ return () => this.connectionListeners.delete(listener);
193
+ }
194
+
195
+ getEvents(): ClientEvent[] {
196
+ return this.events.map((frame) => frame.event);
197
+ }
198
+
199
+ async settle(ms = 150): Promise<void> {
200
+ await delay(ms);
201
+ }
202
+
203
+ async threadStart(params: Record<string, unknown>): Promise<ThreadStartResponse> {
204
+ return this.request<ThreadStartResponse>("thread/start", params);
205
+ }
206
+
207
+ async skillsSetExtraRoots(extraRoots: string[]): Promise<Record<string, never>> {
208
+ return this.request<Record<string, never>>("skills/extraRoots/set", { extraRoots });
209
+ }
210
+
211
+ async threadResume(params: Record<string, unknown>): Promise<ThreadResumeResponse> {
212
+ return this.request<ThreadResumeResponse>("thread/resume", params);
213
+ }
214
+
215
+ async threadRead(threadId: string, includeTurns = false): Promise<ThreadReadResponse> {
216
+ return this.request<ThreadReadResponse>("thread/read", { threadId, includeTurns });
217
+ }
218
+
219
+ async threadList(params: Record<string, unknown>): Promise<ThreadListResponse> {
220
+ return this.request<ThreadListResponse>("thread/list", params);
221
+ }
222
+
223
+ async threadLoadedList(limit = 20): Promise<ThreadLoadedListResponse> {
224
+ return this.request<ThreadLoadedListResponse>("thread/loaded/list", { limit });
225
+ }
226
+
227
+ async threadCompactStart(threadId: string): Promise<Record<string, never>> {
228
+ return this.request<Record<string, never>>("thread/compact/start", { threadId });
229
+ }
230
+
231
+ async turnStart(threadId: string, text: string): Promise<TurnStartResponse> {
232
+ return this.request<TurnStartResponse>("turn/start", {
233
+ threadId,
234
+ input: [{ type: "text", text }],
235
+ });
236
+ }
237
+
238
+ async turnSteer(threadId: string, turnId: string, text: string, clientUserMessageId?: string): Promise<{ turnId: string }> {
239
+ return this.request<{ turnId: string }>("turn/steer", {
240
+ threadId,
241
+ expectedTurnId: turnId,
242
+ input: [{ type: "text", text }],
243
+ // #2539 Phase A — stable acceptance-correlation id (echoed back as the userMessage
244
+ // item's clientId). Absent keeps the legacy wire shape.
245
+ ...(clientUserMessageId ? { clientUserMessageId } : {}),
246
+ });
247
+ }
248
+
249
+ async turnInterrupt(threadId: string, turnId: string): Promise<Record<string, never>> {
250
+ return this.request<Record<string, never>>("turn/interrupt", { threadId, turnId });
251
+ }
252
+
253
+ async accountRateLimitsRead(): Promise<AccountRateLimitsResponse> {
254
+ return this.request<AccountRateLimitsResponse>("account/rateLimits/read");
255
+ }
256
+
257
+ respondToServerRequest(id: JsonRpcId, result: unknown): void {
258
+ if (!this.connected) {
259
+ throw new Error("websocket not connected");
260
+ }
261
+ this.ws.send(JSON.stringify({ id, result }));
262
+ }
263
+
264
+ rejectServerRequest(id: JsonRpcId, code: number, message: string, data?: unknown): void {
265
+ if (!this.connected) {
266
+ throw new Error("websocket not connected");
267
+ }
268
+ this.ws.send(JSON.stringify({ id, error: { code, message, ...(data !== undefined ? { data } : {}) } }));
269
+ }
270
+
271
+ private async request<T = unknown>(method: string, params?: unknown): Promise<T> {
272
+ if (!this.connected) {
273
+ throw new Error("websocket not connected");
274
+ }
275
+ const id = this.nextId++;
276
+ const payload: JsonRpcRequest = { id, method, params };
277
+ const promise = new Promise<T>((resolve, reject) => {
278
+ // #1339: cap the wait so a response that never arrives can't hang forever. The timer
279
+ // is armed here and cleared by settle() the moment the matching frame lands (or the
280
+ // socket closes and onclose rejects every pending). unref so a pending request never
281
+ // by itself keeps the process alive.
282
+ const timer = setTimeout(() => {
283
+ if (!this.pending.has(id)) return;
284
+ this.pending.delete(id);
285
+ this.log(`request timed out after ${this.requestTimeoutMs}ms: ${method} (id=${id})`);
286
+ reject(new Error(`codex request timed out after ${this.requestTimeoutMs}ms: ${method}`));
287
+ }, this.requestTimeoutMs);
288
+ (timer as { unref?: () => void }).unref?.();
289
+ const settle = (fn: (value: any) => void) => (value: unknown) => {
290
+ clearTimeout(timer);
291
+ fn(value);
292
+ };
293
+ this.pending.set(id, { resolve: settle(resolve), reject: settle(reject) });
294
+ });
295
+ this.ws.send(JSON.stringify(payload));
296
+ return promise;
297
+ }
298
+
299
+ private handleMessage(raw: string): void {
300
+ // The frame comes from an external process over a WebSocket; a partial or
301
+ // malformed frame must not throw out of onmessage and drop the connection.
302
+ let parsed: JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
303
+ try {
304
+ parsed = JSON.parse(raw) as JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
305
+ } catch {
306
+ this.log(`dropped malformed frame (${raw.length} bytes)`);
307
+ return;
308
+ }
309
+
310
+ if ("id" in parsed && ("result" in parsed || "error" in parsed)) {
311
+ const pending = this.pending.get(parsed.id);
312
+ if (pending) {
313
+ this.pending.delete(parsed.id);
314
+ if (parsed.error) {
315
+ // #2539 Phase A — reject with the STRUCTURED error (code + data preserved) so the
316
+ // adapter can classify turn/steer rejections; the message keeps the legacy shape.
317
+ pending.reject(new CodexRpcError(parsed.error.code, parsed.error.message, parsed.error.data));
318
+ } else {
319
+ pending.resolve(parsed.result);
320
+ }
321
+ }
322
+ this.record({ type: "response", message: parsed }, raw.length);
323
+ return;
324
+ }
325
+
326
+ if ("id" in parsed && "method" in parsed) {
327
+ this.log(`server-request ${parsed.method}`);
328
+ this.record({ type: "server-request", message: parsed }, raw.length);
329
+ return;
330
+ }
331
+
332
+ if ("method" in parsed) {
333
+ this.record({ type: "notification", message: parsed }, raw.length);
334
+ }
335
+ }
336
+
337
+ private record(event: ClientEvent, size: number): void {
338
+ this.events.push({ event, size });
339
+ this.eventBytes += size;
340
+ // Decide how many oldest frames both ceilings require dropping, then drop them in ONE splice —
341
+ // this runs on every received frame, so a second shift of a 5,000-entry array would itself be
342
+ // the kind of steady per-frame churn #2504 is about.
343
+ let drop = Math.max(0, this.events.length - CODEX_APP_CLIENT_EVENT_CAP);
344
+ let bytes = this.eventBytes;
345
+ for (let i = 0; i < drop; i++) bytes -= this.events[i]!.size;
346
+ // `drop < length - 1` retains the newest frame however large it is, so an oversized frame can
347
+ // neither empty the ring nor spin this loop.
348
+ while (bytes > CODEX_APP_CLIENT_EVENT_BYTE_CAP && drop < this.events.length - 1) {
349
+ bytes -= this.events[drop]!.size;
350
+ drop++;
351
+ }
352
+ if (drop) {
353
+ this.events.splice(0, drop);
354
+ this.eventBytes = bytes;
355
+ }
356
+ for (const listener of this.listeners) listener(event);
357
+ }
358
+
359
+ private emitConnection(connected: boolean): void {
360
+ for (const listener of this.connectionListeners) listener(connected);
361
+ }
362
+ }